Skip to content

feat(work): add persistent auto-approval and exact-call Pi MCP grants - #234

Merged
Lucas1479 merged 10 commits into
mainfrom
codex/feat-pi-work-scoped-approvals
Oct 20, 2026
Merged

Lucas1479 merged 10 commits into
mainfrom
codex/feat-pi-work-scoped-approvals

Conversation

@Desolate-Jix

@Desolate-Jix Desolate-Jix commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Repeated MCP polling currently requires approval on every call. This adds two explicit choices, with automatic approval off by default:

  • Persistent automatic approval: Settings → Providers → Work and Chat approval can automatically answer permitted execution confirmations across Work runs, Cooperative chats, Providers and application restarts, including MCP and Shell. Permission-card shortcuts require confirmation before enabling this global setting; disabling it remains immediate.
  • Exact-call approval: Pi MCP cards can allow matching calls for the current run, bound to the connection/configuration, original tool/schema and all argument values. Grants remain visible and individually revocable; completion, cancellation and session restoration do not retain authority.

Authority and persistence

Automatic approval only replaces a confirmation within the Host's existing permission boundary. Cooperative request options, manual approval and automatic authorization share the same eligibility check: deny remains denied, and read-only contexts still reject Shell, file-write and generic tool execution. External MCP calls under ask remain approvable in read-only contexts; eligible background chats do not need to be visible.

Before an automatic response is dispatched, the authoritative Work or Cooperative owner must find the exact committed active permission request and commit its policy decision. EventBus broadcast completion is not persistence acknowledgement. Missing owners, ordinary request/decision write failures and cancelled checkpoints fail closed. A committed decision records Host authorization intent, not proof that the Provider executed the operation.

Automatic attempts are reserved before awaiting the owner; unknown responses are never resent. Settled request identities stay fenced until their run ends, including when an expiry write fails. Turning the setting off after a decision commits does not discard that response; subsequent requests wait for approval. User decisions remain immutable, workspace limits remain enforced and external tools retain their own gates.

Validation

Current revision: c12692efa79e62ca692965448ef9e30b40965321 (permission fix: 9c7ef962). Integrated main 02f485ab7ea4119beac19241831356912d709287, including #225's Pi model-metadata changes and #237.

  • 73 permission regressions passed, including a real Runtime/EventBus/SQLite matrix for deny, read-only Shell/file-write/generic tools, permitted MCP and writable Shell, changed owner limits, hot enable, background chats, and dispatch after a committed decision.
  • The nine related integration modules initially produced 168 passed / 1 failed, with no skips. The failure was a Canvas assertion naming the old settings group; after correcting it, the full Canvas module produced 2 passed. All eight real pinned Pi scoped-approval subprocess tests passed in that integration run, using synthetic loopback model/MCP services.
  • Full Electron tests initially produced 530 passed / 1 failed because new translation keys were unsorted; after restoring the required ordering, 531 passed. Production build passed, including config, extension SDK and Pi TypeScript checks.
  • Main advanced during this repair. After retaining both sides of the generated .env.example conflict and regenerating the catalog, 145 Python permission/configuration/Canvas tests passed, 535 Electron tests passed, and the production build passed again. The permission implementation and Pi runtime were unchanged by this final main integration.
  • Full-repository Ruff, generated architecture/README checks and diff checks passed. Before/after English and Chinese renders of the actual permission component were visually checked.
  • All 16 hosted checks passed at c12692efa79e62ca692965448ef9e30b40965321, across all six workflows: Python Windows (including all three full-suite shards, desktop smoke and the aggregate gate), Python Linux, Python macOS, Electron Windows, optional character RAG and local-model installation candidates. Current Windows CI. Earlier green runs remain historical and are not substituted for this head's results.

Verification history, exact commands, limitations and rendered evidence retain the original failures and subsequent repairs. No external model calls or real desktop tool execution were used.

Closes #224. The maintainer-approved scope retains both current-run exact-call grants and the explicit global policy; the global policy cannot override the Host's permission limits.

@Lucas1479 Lucas1479 left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed revision: 7bd966f30baf1e6e764088a1de9a7d8a8d4afeb4.

There is a permission-persistence boundary defect that needs to be fixed before merging; see the inline comment. The new automatic-approval logic treats a return from EventBus.emit() as confirmation that the permission record was committed, but the bus catches ordinary subscriber exceptions and returns normally. Injecting a permission-table write failure still results in an automatic allow response being sent to the Provider.

The independent reproduction uses this revision's actual ProviderRuntime, EventBus, WorkLedgerCoordinator, and a temporary SQLite store, with the repository's existing synthetic _InteractiveAdapter as the Provider. It makes no external model requests and executes no real tools:

  1. Register the coordinator, configure Runtime to use coordinator.prepare_request, and start a synthetic Work.
  2. Enable WORK_PERMISSION_AUTO_APPROVE.
  3. Replace only store.create_permission_request with a function that raises sqlite3.OperationalError("synthetic permission persistence failure").
  4. Publish permission.requested through runtime._emit, with a request_id, mcp.tool.call, and ["allow_once", "deny"].
  5. Inspect that Work's permission records and adapter.responses.
Control Permission records Automatic allow responses
Automatic approval off; write succeeds 1 0
Automatic approval on; write succeeds 1 (allowed) 1
Automatic approval on; write fails 0 1 (defect)

The third case should not dispatch approval. The three contract probes produced 2 passed / 1 failed; the error log identifies the create_permission_request exception caught by EventBus. Gate automatic approval on the actual commit result from the authoritative permission owner. Fix this at the owning boundary and cover ordinary write failures as well as the existing cancellation and hot-enable cases. Completion of a general event broadcast must not stand in for persistence acknowledgement, and responses with an unknown outcome must not be resent.

Other validation: 529 Electron tests and the production build passed. The selected Python regressions initially produced 62 passed / 1 failed. In the failing case's synthetic session, the first two tool calls returned Tool ... not found; rerunning that native module alone produced 8 passed. The initial failure and rerun are recorded separately. This does not establish another defect in the permission implementation and is not counted as an entirely green initial run. All 16 remote checks for this head succeeded.

Product scope is confirmed: the maintainer explicitly agreed to retain both exact-call approval for the current Work and global automatic approval across Work runs and application restarts, including Shell, in this PR. The global switch remains off by default; splitting the PR is not required. The persistence defect above still needs to be fixed; scope approval does not replace technical acceptance.


复核提交:7bd966f30baf1e6e764088a1de9a7d8a8d4afeb4。

发现一项合并前需修复的权限持久化边界问题,见行内评论。新增自动审批逻辑把 EventBus.emit() 返回视为权限记录已提交,但该总线会捕获普通订阅异常并正常返回。注入权限表写入失败后,仍会向 Provider 发送自动允许响应。

独立复现使用本提交的真实 ProviderRuntime、EventBus、WorkLedgerCoordinator 和临时 SQLite store,Provider 使用仓库现有的合成 _InteractiveAdapter;无外部模型请求或真实工具执行:

  1. 注册 coordinator,令 Runtime 使用 coordinator.prepare_request,启动一个合成 Work。
  2. 开启 WORK_PERMISSION_AUTO_APPROVE。
  3. 仅将 store.create_permission_request 替换为抛出 sqlite3.OperationalError("synthetic permission persistence failure") 的函数。
  4. 经 runtime._emit 发布带 request_id、mcp.tool.call、["allow_once", "deny"] 的 permission.requested。
  5. 读取该 Work 的权限表和 adapter.responses。
对照 权限记录数 自动允许响应数
自动审批关闭,写入正常 1 0
自动审批开启,写入正常 1(allowed) 1
自动审批开启,写入失败 0 1(缺陷)

预期第三种情况不派发批准。三组契约探针结果为 2 passed / 1 failed;错误日志定位到 EventBus 捕获的 create_permission_request 异常。请让权威权限记录的实际提交结果约束自动审批,在 owning boundary 修复,覆盖普通写入异常以及既有取消/热启用情形;不要用通用事件广播完成替代持久化确认,也不要重发结果未知的响应。

其他验证:529 项 Electron 测试、生产构建通过。选定 Python 回归首轮 62 passed / 1 failed;失败用例的合成会话中前两次工具调用是 Tool ... not found,该原生模块单独复跑 8 passed。首次失败与复跑分别记录,未据此认定权限实现有另一项缺陷,也未将其算作全绿。远端本 head 16 项检查成功。

产品范围已经确认:维护者明确同意在本 PR 同时保留“本次 Work 的精确调用授权”和“跨 Work、跨重启、包含 Shell 的全局自动审批”,全局开关默认关闭,不要求拆 PR。上述持久化缺陷仍需修复,产品范围确认不替代技术验收。

Comment thread agent_host/provider_runtime.py Resolved

@Lucas1479 Lucas1479 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复核提交:9f58b5e5ec5d4213826713e533e1484c570a0d31。

Request changes:发现一个仍需修复的 P1——全局自动审批越过了 Cooperative 手动审批的权限边界。此前“权限记录写入失败仍派发自动批准”的 P1 已有修复,本次四个既有权限回归模块全部通过;这里报告的是另一条授权边界缺陷。

[P1] 自动审批只能替代确认,不能扩大可授权范围

server/cooperative_chat_ingress.py:4668–4703 中,ask_user 已编码 permission_policy == "ask" 和只读上下文限制,但新增的 auto_approve 不受它约束。开关开启后,本应拒绝的请求仍被持久化为带 allow_once 的 pending 请求,并跳过拒绝路径。随后 authorize_automatic_permission(4520–4563)接受 deny 策略,也不检查 workspace 只读限制,将决定提交为 policy_allowed 并交给 Runtime 派发。

手动入口则在 4799–4800 拒绝非 ask 策略,在 4840–4842 拒绝只读上下文中的非 MCP 操作。同一条 pending 记录因此出现“用户手动不能允许、自动授权却返回 True”。这违背了 ARCHITECTURE.md:22–23 中不改变 workspace capabilities 的承诺。

独立复现与对照

使用本提交的真实 ProviderRuntime、EventBus、CooperativeChatManager、持久 Cooperative context 和临时 SQLite,Provider 使用仓库现有的 InteractiveNativeFixture。测试前逐一校验了相关目录的 706 个 Python 文件与被审提交一致。环境:Windows x64、CPython 3.12.10、pytest 8.4.2、pytest-asyncio 1.4.0。

复现步骤:

  1. 复用 tests/test_cooperative_context_recovery.py 的 host_factory,用其 InteractiveNativeFixture 作为支持双向权限响应的合成 Provider。在启动上下文之前设置对应的 workspace_access,并设置 manager 的 permission_policy。
  2. 将 manager 的 _handle_provider_event 注册到真实 EventBus,将 authorize_automatic_permission 注册为 Runtime 的自动授权函数。
  3. 分别关闭/开启 WORK_PERMISSION_AUTO_APPROVE,通过 runtime._emit 发布 permission.requested。请求包含固定的合成 request ID、下表 capability,以及 options=["allow_once", "deny"]。
  4. 读取 SQLite 权限记录与合成 Provider 的 permission_responses。另以四组受限条件停在请求已持久化、尚未自动派发的边界,在同一条 pending 记录上先调用手动 resolve_permission(..., decision="allow_once"),再调用 authorize_automatic_permission。
Cooperative 条件 开关关闭:记录 / Provider 响应 开关开启:记录 / Provider 响应
deny + write + shell.execute denied / allow=False allowed / allow=True
deny + write + mcp.tool.call denied / allow=False allowed / allow=True
ask + read + shell.execute denied / allow=False allowed / allow=True
ask + read + file.write denied / allow=False allowed / allow=True
ask + read + mcp.tool.call(合法对照) pending / 无响应 allowed / allow=True
ask + write + shell.execute(合法对照) pending / 无响应 allowed / allow=True

四组同记录对照中,手动入口分别返回 cooperative_permission_interaction_unavailable 或 cooperative_permission_read_only_context,自动授权却全部返回 True。四组越界允许记录还同时包含:

resolution_policy=automatic_deny
resolution=policy_allowed

按预期权限契约断言,独立探针共 8 passed / 8 failed:12 组开关矩阵中 4 组失败,加上 4 组同记录手动/自动一致性断言失败。这是同一缺陷的多组验证,不是八个独立问题。以下四个既有模块另行运行,结果 53 passed:

python -m pytest -q tests/test_cooperative_scoped_permissions.py tests/test_automatic_permission_owners.py tests/test_automatic_permission_persistence.py tests/test_provider_automatic_permissions.py

其中 test_cooperative_scoped_permissions.py:75–99 明确断言“只读 Shell 在全局开关开启后可变为 allowed”,因此现有绿灯不能证明权限边界正确。当前 head 的 16 项远端检查也已核实通过。

本次验证到 Host 向合成 Provider 派发允许这一层,没有执行真实 Shell/file.write、调用外部模型或重跑真实 Pi 子进程测试。最初的本地沙箱运行卡在 Windows asyncio 的本机 socketpair 初始化,尚未进入权限逻辑;移到允许本机通信的环境后完成上述结果,不将该环境问题计入产品缺陷。

建议修复与验收

请求选项生成、手动审批和自动授权应共用所属 Host 的权限资格判断。自动审批不能覆盖 deny,也不能允许只读上下文中的非 MCP 操作。将上述反向断言改为拒绝,并补齐 deny 用例;保留“ask + read + MCP”和“ask + write + Shell”正向对照,避免把现有合法操作一并禁止。UI 是否当前可见等交互条件可保留在各自入口,不应机械复制整个手动交互函数来阻止后台自动审批。

同时明确 Cooperative 是否属于这个开关的产品范围:配置和按钮写的是“所有 Work”,但实际也作用于聊天。若范围仅限 Work,应在拥有者路由处排除 Cooperative;若包含聊天,应明确文案,并仍保留上述权限边界。此前已同意的跨 Work、跨重启、包含 Shell 的全局策略,不等于同意覆盖 deny/只读限制。

补充说明(低优先级,不另列阻塞项)

  • agent_host/provider_runtime.py:1960–1961 在拥有者已经提交 allowed 后再次检查开关;若此时开关变为关闭,会直接返回而不给 Provider 答复,并且本次 automatic attempt 已被占用。当前两个生产授权函数均为同步调用,因此没有复现这条竞态,也不将它升级为当前可达的阻塞缺陷。建议删除提交后的重复检查,使授权决定与派发保持一致。
  • server/app.py:541 和 1893 两次设置自动授权函数,后者覆盖前者;可清理初始化阶段的冗余注册。
  • 审批卡直接提供“始终自动批准所有 Work”,点击会持久化覆盖 MCP/Shell 的全局设置。是否加一次确认属于产品选择,建议明确呈现持久范围;不把它与本次 P1 混算。
  • 复核时 main 为 7094366dbe5f97b314231d0d2884f8818956fa02,本分支落后 main 6 个提交(包含 #225),已不是此前的 3 个。只读三方合并检查未见文本冲突;这不替代集成后的 Pi 回归。

Comment thread server/cooperative_chat_ingress.py Resolved

@Lucas1479 Lucas1479 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

按维护者本轮明确授权,批准合并已验证的提交 c12692e。此前两个 P1 均已修复:自动响应必须先由权威拥有者确认权限记录与决定落库,且自动审批遵守手动审批的 deny/只读权限边界。相关回归、真实 Pi 子进程、145 项本地 Python 检查、535 项 Electron 测试与生产构建已验证;此 head 的 16 项远端检查全部通过。此次批准替代之前针对已修复问题的 Request changes。

@Lucas1479
Lucas1479 merged commit 8746938 into main Oct 20, 2026
16 checks passed
@Lucas1479
Lucas1479 deleted the codex/feat-pi-work-scoped-approvals branch October 20, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pi: optional revocable Work-scoped approvals for repeated MCP calls

2 participants