Repository navigation
feat(permissions): one named permission-mode vocabulary and two admission gates - #1935
Conversation
aad7dbb to
494f216
Compare
494f216 to
2c56527
Compare
2c56527 to
bd9a699
Compare
bd9a699 to
2446c27
Compare
8e6459b to
363f183
Compare
|
I think we should go further with the convergence here. If I choose What I'd propose is five permission modes that actually change the session's behavior:
Explicit deny and deliberately configured human-required approval rules should still apply in every mode. Permission modes also remain separate from execution isolation: automatic approval does not imply sandboxing. Workspace trust should be a separate yes/no decision, rather than another permission mode. That removes The UI could show something like For switching, the server should define which modes a session is allowed to select, and selecting one should apply its full behavior within that boundary. Switching from I'd also avoid translating an empty checker setting into This is a larger change than adding startup presets: it needs session-specific effective policy bounded by server policy, rather than a fixed process-wide posture doing most of the work. But it addresses the actual UX problem. One control for what the agent may do, a separate decision for whether workspace instructions are authoritative, and mode switches that do what their labels say. |
92de54a to
e6f425c
Compare
d2413c5 to
4acf7fe
Compare
4acf7fe to
98fc96f
Compare
JAORMX
left a comment
There was a problem hiding this comment.
The move to five real session modes and per-session guardrail composition is the right direction. The separate trust indicator and improved mode confirmation/refusal handling help too. But some permission layers and callers still use the old process-posture assumptions, so the selected mode does not yet reliably describe what will execute.
This follows the proposal in #1935 (comment). Reviewed the full change with independent spec, standards, test, security, architecture/API, UX, deployment and reuse reviewers. The branch was rebased during review; git range-diff confirms all 22 PR patches are unchanged. This review is pinned to 98fc96fa2f8be61916de443a17262adc50c050d0. Findings are from source tracing and CI inspection, not locally executed reproductions.
What needs fixing before merge
The inline comments cover persisted-session admission, checkerless default-auto, filesystem permissions surviving a downgrade, the incomplete TypeScript/API migration, trust still being implied by startup mode, wrapper checker opt-outs, and composer blocking.
There is one additional execution-path blocker outside a changed hunk: cmd/mecatequi/run.go:166 still calls CreateSession(ctx, session.ModeDefault, limits). It never inherits the mode resolved from the CLI. Since allow-all is now mode-driven, auto/yolo runs can ask and cancel on ordinary mutations, accept-edits does not take effect, and plan loses its hard mutation restriction (a configured Allow can authorize a write under the incorrectly selected default mode). Pass the unspecified mode so the service default applies, and test the actual CLI/composition/run boundary. Source:
Lines 165 to 166 in 98fc96f
The requested pending-approval behavior is also absent: engine/session/session.go:1188 rejects mode changes in StateAwaiting, and the TUI only cycles at idle/running. This is safely fail-closed; I am not claiming a stale verdict runs under a newly accepted mode. But it does not implement the requested ability to apply a mode transition and re-evaluate a pending ask. We should implement that boundary or explicitly agree on a narrower contract rather than describe it as completed.
Standards and test adequacy
internal/app/build.go:9165,9251still tells the model there are exactly three session modes and advertises the removed trusted tokens. Update the real prompt factories and test the five-mode semantics through them. The user permissions guide, keybinding reference, action input help andmecatequirecovery message also retain removed tokens or old posture behavior. The PR description still claims seven modes and no engine/proto changes.- Advertise runtime-mode support through a stable feature identifier. An older server can already advertise auto/yolo posture, so posture does not establish support for the new mode enums. Document the actual wire and stored-session compatibility decision; additive enum numbering alone is not enough.
internal/adapter/server/mode_admission_test.go:152–163currently requires checkerless default-auto to succeed. That is a contrary acceptance condition, not coverage of the requested guarantee.- The strongest new guardrail switch test changes mode before the first run. Add production-path runs before and after switches, persisted-session reload under a tighter ceiling, external file effects, and delegated children. Include planted configured Deny/Ask rules and positive controls.
- Wrapper tests that only record argv and policy tests supplied with a mode manually miss callers such as
mecatequi. The UI tests also explicitly require the composer blocking we asked to remove.
Other follow-ups
These are nonblocking findings, separate from the enforcement and compatibility blockers above:
- Ordinary subagents/parallel branches still construct default-mode children (
engine/agent/subagent.go:4201,engine/agent/parallel.go:1187). The new inheritance comment is not backed by propagation, so a yolo parent's substitution behavior can turn into child asks/review/denials. Children retain their existing builtin allow-all floor; this is not a claim that all child mutations fail. - ACP explicitly creates default sessions and retains its three-mode picker (
internal/adapter/acp/agent.go:296,321,462). Define and migrate that integration's mode behavior. - The blocked-mode/help guidance says yolo needs a checker, although only auto has that prerequisite.
- Existing Helm
extraArgswith--posture=strictnow conflict with the template's unconditional--permission-mode=auto. Add a chart-level mode choice and migration guidance. - The legacy posture-to-replacement-token mapping is copied across three command packages; reuse the composition mapping rather than maintaining three switches.
- Exposing reviewer delegation in
/posturewould help discovery. Startup diagnostics already disclose it, so this is not a blocker.
I am not carrying forward the initial concern about the chart's old default image as a release blocker: release automation synchronizes the chart/image versions. Nor am I treating the pre-existing HTTP unknown-mode fallback as a newly introduced escalation.
CI on this rebased head is still running, with the two Node SDK jobs already failing. The request for changes is based on the source-level defects above, not red checks alone. The immediate priorities are admission on restored execution, enforcing auto consistently, and removing the remaining startup-posture authority from runtime mode changes.
| // UNSPECIFIED mode stays empty so Service.createSession applies the server's | ||
| // configured DefaultMode (the session half of the operator's permission mode), | ||
| // matching the HTTP create path. | ||
| func createModeFromProto(m mecatlv1.PermissionMode) session.PermissionMode { |
There was a problem hiding this comment.
Blocker: this changes an existing wire contract, independently of whether adding enum values is protobuf-compatible. Previously an unspecified gRPC create produced DEFAULT; it can now produce AUTO/YOLO on the same permissive deployment. Existing conforming clients can reject those snapshots. An old client explicitly requesting DEFAULT also loses the former process-wide allow-all behavior now that policy is session-driven.
Please assess this against docs/architecture/api-surface.md:61–89. Preserve the existing contract or obtain explicit approval for a coordinated breaking transition with client/server upgrade and stored-session implications. We also need a stable feature identifier for runtime-mode support; the old posture field does not provide it. The latest commit's additive assessment and the PR body's 'no engine/proto change' statement do not account for these observable changes.
There was a problem hiding this comment.
Assessed and approved by the operator as a break: unspecified creates resolve to the server default (possibly AUTO/YOLO), and an explicit DEFAULT no longer inherits process-wide allow-all. The major stays at 1 by the operator's decision. Discovery is via the new session_permission_modes feature identifier. The API compatibility statement in the PR description now covers the old and new contracts, affected clients, and stored-session and upgrade implications. User-facing upgrade notes are in permissions-and-posture.md#upgrade-from-the-posture-model.
b83dad4 to
9d276cc
Compare
|
Thanks for the thorough review. Inline blockers are answered in their threads; all changes are in 9d276cc. The rest of the review body: Blockers
Standards / tests
Non-blocking follow-ups (done)
Known remaining
|
mecated.md said --guardrails off lets auto start without a checker; it is refused, and unchecked autonomy is yolo. --trust-project no longer talks about raising the posture. The upgrade notes add that an older TypeScript SDK sends an explicit default over HTTP when no mode is given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… server posture The escape decision already keyed on the call's session mode, but the workspace that serves an approved lexical ../ operand was built once per root with a server-posture flag. A default session on a yolo server was served an approved relative escape, and a yolo session lost it after the server posture changed. The factory workspace now never serves relative escapes. The run-entry funnel binds a per-run view through the new optional server.ModeScopedWorkspace capability; escapeWorkspace.ForPermissionMode enables relative serving only for a yolo run, fails closed for any other or unknown mode, and keeps child views confined. The mode is fixed per run because SetMode is refused mid-turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… agent Only yolo selects the loose substitution evaluator, so under auto the main agent's unprovable $(...), backtick, and subshell commands ask, as on subagents. The governance table, the user guide, and the auto startup log still said the main agent loses the guard. Fix them, call out the change from --posture auto in the upgrade notes, and record the refused-mode recovery exception in the domain model's mid-turn mode-switch invariant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An upgrade with the default permissionMode: auto and no checker rendered fine, then crash-looped because mecak8s refuses auto without an enforcing checker; Recreate releases went down. The chart now fails at render time unless guardrails.model is set or the new guardrails.fromSettings asserts that mounted settings bind the checker, and it rejects auto with guardrails.disabled. extraArgs can no longer carry --guardrails or --guardrails-model, and the checker choices are mutually exclusive. Fixtures, deploy:check sets, unit suites, and the quick start now declare a checker choice so they keep rendering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reusable workflow only forwarded the deprecated posture input, with a default of "auto" that differed from the action's empty default, so every call warned and callers could not migrate. It now forwards a permission-mode input and defaults posture to empty. The example workflow and the microVM dev command no longer run auto without a checker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chart now refuses to render permissionMode auto without a guardrails checker. The continuous and release lint/render steps used overrides with no checker choice, so they would fail. Declare guardrails.fromSettings, and name a checker model in the published install command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cak8s and mecatequi The three commands each carried their own copy of --permission-mode validation, the deprecation warning, and the usage line, and the help texts had drifted: mecatequi omitted the ceiling table and the --guardrails off refusal, and only mecak8s mentioned --guardrails off. Move them into app.PermissionModeFlags and app.PermissionModeHelp. The help text is now one shared line, parameterized only by what really differs: the default token, a one-shot session, the root/sandbox refusal (mecatequi has none), and the --yolo alias (mecated only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot remove The header's always-shown badge carries project trust, and the permission mode appeared only in the template's mode segment, so the Minimal template or a custom header could hide that a session runs auto or yolo. Add renderer-owned chrome beside the trust badge: amber "⚠ auto" or the YOLO alarm pill for a confirmed auto/yolo session, or for the process-wide posture on a server without session_permission_modes. A header too narrow for every indicator sheds the tail and trust badge and truncates the identity line, but keeps this one. The default templates' mode segment is now plain text, so the alarm is not shown twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… mode, not the posture The shared engine's advisory demotion fell back to cfg.Posture >= yolo, and Built.GuardrailsAdvisory, the guardrails startup line, and the permission-mode narration keyed on the posture tier. Key the shared engine on the server's default session mode (an unresolved mode stays enforcing), the advisory flag and the startup line on that same demotion, and the narration on the ceiling. Drop the guardrails-off UNSUPERVISED note for auto: checkerRefusal now refuses auto without an enforcing checker before that line is logged, and the note claimed auto runs without one. Pin that an auto agent-bound session on a yolo server keeps an enforcing checker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-all grant The PostureAuto/PostureYolo comments still said those tiers grant allow-all and loosen substitution, and server.Config.Posture called the capability display-only. Allow-all and loose substitution follow each session's mode; the posture tier tracks the ceiling and bounds which modes a client may cycle to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ender The chart refuses to render permissionMode auto without a guardrails checker, so this render fixture needs guardrails.fromSettings like the other chart fixtures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The action defaults to auto, which needs an enforcing guardrails checker. With an empty guardrails-model and no operator settings file, or with "off", the binary refused only after setup. Fail the step first with an error naming guardrails-model and the yolo alternative. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the thorough review, @jhrozek. Everything is addressed in the latest push except the crash-orphan recovery, which moves to #2425. I replied on each thread. For the items outside the diff:
One thing I found along the way: ApprovePlan with UNSPECIFIED approves, while the proto says it denies. It's the same on main, so I'll handle it in #2425. |
…cted TestUnavailableModeKeepsComposerUsable accepted any returned command as proof that enter sent the draft, and TestUnavailableModeDoesNotHoldQueuedPrompt never ran the drain's command. Both now execute the returned commands and check the transport: idle sends exactly one Prompt frame carrying the draft, running stages exactly that draft and sends nothing, and the drain sends one Prompt frame with the queued follow-up. Follow-up agreed in the review of #1935. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestPermissionModeTokenDerivesNoTrust only read the trust_project narration. It now also seeds an AGENTS.md sentinel, runs one turn of a server-default session through the real Build, and checks whether the sentinel reaches the provider request. No --permission-mode token feeds untrusted instructions to the model, --trust-project does, and the deprecated --posture yolo trust floor still does. Follow-up agreed in the review of #1935. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session saves overwrite whole snapshots, and the session lease is the only thing that keeps two replicas off the same session. A store other processes may share (a remote driver or a store without its own lease) with no lease configured got no exclusion and no warning, and a backend that reports leasing unsupported downgraded with only an INFO line. The under-lease snapshot adoption in #1935 is a no-op in both setups. Build now logs a WARN naming the lease flags when it resolves no lease for any store other than the process-private memstore (a local StoreDir already gets the flock lease). The run-entry and child-session lease-unsupported downgrades log at WARN instead of INFO; the staleness sweep's downgrade stays INFO since it only disables the sweep. Follow-up agreed in the review of #1935 (the pre-existing concurrency limitations). The storage-level gaps (no fencing token or aggregate version on Save) are out of scope here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The --permission-mode help text in internal/app/permission_mode_flags.go changed in #1935 without regenerating the reference, so task docs:server-cli:check reported drift. Regenerated with task docs:server-cli; no flag behavior changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cted TestUnavailableModeKeepsComposerUsable accepted any returned command as proof that enter sent the draft, and TestUnavailableModeDoesNotHoldQueuedPrompt never ran the drain's command. Both now execute the returned commands and check the transport: idle sends exactly one Prompt frame carrying the draft, running stages exactly that draft and sends nothing, and the drain sends one Prompt frame with the queued follow-up. Follow-up agreed in the review of #1935. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestPermissionModeTokenDerivesNoTrust only read the trust_project narration. It now also seeds an AGENTS.md sentinel, runs one turn of a server-default session through the real Build, and checks whether the sentinel reaches the provider request. No --permission-mode token feeds untrusted instructions to the model, --trust-project does, and the deprecated --posture yolo trust floor still does. Follow-up agreed in the review of #1935. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session saves overwrite whole snapshots, and the session lease is the only thing that keeps two replicas off the same session. A store other processes may share (a remote driver or a store without its own lease) with no lease configured got no exclusion and no warning, and a backend that reports leasing unsupported downgraded with only an INFO line. The under-lease snapshot adoption in #1935 is a no-op in both setups. Build now logs a WARN naming the lease flags when it resolves no lease for any store other than the process-private memstore (a local StoreDir already gets the flock lease). The run-entry and child-session lease-unsupported downgrades log at WARN instead of INFO; the staleness sweep's downgrade stays INFO since it only disables the sweep. Follow-up agreed in the review of #1935 (the pre-existing concurrency limitations). The storage-level gaps (no fencing token or aggregate version on Save) are out of scope here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The --permission-mode help text in internal/app/permission_mode_flags.go changed in #1935 without regenerating the reference, so task docs:server-cli:check reported drift. Regenerated with task docs:server-cli; no flag behavior changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cted TestUnavailableModeKeepsComposerUsable accepted any returned command as proof that enter sent the draft, and TestUnavailableModeDoesNotHoldQueuedPrompt never ran the drain's command. Both now execute the returned commands and check the transport: idle sends exactly one Prompt frame carrying the draft, running stages exactly that draft and sends nothing, and the drain sends one Prompt frame with the queued follow-up. Follow-up agreed in the review of #1935. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestPermissionModeTokenDerivesNoTrust only read the trust_project narration. It now also seeds an AGENTS.md sentinel, runs one turn of a server-default session through the real Build, and checks whether the sentinel reaches the provider request. No --permission-mode token feeds untrusted instructions to the model, --trust-project does, and the deprecated --posture yolo trust floor still does. Follow-up agreed in the review of #1935. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session saves overwrite whole snapshots, and the session lease is the only thing that keeps two replicas off the same session. A store other processes may share (a remote driver or a store without its own lease) with no lease configured got no exclusion and no warning, and a backend that reports leasing unsupported downgraded with only an INFO line. The under-lease snapshot adoption in #1935 is a no-op in both setups. Build now logs a WARN naming the lease flags when it resolves no lease for any store other than the process-private memstore (a local StoreDir already gets the flock lease). The run-entry and child-session lease-unsupported downgrades log at WARN instead of INFO; the staleness sweep's downgrade stays INFO since it only disables the sweep. Follow-up agreed in the review of #1935 (the pre-existing concurrency limitations). The storage-level gaps (no fencing token or aggregate version on Save) are out of scope here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage: Implementation. Implements the Plan / Interface contract in #1730, as revised by the review proposal in #1935 (comment). Stacked on #1730.
What it does
Five session permission modes that each change what the session may do:
plan,default,accept-edits,auto,yolo.--permission-mode(onmecated,mecatui,mecak8s,mecatequi) and the operator-tierpermissionMode:key set the mode new sessions start in and the server ceiling, the most permissive mode a session may switch to (accept-editsfor plan/default/accept-edits;auto;yolo).autokeeps the command-substitution guard for the main agent and children), the out-of-workspace path-escape decision, and serving a relative../path after an approved escape all follow the session's current mode, so switching fromyolotodefaultrestores ordinary approvals. Guardrail demotion and startup narration follow the session mode or the server ceiling, not the legacy posture. Deny rules and configured asks win in every mode.autoneeds an enforcing checker at startup, on create, and on SetMode; missing,--guardrails off, and advisory-only all refuse. Unchecked autonomy is an explicityolo, where a configured checker is advisory.FailedPrecondition) at run, retry, approval resume, fork, clear, and MCP-authorization continue before any provider or tool work, history untouched, until the owner switches it.session.PermissionMode.Rankorders all five modes and replaces the local rank tables, so an agent definition'spermissionModeceiling clamps auto/yolo requests (explicit or from the server default), and fork/clear of an agent-bound auto/yolo session keeps its mode.refused_mode_recovery).--permission-modetoken never grants project trust; use--trust-project,trustedWorkspaces, or remembered trust. Only the deprecated--posturekeeps its interactive trust floor./postureshows the subagent ask reviewer state and says "server-wide allow-all" on servers withoutsession_permission_modes. ACP starts sessions in the server default and offers only admitted modes. HTTP rejects an unrecognised mode string with 400 instead of falling back to the server default. The TypeScript SDK supports Auto/Yolo over gRPC and HTTP.permissionMode(defaultauto) and a checker choice:guardrails.model,guardrails.fromSettings: true(the checker comes from mounted settings), orguardrails.disabled. It refuses to renderautowithout a checker or withdisabled, and rejects--posture/--permission-mode/--yolo/--guardrails/--guardrails-modelinextraArgs. The mecatequi action'sguardrails-modelis empty = inherit,off= kill-switch, else a model; underautothe step fails early when there's no model and no operator settings file, or withoff. The reusable workflow forwards a newpermission-modeinput.--posture,--yolo, mecatui--mode, and theposture:key still resolve, each warning once with the replacement token; combining them with--permission-modeis a startup error.mecated,mecak8s, andmecatequishare one flag helper and help text ininternal/app.API compatibility
Assessment: breaking change to the observable session-mode contract. API major not bumped (stays 1).
The wire policy asks for a major bump when existing conforming clients must change to stay correct. This change does require that for some clients (listed below). I, as the operator, approved keeping major 1 and recording the break here instead, with discovery through feature identifiers and the upgrade notes below.
Old contract:
CreateSessionwithPERMISSION_MODE_UNSPECIFIEDproduced aDEFAULTsession; the only modes on the wire were DEFAULT, PLAN, ACCEPT_EDITS.--posture auto|yoloserver every session, including an explicit DEFAULT one, ran allow-all. Under--posture autothe main agent also ran unprovable command substitutions without asking.ServerCapabilities.posturewas display-only.New contract:
PERMISSION_MODE_-prefixed spellings; any other unrecognised mode string is now 400 InvalidArgument instead of falling back to the server default.ServerCapabilities.posturenow bounds which modes a client may cycle to.FailedPrecondition) at run, retry, approval resume, fork, clear, and MCP-authorization continue until the owner switches it; history is untouched.refused_mode_recovery: for a stored session awaiting an approval in a mode the server no longer admits, a change to an admitted, strictly lower mode cancels the pending turn without running it and applies the mode.Affected clients:
mecatuibuilds that predate AUTO/YOLO may reject an AUTO/YOLO session snapshot or report it as unspecified."default"when no mode is given, so its sessions start in DEFAULT rather than the server default until upgraded.Discovery:
session_permission_modesandrefused_mode_recovery.ServerCapabilities.project_trusted(32) andServerCapabilities.subagent_ask_reviewer(33,on/off/inert).session_permission_modesand check the echoedmode: an older server silently turns AUTO/YOLO into DEFAULT.Upgrade implications:
mecatui) together with servers, or pass explicit modes from older clients.--posture autowithout a checker must configure an enforcing checker or choose--permission-mode yolo. Themecak8schart now fails at render time forpermissionMode: autowithoutguardrails.model(orguardrails.fromSettings: true), so existing releases must make the choice before upgrading. Themecatequiaction fails the step early underautowithout a checker; callers of the reusable workflow with defaults must setguardrails-modelorpermission-mode: yolo.--posture autodeployments whose main agent ran$(...), backtick, or subshell commands unattended now get approval requests for them; useyoloif they must run unattended.--permission-modeno longer grants project trust; use--trust-projectortrustedWorkspaces.Go source: engine adds
session.ModeAuto/ModeYoloandPermissionMode.Rank(Added), and delegated children inherit an auto/yolo parent's mode (Changed, no signature change); seeengine/CHANGELOG.md.Stored data: no schema change. Rolling back to a release without session-scoped modes still loads AUTO/YOLO sessions but treats them as DEFAULT (ordinary approvals unless that server's own
--postureallows all tools, reported as DEFAULT over gRPC). The stored mode is kept, so upgrading again restores it and re-admits it.User-facing documentation:
user-docs/features/security-and-execution/permissions-and-posture.md#upgrade-from-the-posture-model.Gates
task lint: clean except a pre-existing darwin-only gosec finding inenvironment/microvm(untouched).task docs,task site:build,task api:check,task sdk:*: pass.mainon the author's macOS machine (socket paths,/privatetemp symlinks, git worktrees, microVM). Chart tests needHELM_NAMESPACE=defaultwith a non-default kube context.task lintandtask test:racererun. Every remaining failure also fails on the pre-fix head950fbd8ef(mecated/mecatui sockets, executionexecutor,deploy/mecatl-execution-kind) or passes in isolation (race-load flakes).go run ./cmd/mecademo: tool call, permission ask/approval, and result.