The web host now composes the sandboxed product path (sandbox-local + sandbox-policy behind bash-sandbox/fs-sandbox, with user-approval and permission on top); BootHostOptions.sandbox carries the deployment defaults (workspace-write + ask). createApiProxy owns the approval pending registry: a ctx.approval ask becomes an answerable approval/requested mux frame with a stable rpcId, replayed verbatim on every mux open until settled; respond routes by the echoed rpcId, validates the ApprovalResponsePayload audit correlation, and broadcasts approval/resolved; the ask's abort signal withdraws the question as cancelled. session.permissions / session.setPermission project ctx.permission into a protocol-owned PermissionOption select; idle switches are held last-write-wins and flushed into the next prompted turn (the ACP bridge's anchoring pattern). The shared hasOpenTurn fold moved to dsh-session, deduplicating the private copies in user-approval, the ACP bridge, and the proxy. Client, per the designer draft: a pending approval takes over the composer (ApprovalPanel replaces the InputBar — amber strip, justification headline, paired command, one-shot refuse/allow, keyed by rpcId so a queued second approval remounts live; the resolved frame restores the composer); the sidebar session row shows an amber waiting-approval dot that outranks the running ring (manager-tracked approvalId set, idempotent under mux-open replays, cleared per connection generation, lit for uninstantiated sessions too); the permission selector is a composer bottom-row chip over an invisible native select, with a presentation-only title-case transform (workspace-write renders as Workspace Write; wire names untouched). Question placeholders stay in the message flow. The connection fixture mirrors the host behavior for keyless browser acceptance.
5.7 KiB
Agent Note: Web UI 权限预设与审批应答
Status: implemented
English | 中文
问题
Web 承载层启动的是一个不受限的 agent(智能体):bootHost 组合了 dsh-bash-local 与 dsh-fs-local,因此每个 Web 会话都以完整文件访问权限运行,既无审批通道,也无权限管控——而 ACP 组合早在数月前就已交付完整的沙箱化产品路径(沙箱提供方 + 策略归属 + 受限的 bash/fs + 审批 + 预设)。Web 协议契约其实早已预留了对应位置——approval/requested/approval/resolved 的 mux 帧、携带 ApprovalResponsePayload 的 POST /api/respond、client 侧的 pendingBuffers——但 host 的 respond 只是一个 stub,没有应答者把 ctx.approval 桥接到流上,没有 RPC 暴露权限选择,PendingCard 把审批渲染成可见却无法应答的样子。
决策
Web 承载层组合与 acp-agent 相同的沙箱化产品路径:dsh-sandbox-local、dsh-sandbox-policy、dsh-bash-sandbox、dsh-fs-sandbox、dsh-user-approval 与 dsh-permission,由 BootHostOptions.sandbox 提供部署默认值(mode,默认 workspace-write;approvalPolicy,默认 ask)。
createApiProxy 拥有审批 pending 注册表。它的 approval/request waterfall(瀑布式事件)应答者从会话刚追加的 approval/asked 审计事件中读取审批 id(没有审计事件的 ask 属于外部通道,予以委托),为每个问题 mint 一个稳定的 rpcId,向每个打开的 mux 流广播可应答的 approval/requested 帧,并在每次 mux 打开时原样重放仍处于 pending 的帧——这正是契约早已承诺的刷新恢复基线。respond 按回显的 rpcId 路由,用既有的 zod schema 校验 ApprovalResponsePayload,将载荷的审计关联与所路由的条目交叉核对,解析应答者,并广播 approval/resolved;ask 的中断信号会以 cancelled 撤回该问题。
权限选择依托两个新的一元 RPC,session.permissions 与 session.setPermission,把 ctx.permission 投影为一个由协议拥有的 PermissionOption DTO(沿用 ACP bridge 的先例:每个协议拥有自己的呈现形状)。无权限的组合提供空的选择项,client 隐藏该控件。空闲期的切换以后写胜出(last-write-wins)的方式保存在 proxy 侧的 pending map 中,并在 agent/prompt-submit 时冲刷,因为旋钮事件必须保持轮次内闭合以支持持久回放;共享的 hasOpenTurn 折叠迁入 dsh-session,取代了 dsh-user-approval、ACP bridge 与 proxy 中各自的私有副本。
在 client 侧,Session 新增了 permissions 与 setPermission,审批应答则依托运行时的 PendingWait 载体。按照设计师草稿,处于 pending 的审批会接管 composer:ApprovalPanel 注册为由会话声明的 conversation.composer 链中一个按选择器路由的条目(即 ui-question 模式),以理由标题、配对的命令与一次性的拒绝/允许按钮取代 InputBar;ui-conversation 契约中的 PendingApproval 领域面拥有 ApprovalResponsePayload 在该载体上的协议编码(wire encoding),广播的 resolved 帧使该等待落定并恢复 composer。问题占位符仍留在消息流中。侧边栏用一枚琥珀色警示圆点同步呈现这一阻塞状态,且其优先级高于表示运行中的圆环:manager 跟踪每个会话尚未解决的 approvalId(对 mux 打开时的回放幂等,并按连接代次清除,以保证重开后的回放才是权威依据),而非读取 Session 实例,因此从未实例化过的会话也能点亮该圆点。composer 底行的 chip 经会话注入面挂载 PermissionSelect 控件。连接 fixture(测试前置数据)与 host 保持一致:它的常驻审批可应答一次,其权限选择项按会话持久保存。
曾考虑的替代方案
在 Web 协议上复用 ACP 的 session/set_config_option 形状。 不予采纳:Web 契约的一元方法注册表(RpcMethodMap + 逐方法的 zod schema)是它自成一体的方言;一个通用的 config-option 接口会为一个选择项绕开编译期锁定的 schema 表。一对专用方法让两侧都能从签名推导得出。
用一个会话事件承载 pending 审批,而非 proxy 侧注册表。 不予采纳:审批请求是瞬态的交互状态,而非持久的会话数据——approval/asked/decided 审计对已经记录了持久的那一半。持久化 requested 帧会在回放时重新问出已经作废的问题。
仅在存在 mux 订阅者时才注册应答者。 不予采纳:pending 条目必须在 client 断连后依然存活(刷新恢复正是要点所在),因此注册表的生命周期长于任何单个流;一个受订阅者门控的应答者,会让在重载窗口期间关闭的 ask 落空。
点击即乐观移除卡片。 不予采纳:广播的 resolved 帧才是真相;点击即移除会隐藏一个因拒绝回执或传输失败而仍然悬置的问题。面板改为在本地禁用其按钮,并在失败时重新启用。
后果
Web 会话现在从受限状态启动(默认 workspace-write + ask),一次沙箱拒绝的升级会以可应答的卡片形式抵达浏览器;部署方可以通过 BootHostOptions.sandbox 放宽或收紧默认值,无需触动装配。问题应答已通过同一注册表模式单独交付(ui-question 基于问题 pending 表)。权限选择在每次挂载时读取一次;来自另一个 client 切换的实时刷新暂缓实现。覆盖情况:proxy 注册表与权限 RPC 的单元测试套件、会话对象与 fixture 的单元测试套件,以及无密钥 Web 冒烟测试在真实浏览器中演练 fixture 模式的审批应答与预设切换。