Aster文档
搜索 Aster 文档
ESC
产品
版本

输入关键词开始搜索。

    文档目录

    ASTER CODE / 构建终端智能体

    04 · 修改操作确认

    区分权限决策与确认交互,给终端增加准确绑定到工具调用的批准和拒绝操作。

    启用需要确认的修改

    前三章只允许 read 和 ls。本练习允许读取自动执行,修改和命令执行则需要确认。PermissionPolicy 负责权限决策,ConfirmationManager 负责处理用户回答。

    架构图FIG. 07 · 权限与确认的分工权限策略先决定拒绝、允许或询问。确认面板回答的是准确的一次工具调用,超时不会产生批准。
    一次确认如何回到原调用策略决定 Ask 后,宿主等待明确回答。计时器不会生成批准。1234请求确认显示准确参数回答本次请求绑定原调用 IDCore宿主用户Core宿主用户
    1. 01先绑定调用

      确认请求携带会话、工具 ID 和实际参数。

      permission_ask.rs L11 ↗
    2. 02再取得回答

      界面显示本次调用。回答必须返回原请求。

    3. 03最后恢复原调用

      Core 接收匹配回答后继续判定。超时保持未授权。

    下载 SVG ↗

    权限检查的顺序是 Deny、Allow、Ask、默认决策。因此同一个工具不能同时在 Allow 和 Ask 里期待后者生效。界面也不能因为用户按过一次“允许”,就替下一次不同参数的调用自动批准。

    在运行层保留确认管理器

    在 runtime.rs 添加 Runtime,并用下面的 open 替换第一章的版本。arguments() 保留。

    use std::sync::Arc;
    use aster_core::AgentEvent;
    use aster_core::hitl::{ConfirmationManager, ConfirmationPolicy, TimeoutAction};
    use tokio::sync::broadcast;
    
    pub struct Runtime {
        pub agent: Agent,
        pub session: AgentSession,
        pub confirmations: Arc<ConfirmationManager>,
        pub confirmation_events: broadcast::Receiver<AgentEvent>,
    }
    
    pub async fn open(config: &std::path::Path, workspace: &std::path::Path)
        -> anyhow::Result<Runtime>
    {
        let config = config.to_str().context("ACL 路径必须是 UTF-8")?;
        let workspace = workspace.to_str().context("项目路径必须是 UTF-8")?;
        let agent = Agent::new(config).await?;
        let (tx, rx) = broadcast::channel(64);
        let confirmations = Arc::new(ConfirmationManager::new(
            ConfirmationPolicy::enabled().with_timeout(60_000, TimeoutAction::Reject), tx,
        ));
        let mut policy = PermissionPolicy::new()
            .allow_all(&["read", "ls"])
            .ask("write").ask("edit").ask("patch").ask("bash");
        policy.default_decision = PermissionDecision::Deny;
        let options = SessionOptions::new()
            .with_planning(false)
            .with_permission_policy(policy)
            .with_confirmation_manager(confirmations.clone());
        let session = agent.session_async(workspace, Some(options)).await?;
        Ok(Runtime { agent, session, confirmations, confirmation_events: rx })
    }
    

    确认事件发送到练习程序创建的 broadcast 通道。安装外部管理器不会把事件自动转发到 stream_with_run 的通道,界面必须同时消费两个来源。agent 字段用于保留宿主对象的寿命。

    main.rs 相应改为保留 Runtime。同时给 ui::run 增加 confirmations: &ConfirmationManager 和 confirmation_events: &mut broadcast::Receiver<AgentEvent> 两个参数,放在 session 与 terminal 之间:

    mod runtime;
    mod ui;
    
    #[tokio::main]
    async fn main() -> anyhow::Result<()> {
        let (config, workspace) = runtime::arguments()?;
        let mut runtime = runtime::open(&config, &workspace).await?;
        let mut terminal = ratatui::init();
        let result = ui::run(&runtime.session, &runtime.confirmations,
            &mut runtime.confirmation_events, &mut terminal).await;
        ratatui::restore();
        runtime.session.close().await;
        result
    }
    

    确认面板保存准确的调用

    给 App 加入 pending: Vec<PendingConfirmationInfo>,类型来自 aster_core::hitl。面板至少显示工具名称、准确的 tool_id、当前参数和剩余时间。参数可能含凭证或私人内容,正式应用应按工具的结构化规则脱敏,不能只把全部 JSON 写进日志。

    在 ui.rs 添加这个等待函数,再在原 tokio::select! 中增加 pending = confirmation_update(confirmations, confirmation_events) 分支,把 app.pending 更新为 pending?:

    use aster_core::hitl::{ConfirmationManager, PendingConfirmationInfo};
    use tokio::sync::broadcast;
    
    async fn confirmation_update(manager: &ConfirmationManager,
        events: &mut broadcast::Receiver<AgentEvent>)
        -> anyhow::Result<Vec<PendingConfirmationInfo>>
    {
        match events.recv().await {
            Ok(_) | Err(broadcast::error::RecvError::Lagged(_)) => {}
            Err(broadcast::error::RecvError::Closed) => {
                manager.cancel_all().await;
                anyhow::bail!("确认事件通道已关闭");
            }
        }
        let mut pending = manager.pending_confirmation_details().await;
        pending.sort_by(|a, b| a.tool_id.cmp(&b.tool_id));
        Ok(pending)
    }
    

    排序只用于展示,批准依据始终是当前显示记录的 ID。先展示列表中的第一项,后续可以增加选择控件;界面显示哪一项,按键就只能回答那一项。

    如果广播报告 Lagged,重新查询待确认列表,不要用漏掉的事件猜当前状态。通道关闭时暂停批准操作并显示错误。确认面板不能在按键处理里阻塞等待;模型工作线程正在等它回答。

    批准与拒绝都要检查回执

    给 F6 和 F7 分别绑定批准和拒绝,按键处理调用下面的函数。tool_id 必须来自当前选中的 PendingConfirmationInfo,而不是工具名称或模型回复。

    async fn respond(
        manager: &aster_core::hitl::ConfirmationManager,
        tool_id: &str,
        approved: bool,
    ) -> anyhow::Result<()> {
        let accepted = manager.confirm(tool_id, approved, None).await
            .map_err(anyhow::Error::msg)?;
        anyhow::ensure!(accepted, "确认已过期或已处理,请刷新待确认列表");
        Ok(())
    }
    

    按键时先复制选中 ID,再等待回执。管理器返回 false 表示没有可处理的那次确认;界面不能继续显示“已批准”。超时使用拒绝策略,关闭会话时也要取消待确认记录。

    在键盘分支确认是 Press 后、处理普通字符前,调用下面的函数。返回 true 时本次按键已处理,不再进入原来的输入分支;调用后也重新查询待确认列表。

    async fn confirmation_key(manager: &ConfirmationManager,
        pending: &[PendingConfirmationInfo], key: crossterm::event::KeyCode)
        -> anyhow::Result<bool>
    {
        let approved = match key {
            crossterm::event::KeyCode::F(6) => true,
            crossterm::event::KeyCode::F(7) => false,
            _ => return Ok(false),
        };
        if let Some(selected) = pending.first() {
            let tool_id = selected.tool_id.clone();
            respond(manager, &tool_id, approved).await?;
        }
        Ok(true)
    }
    

    给原来的 Layout 增加一块确认区域,显示当前工具、ID、参数、剩余时间和 F6/F7 提示。参数展示与正式日志分开处理;如果请求到达后看不到这块区域,先检查独立广播是否已接入,而不是放宽权限。

    过期确认属于可恢复的界面错误。键盘分支应把 confirmation_key 的错误写入状态栏并刷新列表,不要用 ? 直接退出整个终端。

    完整面板的按键与限制

    参考工程的 ui.rs 已把列表、选择、刷新和绘制接在同一个事件循环里。F5 在待确认项之间切换,选择依据是 tool_id;F3/F4 滚动参数,F6/F7 批准或拒绝显示的那一项。列表更新后优先保留相同 ID,原项消失才切换到另一项。

    显示参数超过 32 KiB 时,面板明确提示并禁止批准;拒绝仍可用。截断不能隐藏实际执行内容后继续让用户确认。这个限制仅属于练习界面,不修改 Core 的工具参数。

    业务问题单独处理:F8 选择选项,F9 提交输入框文字或当前选项。终端不便发送功能键时,Alt+j 切换确认,Alt+y/Alt+n 批准/拒绝,Alt+o/Alt+a 选择/提交业务回答。回答只送给原 question_id,不调用 confirm。结构化表单由 ACP 客户端教程处理,这个小型 TUI 会明确提示不支持。

    验收

    让 Agent 在练习项目中新建一个文件:批准前文件不存在;F7 拒绝后仍不存在;再次请求并 F6 批准后才出现。再让确认超时,检查没有执行开始事件,也没有磁盘修改。最后同时触发两次确认,确认批准一个 ID 不会放行另一个。

    需要宿主直接调用工具时,使用 governed_tool 保留这些检查。tool() 表示宿主已经完成授权,不能拿它绕过刚做好的确认面板。

    相关文档