Skip to content

fix(permission): ask once when approving a worker's frozen call - #1567

Merged
TheGreatAxios merged 1 commit into
mainfrom
cl-10768-let-the-approve_worker_call-prompt-approve-the-workers
Oct 25, 2026
Merged

TheGreatAxios merged 1 commit into
mainfrom
cl-10768-let-the-approve_worker_call-prompt-approve-the-workers

Conversation

@bwachman

@bwachman bwachman commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • The approve_worker_call prompt shows the worker's frozen call (tool, arguments, and which worker) and prompts on every use. It offers no standing grant, and a saved rule for it is ignored with a warning in the log.
  • On the top-level parent, approving that prompt answers the frozen call's ordinary ask, so a worker's denied call reaches the operator once. A forced ask (a sensitive or restricted target, or a plugin-policy ask) still prompts on its own, and a deny still blocks.
  • The approval supersedes a decline the operator gave the same call earlier in the turn. The cached decline stays in place, so an identical retry nobody approved is still denied, and other calls' cached declines are untouched. A forced ask declined earlier in the turn prompts again when approved, and a second decline denies it.
  • A nested orchestrator's approve_worker_call runs without an operator prompt, so its frozen call keeps full evaluation.

Verification

  • bun run check passes (lint, typecheck, dead exports, build, 9468 tests)
  • New tests cover: no standing grant, a saved rule ignored (including queue drain), a deny still blocking, an ordinary ask collapsing into one prompt, each forced-ask second prompt, a denied worker call reaching the operator exactly once, decline then approve of the same call, an unapproved retry after an approval, an approval leaving a different call's decline in force, and a declined forced ask prompting again
  • Live, in interactive QA sessions on codex/plus / gpt-5.6-terra with this commit stacked: approve_worker_call prompted on 6 of 6 uses; 2 ordinary worker actions needed one prompt each; 3 commands touching ~/.corbits raised the sensitive-path second prompt. Details are on the Linear issue.

Fixes CL-10768

@linear-code

linear-code Bot commented Oct 24, 2026

Copy link
Copy Markdown

CL-10768

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fleet review verdict: REQUEST CHANGES

PR: #1567 — fix(permission): ask once when approving a worker's frozen call (CL-10768)

Convergence: swarm → defect / quality / solve convergence → all three converged request-changes.

Blocker — confirmed, must fix

Operator approval of a declined worker's frozen call is vetoed by the stale decline cache.

  • The approve_worker_call path reaches PermissionGate.evaluateApprovedWorkerCall(frozenCall).
  • That seam answers an ordinary ask directly, but it consults the denial-aware decide() first, so a cached decline from the same turn still returns { allowed: false }.
  • Verified with a temporary repro (decline → cached denial; approve-via-seam → still denied; clearDenials() → allowed). Temporary test deleted; no permanent edits made.

Net effect: an operator's approval is converted into a denial, and the worker stays blocked until the denial cache clears.

Required change to merge

Clear / override the cached decline on the approval path (approval supersedes a prior decline), while keeping the cache as a same-turn retry guard only.

Why it matters

The PR's core success criterion — approving a pending worker call once — is unachievable after a decline in the same turn.

Verification status

  • Unit: approve-worker-call suite green; decline-then-approve repro confirmed the defect.
  • Live QA: not rerun here (inference credentials unavailable in the swarm environment).

The approve_worker_call prompt shows the worker's frozen call and
prompts on every use: it offers no standing grant, and a saved rule for
it is ignored. On the top-level parent, approving it answers the frozen
call's ordinary ask, so the operator is asked once per worker action. A
forced ask (a sensitive or restricted target, or a plugin-policy ask)
still prompts on its own, and a deny still blocks.

A nested orchestrator's approve_worker_call runs without an operator
prompt, so there the frozen call keeps its full evaluation.
@bwachman
bwachman force-pushed the cl-10768-let-the-approve_worker_call-prompt-approve-the-workers branch from 84130e2 to 5cf5e9e Compare October 24, 2026 20:44
@bwachman

Copy link
Copy Markdown
Contributor Author

Operator approval of a declined worker's frozen call is vetoed by the stale decline cache: fixed. evaluateApprovedWorkerCall now decides the frozen call with an approved option that skips the cached-decline lookup for that call only, so the operator's approval through approve_worker_call supersedes a decline they gave the same call earlier in the turn. The cache entry itself is left in place, so an identical retry that nobody approved is still denied with the original reason, and no other call's cached decline is touched. Hard denies and the ignored saved approve_worker_call rule behave as before. Forced asks (sensitive path, restricted target, plugin-policy ask) still prompt on their own, with one visible change: a forced ask the operator declined earlier in the turn now prompts again when approved through approve_worker_call, instead of being denied from the cache; a second decline still denies it. New tests in src/permission/approve-worker-call.test.ts cover decline then approve (now allowed, no extra prompt), the unapproved retry after an approval (still denied from the cache), an approval of one call leaving a different call's cached decline in force, and the declined forced ask that prompts again.

@bwachman

Copy link
Copy Markdown
Contributor Author

Resolved findings at 5cf5e9ef, with line citations

Finding Status Where
Blocker: the stale decline cache vetoes the operator's approval Resolved decide takes { approved } (src/permission/gate.ts:708-715). When approved is true, only the cached-decline lookup is skipped, and the entry stays for an unapproved retry (:760). evaluateApprovedWorkerCall is the only caller that passes approved: true (:1207-1214). Every other path passes false (:1201, :1266, :1303). The approved path is used only when approvalPrompted is true (src/subagent/lifecycle-tools.ts:745-751), which is set only for the top-level toolset (src/agent/tools.ts:631).
Tests for the fix Added In src/permission/approve-worker-call.test.ts: a decline superseded by approval (:238), an unapproved retry still denied (:249), another call's decline left in force (:261), and a declined forced ask prompting again (:281).

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at 5cf5e9e. Approving a frozen worker call now supersedes a same-turn cached decline, and the decline still blocks an unapproved retry. Approving.

@corbits-triage

Copy link
Copy Markdown
Contributor

Triage: Pass

No changes needed from you. A maintainer will review this pull request.

@TheGreatAxios
TheGreatAxios merged commit e6a82d9 into main Oct 25, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants