[N4] Permission modes: plan, acceptEdits and dontAsk, switchable mid-session - #315
Merged
Merged
Conversation
…session Presets over permission rules and needsApproval, applied last in the tool-call gate; session.setPermissionMode() with onPermissionModeChange; editsFiles marker; sub-agents inherit the lead's mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lead Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md # src/memory/withMemory.ts
This was referenced Oct 2, 2026
main was 45 KB under the 14 MiB cap; N4 adds ~98 KB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #215
What
Permission modes as presets over the permission rules and
needsApproval, applied last in the tool-call gate (gateToolCall()):PermissionMode = 'default' | 'plan' | 'acceptEdits' | 'dontAsk';permissionMode?: PermissionMode | (() => PermissionMode)onPermissionOptions(so oncreateAgent(),AgentExecutor.execute()/stream()andresumeAfterApproval());send()/stream(){ permissionMode }for one run;agent.session({ permissionMode }),session.setPermissionMode()and thesession.permissionModegetter.plan: a tool that is not read-only is denied (kind: 'denied', the ticket's reason), also when anallowrule matched. Read-only =metadata.mcp.annotations.readOnlyHint === true, plus the built-inask_questionandtask(marked internally withallowInPlanMode(), so a user tool with the same name is not trusted). Default closed for tools that say nothing. Unknown tools keep the not-found error. A run that starts in plan mode gets one system-prompt paragraph.acceptEdits: a call that would ask runs when its tool hasmetadata.editsFiles(newdefineTool({ editsFiles }), set onwrite_file/edit_file; the N7: workspace rewind - snapshot files before write_file / edit_file, rewind by turn #309 checkpoint wrapping is unchanged).dontAsk: a call that would ask (askrule,needsApproval,ask_question) is denied; theapprovecallback is never called.denyrule,needsApprovaldeny and guardrail blocks are decided before the mode.readOnlyHint: trueadded toload_skill,recall_<name>,agent_status,agent_await.checkPermission()'s early return accounts for it);PermissionDecisionEntry.modeis set when the mode changed the outcome. Mode switches are explicit (setPermissionMode()) and recorded through the newonPermissionModeChange({ sessionId, from, to, at })(agent option, defaulted into sessions).permissionModeadded toInheritedRuntimeandchildOptions(). A sub-agent runs under the lead's mode unless the lead's is'default'(then its own); a sub-agent whose own mode is'plan'always stays in plan mode. Read at each child tool call, so a lead switch applies to running sub-agents. Remote sub-agents cannot inherit:taskto one is refused in plan mode (without a request), and indontAska remote approval fails the task instead of pausing the lead (pausable: false).agent.approvals.resolve()/streamResolve()reads the paused session's current mode (a getter), else the agent's. In plan mode, a call approved before the switch is refused too (audited). The mode is not saved in checkpoints.Docs
docs/permission-modes.md(headings: The modes; Which tools are read-only; File edits: theeditsFilesmarker; Order of evaluation; Setting and switching the mode; The audit log; Sub-agents; Resuming after an approval). Needs a docs-sitePAGESentry, navigation in both languages and an Arabic translation (G9).docs/approvals.md("Permission policies": one link paragraph; the audit sentence mentions modes),docs/tools.md(defineTool()options table:annotations,editsFilesrows),docs/stream-events.md(permission.decisionrow),docs/sub-agents.md("What a sub-agent inherits":permissionModerow),README.md(docs table row).examples/plan-mode/+examples/README.mdentry +example:plan-modescript.Live test
Not run: the OpenRouter account is out of credit (
/api/v1/credits:total_credits: 10,total_usage: 10.1995, checked 2026-10-02). Live test spend: 0.src/execution/permissionModes.live.test.tsis merged and skips withoutLOUSHO_RECORD+ key or the cassette; the recording (src/execution/__fixtures__/cassettes/n4-plan-mode.json) and the CI replay test are added to #260.Outside the ticket: pack-smoke size cap
origin/main(4a612be) packs to 14,634,428 bytes unpacked, 45 KB underpack-smoke's 14 MiB cap; this PR adds ~98 KB (source maps ~41 KB, JS ~21 KB,.d.ts~8 KB, shippedsrc/~17 KB, docs ~10 KB), sopack-smokefailed withtarball unpacked size 14732295 exceeds 14680064. Commit 73a8341 raisesMAX_UNPACKED_BYTESto 16 MiB (a check threshold; nothing about what is published changes) with a CHANGELOG "Tests" line. Opened #316 for the owner: keep raising the cap, or shrink the package (maps withsourcesContentplus shippedsrc/).Verification
Run on the branch after merging
origin/mainup to 4a612be (#313, #310 = the run-loop ticket, #312, #314):npx tsc --noEmitnpm run lintnpm run build/--workspace=packages/create-lousho-agentnpm run test:typesnpm run docs:verify-snippets -- --skip-buildnpm run docs:llms:checknpm run test:coveragenpm run fallowtypecheck/typecheck:servertest -- --run/test:servernpm run pack-smokenpx tsx examples/plan-mode/index.tsNew tests:
src/execution/permissionModes.test.ts(45: the mode table for all 4 modes x 7 tool kinds, auditmodefield andpermission.decisionevent, plan system-prompt paragraph, unknown tool, deny precedence,dontAsk+approve, function mode, sub-agent inheritance incl. remote refusal, mid-turn session switch, resume aftersetPermissionMode('dontAsk')/('plan')),src/execution/permissionModes.test-d.ts, aneditsFilestest infsTools.test.ts. A mutation check (plan deny removed) failed 11 of them.An earlier full run on this branch had two failures that pass when run alone and are not in code this PR touches:
src/tools/built-in/http.test.ts"rejects near the configured timeout" (known timing flake under load) and the Docker integration test insrc/security/SubprocessSandbox.test.ts("No such container", a daemon shared with other agents).🤖 Generated with Claude Code