Skip to content

feat(permissions): one named permission-mode vocabulary and two admission gates - #1935

Open
aponcedeleonch wants to merge 46 commits into
mainfrom
impl/unified-permission-mode
Open

aponcedeleonch wants to merge 46 commits into
mainfrom
impl/unified-permission-mode

Conversation

@aponcedeleonch

@aponcedeleonch aponcedeleonch commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

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.

  • One startup control. --permission-mode (on mecated, mecatui, mecak8s, mecatequi) and the operator-tier permissionMode: key set the mode new sessions start in and the server ceiling, the most permissive mode a session may switch to (accept-edits for plan/default/accept-edits; auto; yolo).
  • Modes are session-scoped. Allow-all (auto/yolo), loose substitution (yolo), and the out-of-workspace path-escape decision follow the session's current mode, so switching from yolo to default restores ordinary approvals. Deny rules and configured asks win in every mode.
  • auto needs an enforcing checker at startup, on create, and on SetMode; missing, --guardrails off, and advisory-only all refuse. Unchecked autonomy is an explicit yolo, where a configured checker is advisory.
  • Stored sessions are re-admitted. A persisted session in a mode the current deployment doesn't admit is refused (FailedPrecondition) at run, approval resume, fork, and clear before any provider or tool work, history untouched, until the owner switches it.
  • Mode changes and pending approvals. A session awaiting an approval cannot change mode; the pending ask resolves under the mode it was raised in (narrower contract agreed in review).
  • Trust is separate. A --permission-mode token never grants project trust; use --trust-project, trustedWorkspaces, or remembered trust. Only the deprecated --posture keeps its interactive trust floor.
  • Delegates inherit. Subagent children, Parallel branches, and Team members of an auto/yolo session run in that mode, unless explicitly configured narrower.
  • Clients. mecatui cycles all five modes on shift+tab; an unavailable mode keeps the current mode and draft and explains what's needed in the footer. /posture shows the subagent ask reviewer state. ACP starts sessions in the server default and offers only admitted modes. The TypeScript SDK supports Auto/Yolo over gRPC and HTTP.
  • Wrappers. The Helm chart gains permissionMode (default auto) and distinguishes guardrails.model (empty = inherit from mounted settings) from guardrails.disabled; it rejects --posture/--permission-mode/--yolo in extraArgs. The mecatequi action's guardrails-model is empty = inherit, off = kill-switch, else a model.
  • Deprecated aliases. --posture, --yolo, mecatui --mode, and the posture: key still resolve, each warning once with the replacement token; combining them with --permission-mode is a startup error.

API compatibility

Assessment: breaking change to the observable session-mode contract, approved by the operator. API major not bumped (stays 1).

Old contract:

  • A gRPC CreateSession with PERMISSION_MODE_UNSPECIFIED produced a DEFAULT session; the only modes on the wire were DEFAULT, PLAN, ACCEPT_EDITS.
  • Allow-all was process-wide: on a --posture auto|yolo server every session, including an explicit DEFAULT one, ran allow-all.

New contract:

  • Five session modes on the wire (adds AUTO=4, YOLO=5). An unspecified create (gRPC, HTTP, ACP, and the TS SDK's HTTP transport) resolves to the server's configured default, which can be AUTO or YOLO.
  • Allow-all belongs to the session mode. An explicit DEFAULT session gets ordinary approvals on every server.
  • A session can switch to modes up to the server ceiling; AUTO additionally needs an enforcing checker. A stored session in a mode the current deployment doesn't admit is refused (FailedPrecondition) at run, approval resume, fork, and clear until the owner switches it; history is untouched.
  • Mode changes are refused while a session awaits an approval; the pending ask resolves under the mode it was raised in (narrower contract agreed with the reviewer).

Affected clients: SDKs and mecatui builds that predate AUTO/YOLO may reject an AUTO/YOLO session snapshot or report it as unspecified; clients relying on process-wide allow-all for DEFAULT sessions now see asks.

Discovery: new feature identifier session_permission_modes. Also additive: optional ServerCapabilities.subagent_ask_reviewer (on/off/inert).

Upgrade implications:

  • Upgrade clients (TS SDK, mecatui) together with servers, or pass explicit modes from older clients.
  • Deployments that ran --posture auto without a checker must configure an enforcing checker or choose --permission-mode yolo; the mecak8s chart (permissionMode, guardrails.model, guardrails.disabled) and the mecatequi action/reusable workflow (guardrails-model: off) document the choice.
  • --permission-mode no longer grants project trust; use --trust-project or trustedWorkspaces.
  • Stored sessions keep their mode and are re-admitted on use.

Go source: engine adds session.ModeAuto/ModeYolo (Added) and delegated children inherit an auto/yolo parent's mode (Changed, no signature change); see engine/CHANGELOG.md. Stored data: no schema change.

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 in environment/microvm (untouched).
  • task docs, task site:build, task api:check, task sdk:*: pass.
  • Go tests: pass except tests that also fail on main on the author's macOS machine (socket paths, /private temp symlinks, git worktrees, microVM). Chart tests need HELM_NAMESPACE=default with a non-default kube context.
  • go run ./cmd/mecademo: tool call, permission ask/approval, and result.

@aponcedeleonch
aponcedeleonch added this pull request to stack #1936 September 25, 2026 13:07
@aponcedeleonch aponcedeleonch changed the title impl/unified permission mode feat(permissions): one named permission-mode vocabulary and two admission gates Sep 25, 2026
@aponcedeleonch
aponcedeleonch force-pushed the impl/unified-permission-mode branch from aad7dbb to 494f216 Compare October 5, 2026 12:31
Base automatically changed from plan/unified-permission-mode to main October 5, 2026 12:39
@aponcedeleonch
aponcedeleonch force-pushed the impl/unified-permission-mode branch from 494f216 to 2c56527 Compare October 5, 2026 12:39
@aponcedeleonch
aponcedeleonch marked this pull request as ready for review October 5, 2026 12:43
@aponcedeleonch
aponcedeleonch force-pushed the impl/unified-permission-mode branch from 2c56527 to bd9a699 Compare October 5, 2026 13:14
@aponcedeleonch
aponcedeleonch force-pushed the impl/unified-permission-mode branch from bd9a699 to 2446c27 Compare October 8, 2026 12:25
@aponcedeleonch
aponcedeleonch force-pushed the impl/unified-permission-mode branch 2 times, most recently from 8e6459b to 363f183 Compare October 14, 2026 10:29
@JAORMX

JAORMX commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

I think we should go further with the convergence here. If I choose default after running in yolo, I expect ordinary permission evaluation to come back. Changing only the session half while the server keeps its allow-all posture makes the mode selector hard to rely on. Putting restart-only choices into the cycle also makes discovery interrupt the work.

What I'd propose is five permission modes that actually change the session's behavior:

  • plan: inspect and plan, with mutations denied.
  • default: ordinary permission approvals.
  • accept-edits: automatically accept file edits; other mutations follow ordinary approval rules.
  • auto: autonomous execution with enforcing guardrails. An enforcing checker must be available to enter this mode.
  • yolo: autonomous execution without preventive model checking. Advisory monitoring may remain.

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 trusted and trusted-accept-edits from the list. Trust decides whether we automatically adopt the workspace's project instructions and configuration. It does not decide whether we can read its files! Inspecting an unfamiliar repository is often the whole reason to start an agent. We should still be able to read and search an untrusted workspace, including reading AGENTS.md as data without automatically treating it as authoritative. Executing repository scripts remains subject to execution permissions and isolation.

The UI could show something like Accept edits · Workspace instructions enabled. Revoking workspace trust may need a fresh session, since instructions the model already consumed cannot simply be unread.

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 yolo to auto must restore checker enforcement; switching to default must remove the session's automatic allow-all behavior. Changes should take effect at a defined execution boundary, with pending approvals re-evaluated and already-running actions clearly identified. A mode change cannot undo a command that has already started. Unavailable choices should explain what's missing without blocking the composer.

I'd also avoid translating an empty checker setting into --guardrails off in the deployment wrappers. Inheriting configuration, configuring a checker, and explicitly disabling it are different choices. And when a model reviewer can approve eligible subagent requests, that delegation should be visible to the operator.

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.

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

mecatl/cmd/mecatequi/run.go

Lines 165 to 166 in 98fc96f

func run(ctx context.Context, svc *server.Service, limits session.Limits, prompt string, w io.Writer) (runOutcome, error) {
sess, err := svc.CreateSession(ctx, session.ModeDefault, limits)

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,9251 still 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 and mecatequi recovery 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–163 currently 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 extraArgs with --posture=strict now 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 /posture would 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.

Comment thread internal/adapter/server/service.go
Comment thread internal/adapter/server/service.go Outdated
Comment thread engine/adapter/permpolicy/permpolicy.go
Comment thread contracts/proto/mecatl/v1/harness.proto
// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment thread internal/app/posture.go
Comment thread deploy/helm/mecak8s/templates/deployment.yaml
Comment thread cmd/mecatui/ui/mode_cycle.go
@aponcedeleonch

Copy link
Copy Markdown
Member Author

Thanks for the thorough review. Inline blockers are answered in their threads; all changes are in 9d276cc. The rest of the review body:

Blockers

  • mecatequi run mode: run now creates its session with an unspecified mode so the --permission-mode default applies. TestRunUsesPermissionModeFromFlags drives parseFlags → appConfig → Build → run.
  • Pending-approval mode switch: we're taking the narrower contract. A session awaiting an approval can't change mode; the ask resolves under the mode it was raised in, and a mode change never stops a started command. This is documented in permissions-and-posture.md and docs/architecture/governance.md, not described as a re-evaluation.

Standards / tests

  • Prompt text: the self-knowledge prompt now describes five modes, the ceiling, auto's checker requirement, and trust as a separate decision. Its behavioural claims are pinned against the implementation.
  • Stale text: the permissions guide, keybindings, using-the-tui, secure-and-expose, CLI help, the action/reusable-workflow descriptions, the mecatequi no-approver message, and the generated config/CLI references are updated.
  • Feature identifier: session_permission_modes is advertised and mirrored in the TS SDK.
  • Tests: coverage now includes production-path runs across a mode switch, a persisted-session reload under a tighter ceiling, an external-file effect, and delegated-child behaviour, with a configured-Deny positive control.

Non-blocking follow-ups (done)

  • Delegated children: Subagent children, Parallel branches, and Team members of an auto/yolo session inherit that mode (effectiveChildMode); an explicitly narrower configured mode is kept. A real parent-run test covers it, and it's recorded in engine/CHANGELOG.md.
  • ACP: sessions start in the server's default mode, and the picker offers only modes the server admits (Service.AdmittedModes), including auto/yolo when admitted.
  • Help guidance: only auto mentions a checker.
  • Helm: a chart-level permissionMode; extraArgs carrying --posture/--permission-mode/--yolo fails rendering with migration guidance.
  • Posture mapping: the legacy mapping is consolidated into app.PostureReplacementToken.
  • Reviewer visibility: an optional ServerCapabilities.subagent_ask_reviewer (on/off/inert), shown in /posture.

Known remaining

  • Serving of external relative ../ paths still follows the startup ceiling. The authorization decision follows the session mode, so this only affects whether an approved relative escape is served below yolo.
  • The repo's own mecatequi.yml now runs yolo with guardrails-model: off, since unchecked auto now refuses.

aponcedeleonch and others added 17 commits October 16, 2026 16:08
Enforce the selected session mode across every layer and caller:

- Re-admit a stored session's mode against the current ceiling and checker
  at every run entry, approval resume, fork, and clear (FailedPrecondition,
  history untouched).
- Remove auto's default-mode exemption: auto needs an enforcing checker at
  startup, creation, and SetMode; unchecked autonomy is yolo.
- Drive the out-of-workspace escape policy and its checker route from the
  session mode instead of the process posture.
- mecatequi runs in the mode resolved from --permission-mode.
- A --permission-mode token never grants project trust.
- Helm chart and mecatequi action distinguish inherit / model / off for the
  guardrails checker; the chart gains permissionMode.
- mecatui no longer blocks the composer on an unavailable mode.
- TypeScript SDK supports Auto/Yolo over gRPC and HTTP, omits an
  unspecified create mode, and projects new capabilities.
- Advertise the session_permission_modes feature and the optional
  subagent_ask_reviewer capability (shown in /posture).
- Delegated children inherit an auto or yolo parent's mode.
- ACP starts sessions in the server default mode and offers only admitted
  modes.
- Share one deprecated-posture replacement mapping across the roots.
- Document the five-mode contract, the upgrade from the posture model, and
  the narrower pending-approval contract.

Co-authored-by: Mecatl <noreply@mecatl.dev>
Three CI jobs failed on the rebased head; each followed from this
branch's own contract changes:

- deploy:check required --guardrails among the chart's base args, but the
  chart now emits a guardrails flag only for an explicit checker choice
  (empty means inherit). Drop it from the base-arg list.
- The kind fixtures (values-kind.yaml and the execution qualification
  values) install the chart's default permissionMode auto with the mock
  provider and no checker, and auto now refuses to start without an
  enforcing checker, so the pod never became ready and helm --wait timed
  out. Set permissionMode: yolo, which is what the old chart's
  --posture=auto --guardrails=off amounted to.
- The microVM daily journey created its session with an explicit
  ModeDefault, which no longer inherits the process allow-all, so its
  Write waited on an approval nobody gives until the 1200s timeout. Use
  the unspecified mode so the deployment default applies, as the test's
  "ordinary no-selector API" comment intends.

Co-authored-by: Mecatl <noreply@mecatl.dev>
The kind e2e and execution qualification suites created every session
with an explicit "mode":"default". Under the old chart's --posture=auto
that still carried process-wide allow-all; with session-scoped modes an
explicit default means ordinary approvals, so the learned-skill run and
the borrowed-schedule proof write waited on asks nobody answers until
their deadlines.

Omit the mode for sessions that go on to run tools so they start in the
deployment default (yolo in the Kind fixtures). Rejection-only creates
keep their explicit mode.

Co-authored-by: Mecatl <noreply@mecatl.dev>
Co-authored-by: Mecatl <noreply@mecatl.dev>
…mpiles

The auto admission gate and the checker startup state read the raw
guardrail specs, while the installed reviewer runs on the rules that
survive CompileRule. Settings decoding accepts phases and matchers that
CompileRule rejects, so an operator rule marked block with an invalid
phase passed admission, was then skipped at compile time, and left auto
with no contextual reviewer at all. A whitespace-padded advisory mode
was also misread as block before compilation normalized it.

Admission and the checker state now compile the effective rules the same
way buildGuardrailsActionReviewer does, refuse auto unless an enforcing
rule survives, and report the checker disabled when nothing compiles.
Deterministic Deny and Ask rules are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fold

The deprecated --posture branch of foldPermissionMode wrote the default
session mode and server ceiling from the --posture value before
resolvePosture applied the --yolo and --trust-project MAX fold, and the
Build fallback then skipped recomputing them because they were already
set. So --posture strict --yolo started yolo-posture sessions in default
mode under an accept-edits ceiling, and --posture auto --yolo was refused
as an auto deployment without a checker, while --yolo alone resolves to
yolo with a yolo ceiling.

The deprecated surface now derives both values once, from the posture
after the full alias fold, so a combination lands on the same pair as the
raised tier alone. Permission-mode tokens are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run entry admitted the pre-lease snapshot only. During a rolling upgrade a
looser peer could take the session lease and save a mode this replica
refuses while it waited, and the reload under the lease then executed that
mode unchecked. Every run entry now re-admits the authoritative snapshot once
the lease is held, before recovery effects or engine construction; each
reload that replaces the run's aggregate re-admits it; and both launch
fences check the mode of the aggregate about to execute, not just lease
ownership.

Construction now runs the complete admission on the default mode and
validates the ceiling, so a service whose default sessions could never be
created fails to start instead of failing every default CreateSession.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The upgrade notes only said stored sessions keep their mode. Operators
rolling back also need to know what an older server does with a snapshot
whose mode it has no name for: it loads it without validation, evaluates
permissions as default (plus its own process-wide posture), reports DEFAULT
over gRPC, and re-saves the string unchanged so a later upgrade re-admits it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n_modes

The mode cycle inferred the new session-scoped semantics from the legacy
posture capability, and startup only checks API major 1. Against an older
yolo server, cycling to default succeeded without removing that server's
process-wide allow-all, yet the header presented default as the effective
mode.

The client now reads the session_permission_modes feature into
Capabilities. Without it the ui falls back to an explicit legacy
presentation: the cycle offers only plan, default, and accept-edits (an
older server has no auto or yolo session mode), and the header and footer
label the session mode with the server's auto or yolo posture, which still
applies to every session there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Manual

The BFF folded the auto and yolo session modes into default, so a chat
created elsewhere in an autonomous mode read as Manual ("Always ask before
making changes") and lost its header badge. Rendering does not change
execution, but the description of the session was wrong.

Session detail and set-mode responses now carry the reported mode,
including auto and yolo. Studio still never requests those modes: the
request schema keeps its three values. A chat in auto or yolo shows a
warning or destructive badge, labels the mode "set outside Studio" in its
details, and gets no Mode control, since Studio cannot show or restore it
there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The extraArgs guard only matched double-dash prefixes, but Go's flag
parser also accepts single-dash long flags. -permission-mode=yolo could
replace the chart-generated mode, and -posture=strict reached a runtime
conflict instead of the render-time migration error.

Match one or two leading dashes with the exact flag name, in both the
"=" and split-argument forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The schema typed permissionMode as any string, so editors and
`helm lint` could not flag a typo until the template's own check failed
at render time. List the five permission modes in the schema enum; the
template check stays as the fallback for renders that skip schema
validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fault

The reusable workflow's opening comment still described
subagent-ask-reviewer as an opt-in autonomous-approval capability. The
action now turns the reviewer on by default in the auto and yolo modes,
and the reusable workflow runs the action's default auto mode, so its
callers get the reviewer. Say so, and keep the note that choosing another
reviewer or "off" is made by editing the action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…med children

An omitted child mode and an explicit WithChildMode/WithParallelChildMode(
ModeDefault) or member permissionMode: default were indistinguishable, so
both inherited an auto or yolo parent and dropped default's builtin asks
and, under yolo, its substitution checkpoint. Empty is now the inherit
sentinel and any explicit mode, default included, is kept; an unknown
member permissionMode pins default rather than inheriting.

A resumed Subagent child also kept the mode it was persisted with, so a
child created under a yolo parent retained yolo's authority after the
parent was downgraded or the ceiling tightened. Both resume paths now run
the child in the narrower of its stored mode and the mode a fresh child of
the current parent would get, before it executes or is re-persisted, with
its history untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s mode

A delegated child shares its parent's reviewRoot, whose reviewer was
compiled with rules already demoted to advisory for a yolo parent. A
member definition could correctly keep a narrower mode under that parent,
yet a rejecting verdict on its action stayed advisory and unsafe inbound
content was released. Subagent and Parallel children shared the same gap.

The host reviewer now compiles the operator's undemoted rules, and the
engine applies the yolo demotion per executing session in reviewPolicy
(and skips the child permission review for a yolo child). A yolo session
stays advisory while a narrower child of it enforces, and trajectory and
evidence stay shared. Coverage and startup narration keep the demoted
specs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Mode downgrade

A stored session parked awaiting an approval in a mode the server no longer
admits had no recovery short of re-enabling that mode: every resume is
refused by the stored-mode check, durable cancel reports no active run, and
the aggregate refuses SetMode mid-turn. SetMode to an admitted, strictly
lower mode now cancels the pending turn without any engine, provider, or
tool work and applies the mode in one save, under runEntryMu and the
mutation lease on the snapshot reloaded under that lease. The next run
repairs history through the ordinary interrupt path. Every other awaiting
SetMode keeps the InvalidArgument refusal, and the refusal for an awaiting
session now names this downgrade as the recovery.

API compatibility: additive. SetMode widens from InvalidArgument to success
only for sessions that are unrecoverable today; no request or response shape
changes and no existing success path changes. Advertised by the new
refused_mode_recovery feature identifier, mirrored in the TypeScript SDK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sture or --yolo

A settings.yaml permissionMode: token combined with the deprecated --yolo
flag let resolvePosture raise the posture to yolo while the token's
session mode and ceiling stayed put, and --posture silently out-ranked the
token even though the key is documented to out-rank the deprecated
surface. Either way the operator had to guess which permission source
won. The cmd roots already refuse --permission-mode with these flags.

Build now refuses the settings key combined with --posture or --yolo,
naming both migrations: drop the flag to keep the key, or replace it with
--permission-mode, which overrides the key. --trust-project is not
deprecated and still combines with the key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The existing auto-session guardrail test switched to auto before the first
run, so it never proved the engine transitions between modes on a live
session. This runs three completed turns on one session through the real
composition, switching mode between runs: yolo runs the mutation with a
rejecting checker consulted but advisory; auto lets that checker block it
while a configured Deny still denies; default surfaces an ordinary
permission ask instead of auto-allowing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aponcedeleonch

Copy link
Copy Markdown
Member Author

Thanks for the follow-up review. The six inline blockers are answered in their threads. Here's the rest of the review body. The branch was also rebased on main (range-diff of the existing 27 commits is unchanged).

Contract decisions

  • Major 1 exception: I'll record the explicit approval on this PR separately so the history is clear.
  • Pending approval: agreed it's a scoped reduction, not re-evaluation. A session awaiting approval still can't change mode, and the ask resolves under the mode it was raised in. The docs describe it that way.
  • Feature consumption in mecatui: done in 7c8d873 (details in the inline thread).
  • Stored-data rollback: documented in c04bba2, in the "Stored sessions" note of permissions-and-posture.md. The mode is a plain string in the snapshot. An older binary restores auto/yolo without validating it and has no ceiling or stored-mode check. Its policy only special-cases plan and acceptEdits, so the session evaluates as default (ordinary approvals) unless that server's own process-wide --posture/--yolo allow-all applies. Over gRPC the old mapper reports DEFAULT, and over HTTP it returns the raw string. The old binary re-saves the string unchanged unless the owner calls SetMode, so after upgrading again the session keeps auto/yolo and is re-admitted under the new ceiling and checker rules.

Other follow-ups

  • Studio permission display: fixed in 7ffbc2f. Session detail and setMode responses carry all five modes. Auto and yolo get their own badge (warning and destructive) and are read-only in Studio, since Studio can't request them. Request schemas still accept only the three Studio modes.
  • Awaiting-session recovery: done in 2de26a4. SetMode to an admitted, strictly lower mode on a stored awaiting session whose mode the server refuses now cancels the pending turn and applies the mode in one save, under runEntryMu and the lease, with no engine, provider or tool work. The next run repairs history through the normal interrupt path. Every other awaiting SetMode is still refused, and the refusal message for an awaiting session now names this recovery. It's advertised as refused_mode_recovery and mirrored in the TS SDK. Wire assessment: additive, since SetMode only goes from InvalidArgument to success for sessions that have no recovery today, and no shapes or existing success paths change.
  • Combined legacy aliases: fixed in d8166f2. Build derives the legacy mode and ceiling after the full alias fold, so --posture strict --yolo lands on yolo/yolo, the same as --yolo alone. On top of that, 8f513e4 refuses a settings.yaml permissionMode: combined with the deprecated --posture or --yolo at Build, with a migration hint, since that combination had the same split.
  • Constructor validation: fixed in 7978c30. normalizeDefaultMode runs full admission on the default mode and rejects an unknown ceiling, so an inadmissible default fails construction. Per-request checks are unchanged.
  • Helm extraArgs: fixed in c5c37d2. The check matches one or two dashes in both = and split forms, with six new chart cases.

Test adequacy

  • Persisted child resume after downgrade: TestResumedSubagentReadmitsModeAfterParentDowngrade, foreground and background (cbb41d1).
  • Two-replica admission race: TestStoredModeReadmittedUnderAcquiredLease (7978c30).
  • Narrower delegated mode with a rejecting checker through the real factory: TestYoloParentNarrowerTeamMemberKeepsEnforcingChecker (a6cf4f1). Parallel branches are covered by TestExplicitDefaultChildModeIsKeptUnderYoloParent at the engine level.
  • Executed yolo→auto→default sequence: TestGuardrailE2EExecutedYoloAutoDefaultTransitions (7b45c22). One session on a real Build, three completed turns with SetMode between them. In yolo the rejecting checker is consulted but doesn't block. In auto it blocks Shell and the configured Write Deny still denies. In default the Shell call surfaces as an ordinary ask. Making SetMode ignore either target fails the matching phase.

Optional

  • Chart JSON-schema enum: done in 1a471d1.
  • Reusable workflow reviewer comment: done in 1e35027.
  • Composer tests executing the returned commands, and a seeded project-instruction fixture for trust: not in this PR. Both are good ideas. I'd rather keep this change set focused on the enforcement fixes, and I'll pick them up in a follow-up.

@JAORMX

JAORMX commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Thanks, I took your latest follow-up and the new regression tests into account. This revision addresses the reported cases substantially, including child-mode inheritance, executing-session checker enforcement, compiled-rule admission, legacy-client presentation, Studio display, and the wrapper/alias fixes. The executed yolo→auto→default test is a useful addition.

I want to narrow the remaining review rather than keep expanding the blocker list. The main correctness concern I would still address here is that the authoritative snapshot loaded under the lease is validated but not adopted by every entry path.

admitLeasedMode loads the current session and returns only an error (internal/adapter/server/service.go:4623–4633). The caller can then continue with its pre-lease sess. For example, StartRun calls reopenLoadedSession(ctx, sess) at :5502, and the legacy approval-resume path continues with its earlier aggregate at :7340–7366.

Your TestStoredModeReadmittedUnderAcquiredLease proves refusal when the peer changes the mode to something the receiving server cannot admit. The remaining case is an admitted downgrade:

  1. Replica A reads a completed session in yolo and waits for the lease.
  2. Replica B acquires the lease, successfully changes that session to default, and releases it.
  3. A acquires the lease, loads and validates the current default snapshot, but discards that object.
  4. A reopens/saves its old yolo aggregate. On a yolo-capable server the later admission checks accept it, so the completed downgrade is overwritten.

The approval-resume variant is more consequential: a peer can cancel the pending turn through the new refused-mode recovery, while the waiting replica later validates the updated snapshot but resumes its old pending ask. The final fence checks that old aggregate too.

This is a source-level trace, not an independently executed reproduction. It requires concurrent operations across replicas sharing storage and leases; it is not a claim about normal single-process mecatui usage, an ownership bypass, or configured Deny rules being defeated. I would call it a distributed-correctness fix, not a broad security emergency.

Could we have the under-lease load return the authoritative aggregate, adopt it in sess/st.sess, and repeat the entry-specific state/run/ask checks before recovery, persistence or execution? Extending the synchronized test to a successful downgrade and a cancelled pending approval would distinguish this from the case already covered. The existing final checks should stay.

The other remaining observations do not need to hold this PR up on their own: making the recovery reachable from the awaiting-approval TUI and observable to watchers, clarifying retained Auto-child behavior when the current parent deliberately runs unchecked, and correcting the Team checker fixture's malformed concerns value so it proves a valid prohibited verdict rather than fail-closed parsing. Those can be explicit follow-ups. I also accept keeping the exact composer-send assertion and seeded trust fixture out of this change set as you proposed.

The documented narrower pending-approval contract is a reasonable scope reduction; I am not asking to implement general mid-approval mode switching here. Please add the promised explicit record for the major-1 compatibility exception separately. The rollback explanation answers the earlier stored-data documentation question.

So my remaining code request is focused: adopt the snapshot that was actually checked under the lease, and prove that a completed downgrade or cancellation cannot be undone by a waiting replica. I'm not asking for another permission-model redesign.

aponcedeleonch and others added 4 commits October 16, 2026 17:52
admitLeasedMode validated the snapshot loaded under the lease but threw it
away, so the entry continued with its pre-lease copy. An admitted change a
peer made while this replica waited for the lease was then lost: a completed
session downgraded from yolo to default was reopened and saved back as yolo,
and an approval resume could resume a pending ask the peer had already
cancelled through the refused-mode recovery.

adoptLeasedSession now returns the authoritative aggregate, re-admits its
mode, and installs it as st.sess. StartRunContent, RetryFailedRun, and the
awaiting approval resume continue with it and repeat their own state, run,
and ask gates before any recovery, persistence, or execution. The existing
reload re-admission and launch fences stay. Without a shared lease the
pre-lease snapshot is already authoritative, since every in-process writer
serializes on runEntryMu, so adoption is a no-op there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ited verdict

The fixture's concerns were bare strings, which the reviewer rejects as a
malformed assessment. The narrower member was then blocked by fail-closed
parsing, not by a prohibited verdict, so the test did not prove what it
claimed. The verdict now carries a schema-valid concern, and the action case
asserts the reason reserved for a parsed prohibited assessment; with the old
fixture that assertion fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Schedule tool always recorded a mutating schedule as ModeDefault. That
was harmless while yolo was a process-wide posture. Now that the permission
mode belongs to each session, a schedule created from a yolo session fired
into a default session. Its Write then asked for approval with no approver,
so the fire hung until its deadline. This is the kind
TestKindExecutionBorrowedSchedule hang.

Dispatch now stamps the executing session's mode on each tool call's context,
read at execution like parentCaps' parentMode. The tool records that mode for
a mutating create. It never reads the mode from model arguments. Read-leaning
schedules still record plan. An unbound context keeps ModeDefault. A plan-mode
context refuses the mutating create.

The fire path already admits the recorded mode in CreateSessionWithProfile.
So a schedule recorded in yolo on a server later relaunched with a lower
ceiling fails fast as a StopError fire instead of escalating or hanging. The
new composition test covers both the yolo fire and the refused fire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A gRPC or REST CreateSchedule/UpdateSchedule mapped an UNSPECIFIED mode
to DEFAULT. On an allow-all server a mutating schedule created without a
mode, which is what the mecatui overlay sends, then fired in default. Its
writes asked with no approver and the fire hung, the same failure as the
in-chat tool before 241042f51. The fire path already treats an empty mode
as the server's DefaultMode, but nothing on the wire could produce one.

protoToScheduleSpec now maps the mode with createModeFromProto, as
CreateSession does, so UNSPECIFIED is stored as "". Each fire resolves it
to the server's default session mode and admits it against the current
ceiling. The read-back maps a stored "" to UNSPECIFIED so a GetSchedule
then UpdateSchedule round trip keeps it unset instead of pinning DEFAULT.
The create seam already validates "" as default, so a read-leaning
schedule without PLAN is still refused, exactly as before.

mecatui sends PLAN for a read-leaning create and UNSPECIFIED for a
mutating one. The mapping lives on the schedule path only, and
ModeFromString("") still means DEFAULT for its other callers. A
read-leaning mecatui create was already refused on origin/main, since it
sent DEFAULT.

API compatibility:
- Assessment: compatible semantic change, no major bump, no new feature
  identifier. Same shape as the approved CreateSession change in
  e68fb44: an UNSPECIFIED ScheduleSpec.mode now means the server's
  configured default session mode instead of DEFAULT.
- Rationale: no field, enum, or RPC changes. On a server whose default
  session mode is default, behavior is identical: the fire resolves to
  default. Only servers started with a permission mode that defaults
  sessions elsewhere differ, and there the old behavior hung mutating
  fires headless. Clients that send an explicit mode are unaffected.
- Read-back: a stored "" (new UNSPECIFIED creates, or embedder specs)
  now reads back as UNSPECIFIED instead of DEFAULT. Stored explicit modes,
  including every schedule created over the wire before this change, read
  back unchanged. A client that rendered UNSPECIFIED as default keeps
  working; Studio's mapper shows UNSPECIFIED as plan, as it already does
  for auto and yolo.
- Stored data: no migration. Existing specs keep their explicit modes.
- Upgrade: none required. To pin default on an allow-all server, send
  PERMISSION_MODE_DEFAULT explicitly.
- Approval: the operator approved this change for the PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e modes

Studio mapped every schedule mode other than default and acceptEdits to
plan. A schedule created in chat now records its creator's mode (auto or
yolo), and a wire create with no mode stores the server default, so those
schedules were shown as plan. Worse, editing one from Studio sent plan back
while it stayed mutating, so its next fire ran in plan and could not write.

Schedule responses now report the actual mode, including auto, yolo, and
serverDefault, with honest labels in the detail view. Create requests keep
the three modes Studio can choose. The update request's mode is optional:
when the form keeps a mode Studio cannot set, it omits it and the stored
mode is preserved unchanged instead of being coerced to plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aponcedeleonch

Copy link
Copy Markdown
Member Author

Thanks for narrowing it down, that helps a lot.

Under-lease snapshot adoption

Fixed in a6cbf1a. admitLeasedMode is replaced by adoptLeasedSession, which reloads the session under the lease, admits its mode, and installs it as st.sess (and titleRevision). The entry then continues with that aggregate and repeats its own gates before any recovery, persistence or execution:

  • StartRunContent: workspace, purpose and pending failed-step retry, before reopenLoadedSession.
  • RetryFailedRun: the retry eligibility gates, recomputed on the adopted copy (retryEntryEligibility).
  • The awaiting approval resume (resumeFromAwaiting, the legacy path you cited): workspace, expected run id, StateAwaiting, pending ask id and resolution validation (validateAwaitingResume).

Persisted ResolveRunAsk and the exact plan ask already reloaded and re-validated under the lease, so they're unchanged. The reload re-admission and both launch fences stay. Without a shared lease adoption is a no-op, since every in-process writer serializes on runEntryMu, which the caller holds across the pre-lease load.

The synchronized test now covers both cases you described, with two services sharing one memstore and one memlease:

  • TestRunEntryAdoptsAdmittedDowngradeUnderLease: both replicas admit yolo. A parks at Acquire, B downgrades the completed session to default and releases, A runs in default and the stored session stays default. With adoption disabled it ends up back in yolo.
  • TestApprovalResumeAdoptsPeerCancelledTurnUnderLease: A parks on an approval resume, B cancels the pending turn through the refused-mode recovery and releases. A returns ErrNoActiveRun with no provider or tool work on either replica, and the session stays cancelled in default with no pending ask. With adoption disabled A resumes the cancelled ask.

Team checker fixture

Fixed in f7bca08. The fixture now returns a well-formed prohibited verdict with a valid concern object. The action case also checks the reason text that only a parsed prohibited verdict produces, so it fails with the old malformed fixture (it gets the fail-closed reason). The inbound phase shows the same withheld text either way, so only the action case tells the two apart.

Follow-ups

Agreed these stay out of this PR: making the refused-mode recovery reachable from the awaiting-approval TUI and observable to watchers, and clarifying a retained auto child's behavior when its current parent runs unchecked.

The major-1 exception record will be posted separately.

Scheduled fires (found by CI)

The kind production profile run surfaced a related regression in this PR, so it's fixed here too. A mutating in-chat schedule was always recorded as default, which used to be harmless under a process-wide allow-all posture. Now that allow-all is session-scoped, its fire on an allow-all deployment created a default session whose write asked with no approver, and FireNow hung until the deadline (TestKindExecutionBorrowedSchedule).

  • dea7609: a mutating in-chat schedule records its creating session's mode, read from the run context and never from model arguments. The fire re-admits it, so a server tightened since creation refuses the fire instead of escalating or hanging.
  • 9689c6b: gRPC and REST schedule create/update now treat an UNSPECIFIED mode the same way CreateSession does (server default, re-admitted at fire), and it reads back as UNSPECIFIED. mecatui's form sends UNSPECIFIED for mutating schedules and PLAN for read-leaning ones, which also fixes its read-leaning create that was already rejected on main. The wire assessment is in the commit message: same shape as the CreateSession change, with stored explicit modes unchanged.
  • aaba681: Studio shows auto, yolo and server-default schedules honestly, and an edit that keeps a mode Studio can't set preserves it instead of rewriting it to plan.

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The remaining snapshot-adoption request is resolved: run entry now adopts the authoritative under-lease aggregate, repeats the relevant state/run/ask checks, and has regressions covering both an admitted downgrade and a peer-cancelled approval. I also checked the scheduling-mode fixes and the latest Studio mode-preservation change.

No remaining merge-blocking code findings from this review. The previously discussed optional work and broader pre-existing concurrency limitations can remain follow-ups. Please retain the documented migration guidance and the promised explicit record of the major-1 exception.

Approving head aaba681. CI is still running with no failures reported at posting time; this approval does not assert that all checks have completed.

This branch was successfully deployed

1 active (outdated) deployment
Preview — 9689c6bb Deployed Oct 16, 2026 by vercel[bot]
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.

2 participants