Skip to content

refactor(permission): collapse permission-mode authority to Host chatDefaults and drop the no-op execute mode #3385

Description

@Astro-Han

Problem

Two overlapping defects in how a permission mode is decided and represented.

1. The default has competing claimants. chatDefaults.permissionMode in the Runtime Host policy is already the authority — session-catalog-coordinator.ts:352 falls back to it whenever a client omits permissionMode. Three leftovers compete with it:

  • useNewTaskChoice holds the composer's new-task pick in a per-draft in-memory Map, so a user who switches to full access before starting a task finds it back on Auto next time. It is a shadow copy of a setting that already persists.
  • resolveCreateSessionInput re-derives the default in the desktop main process. Production only calls the synchronous resolveCreateSessionRequest; this function has no production caller and is kept alive by its own tests.
  • run-command-core.ts:317 sends parsed.options.yolo ? 'bypass' : 'ask', and runtime-host-session-driver.ts:182 defaults to 'ask' again. The CLI therefore always sends an explicit mode and can never inherit the configured default; --yolo reads as "yolo or ask" instead of a one-shot override.

The renderer also mirrors the default twice: app-shell.tsx:569 reads the selected Host's chatDefaults, app-shell.tsx:1309 reads a separate global copy hydrated from the default Host. With more than one Host connected the composer chip can show another Host's setting.

permission-mode-default.ts is not part of this: runtime-host-boot.ts:939 passes resolveDefaultPermissionMode to the skills IPC, where runtime-host-skills-ipc-main.ts:102 needs a concrete value to predict which mode a new session will start in and filter invocable skills accordingly. It stays.

2. execute is a permission mode with no behavior. Every production branch folds it back into ask:

  • permission-profile-compiler.ts:36case 'ask': case 'execute': compile to the same profile
  • settings.ts:680 — normalized to ask
  • permission-mode-menu.tsx:84 — displayed as ask
  • pi-tui-pickers.ts:716 — counted as Auto alongside ask
  • sandbox-boundary.ts:213 — genesis boundary takes the same branch as ask

executionBoundaryDisplayMode — the single source for what is actually in force — cannot return it at all. The only live writers are two subagent definitions (agent-catalog.ts:151, :176), where 'ask' is behaviorally identical. Five folding sites exist to neutralize one value.

Those writers are live, so stored execute values do exist: every Web Research or Implementation subagent spawn writes one.

Scope

Both halves change where a permission mode comes from and how it is spelled, and they touch the same CLI lines; they ship together.

Single authority

  • Make the composer's new-task picker read and write chatDefaults.permissionMode directly; drop the per-draft shadow state. In-session switching stays per-session.
  • Delete resolveCreateSessionInput and the tests covering it (resolveCreateSessionRequest and its tests stay).
  • Let the CLI pass undefined through when --yolo is absent, so Host chatDefaults applies. --yolo becomes a one-shot override.
  • Collapse the two renderer mirrors onto the selected Host's value.

Removing execute

  • Change the two agent-catalog writers to 'ask' (behaviorally identical; stops producing new execute records).
  • Add decodePersistedPermissionMode in core, folding a stored 'execute' to 'ask', and use it at the four persistence decode sites: session header (session-store.ts:1098), agent run (agent-run.ts:600), subagent tool result (tool-result-record-schema.ts:308), scheduled task (scheduled-task.ts:505).
  • Leave the other four isPermissionMode callers as strict validation: the three protocol frame decoders and runtime-host-session-catalog-ipc-main.ts:154 check new input and wire values, which should reject execute outright once the epoch is bumped.
  • Bump RUNTIME_HOST_COMPATIBILITY_EPOCH to 30 with a note, so a client that still speaks execute is refused at the handshake instead of having a frame rejected mid-session.
  • Remove execute from PERMISSION_MODES and delete the five folding sites.
  • Replace LegacyPermissionMode with PermissionMode (identical member set).
  • Delete isPermissionModeWithinCeiling (no production consumer; its only caller is a test). This also retires the implicit "array order encodes privilege strength" contract on PERMISSION_MODES.

Explicitly not in scope: the permissionCeiling key stays in SUBAGENT_SESSION_RUNTIME_SHAPE. hasExactShape rejects unknown keys, so removing it would make existing subagent records fail to decode.

Rollout

Bumping the compatibility epoch refuses Client-Host pairs that have not upgraded together. That is the established mechanism for a wire semantics change (29 prior bumps are documented in protocol/index.ts), but it does mean a partially upgraded install cannot connect until both sides are updated.

Verification

  • A new session starts in the configured default with no explicit client value, on desktop and on maka run.
  • maka run --yolo still elevates; omitting it inherits the default rather than forcing ask.
  • A stored session header, agent run, and subagent tool result carrying permissionMode: 'execute' still load and present as Auto.
  • maka activate --permission-mode execute still works (kept as an input alias — it is a public subcommand, cli-core.ts:48, whose callers live outside this repo).
  • A client at the previous compatibility epoch is refused at the handshake.

AI use

Generative tooling made a substantive contribution: Claude Code produced the audit that identified these defects and traced the demand chains. Findings were verified against current main sources by the human contributor before filing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions