Problem
The dispatcher's Reasoning Action (the ReasoningAction fallback that routes complex requests into an LLM reasoning loop) executes tool calls without asking the user for confirmation. Both reasoning adapters auto-approve every tool call:
- Claude:
canUseTool: async () => ({ behavior: "allow" }) + permissionMode: "acceptEdits" — packages/dispatcher/dispatcher/src/reasoning/claude.ts (~L584-586)
- Copilot:
onPermissionRequest: approveAll — packages/dispatcher/dispatcher/src/reasoning/copilot.ts (L15 import, L647)
- Shared adapter:
canUseTool: async () => ({ behavior: "allow" }) — packages/dispatcher/dispatcher/src/reasoning/claudeSDKAdapter.ts (~L155)
The reasoning loop exposes tools including execute_action (runs real, side-effecting dispatcher actions), discover_actions, search_memory, and remember. Because approval is hard-wired to allow-all, the loop can perform side-effecting actions (send email, modify lists, browser automation, etc.) with no human in the loop.
Desired behavior
Before executing a side-effecting reasoning tool call, prompt the user to confirm — the same UX GitHub Copilot and Claude use ("Allow / Deny", ideally with "Allow always for this tool/session"):
- Show the tool name and a human-readable summary of the arguments (e.g., which action + parameters
execute_action is about to run).
- Offer Allow once / Deny / Allow for session.
- Deny aborts that tool call and feeds the denial back into the loop so the model can adapt.
- Read-only tools (
discover_actions, search_memory) may run without a prompt; side-effecting tools (execute_action, remember, browser actions) prompt by default.
Suggested approach (implementation notes)
- Reuse
SessionContext.popupQuestion(message, choices, defaultId) (packages/dispatcher/dispatcher/src/execute/sessionContext.ts ~L222) — it already round-trips to the Shell/CLI/RPC clientIO.
- Add an SDK-agnostic gating hook to
ReasoningLoopConfig (packages/dispatcher/dispatcher/src/reasoning/reasoningLoopBase.ts), e.g. onToolApproval?(tool, args): Promise<"allow" | "deny" | "allow_always">. The base already has informational onToolCall/onToolResult callbacks (L31-32); this adds a gating counterpart.
- Wire real permission callbacks in both adapters instead of allow-all:
- Copilot: replace
onPermissionRequest: approveAll — copilot.ts L647.
- Claude: replace the
canUseTool allow-all — claude.ts ~L586 and claudeSDKAdapter.ts ~L155.
- Follow the workflow engine's secure-by-default
ApprovalFn precedent (examples/workflow/model/src/taskDefinition.ts ~L175, examples/workflow/engine/src/runner.ts; design doc docs/architecture/workflows/engineering/task-policy-temp.md).
- Add a policy/toggle under the existing
reasoning config (config.defaults.yaml ~L14): prompt (default) / allow / per-tool, plus an "allow all" escape hatch for headless runs (mirrors the workflow CLI --allow-all / --dry-run).
The auto-approve posture is already documented as deliberately temporary in docs/architecture/workflows/ir/decisions/0010-copilot-task-family.md §7, which points toward a capability-based permission model.
Acceptance criteria
Out of scope
- Full capability-based security model (tracked separately per decision 0010 §7).
- Per-agent permission policies beyond the reasoning loop.
Problem
The dispatcher's Reasoning Action (the
ReasoningActionfallback that routes complex requests into an LLM reasoning loop) executes tool calls without asking the user for confirmation. Both reasoning adapters auto-approve every tool call:canUseTool: async () => ({ behavior: "allow" })+permissionMode: "acceptEdits"—packages/dispatcher/dispatcher/src/reasoning/claude.ts(~L584-586)onPermissionRequest: approveAll—packages/dispatcher/dispatcher/src/reasoning/copilot.ts(L15 import, L647)canUseTool: async () => ({ behavior: "allow" })—packages/dispatcher/dispatcher/src/reasoning/claudeSDKAdapter.ts(~L155)The reasoning loop exposes tools including
execute_action(runs real, side-effecting dispatcher actions),discover_actions,search_memory, andremember. Because approval is hard-wired to allow-all, the loop can perform side-effecting actions (send email, modify lists, browser automation, etc.) with no human in the loop.Desired behavior
Before executing a side-effecting reasoning tool call, prompt the user to confirm — the same UX GitHub Copilot and Claude use ("Allow / Deny", ideally with "Allow always for this tool/session"):
execute_actionis about to run).discover_actions,search_memory) may run without a prompt; side-effecting tools (execute_action,remember, browser actions) prompt by default.Suggested approach (implementation notes)
SessionContext.popupQuestion(message, choices, defaultId)(packages/dispatcher/dispatcher/src/execute/sessionContext.ts~L222) — it already round-trips to the Shell/CLI/RPCclientIO.ReasoningLoopConfig(packages/dispatcher/dispatcher/src/reasoning/reasoningLoopBase.ts), e.g.onToolApproval?(tool, args): Promise<"allow" | "deny" | "allow_always">. The base already has informationalonToolCall/onToolResultcallbacks (L31-32); this adds a gating counterpart.onPermissionRequest: approveAll—copilot.tsL647.canUseToolallow-all —claude.ts~L586 andclaudeSDKAdapter.ts~L155.ApprovalFnprecedent (examples/workflow/model/src/taskDefinition.ts~L175,examples/workflow/engine/src/runner.ts; design docdocs/architecture/workflows/engineering/task-policy-temp.md).reasoningconfig (config.defaults.yaml~L14):prompt(default) /allow/ per-tool, plus an "allow all" escape hatch for headless runs (mirrors the workflow CLI--allow-all/--dry-run).Acceptance criteria
popupQuestion/clientIO.Out of scope