Part V · The work lifecyclePermissions and input
Chapter 24source checked

Permission modes, approvals, and structured input

T3 Code persists a four-value runtime-mode choice on a thread, but each provider maps that choice into its own controls; live approval and structured-input requests travel through distinct canonical flows.

What this chapter resolves
  • Name the four canonical runtime modes without treating them as an equivalent provider security policy.
  • Distinguish approval requests from structured user-input requests, including their response payloads and pending state.
  • Trace an interactive response through durable intent, the provider reactor, and native completion.

T3 Code has four persisted runtime-mode values: approval-required, auto-accept-edits, auto, and full-access. They belong to the thread and travel with provider session/turn start. They are compact product controls, not proof that five native runtimes have the same sandbox, reviewer, allow-list, or escalation semantics.

Four durable labels, provider-specific effects

The canonical union contains exactly these four values; its default is full-access. A runtime-mode change becomes a thread.runtime-mode-set event. When a turn starts, the decider writes the target thread’s already-stored runtime and interaction modes into the start event. Read the labels as durable thread policy, not an independent per-turn override hidden in the start command.

packages/contracts/src/orchestration.ts:120–143 ↗verbatim · typescript · 2ec2b0f6
export const RuntimeMode = Schema.Literals([
  "approval-required",
  "auto-accept-edits",
  "auto",
  "full-access",
]);
export type RuntimeMode = typeof RuntimeMode.Type;
export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";
export const ProviderInteractionMode = Schema.Literals(["default", "plan"]);
export type ProviderInteractionMode = typeof ProviderInteractionMode.Type;
export const DEFAULT_PROVIDER_INTERACTION_MODE: ProviderInteractionMode = "default";
export const ProviderRequestKind = Schema.Literals(["command", "file-read", "file-change"]);
export type ProviderRequestKind = typeof ProviderRequestKind.Type;
export const AssistantDeliveryMode = Schema.Literals(["buffered", "streaming"]);
export type AssistantDeliveryMode = typeof AssistantDeliveryMode.Type;
export const ProviderApprovalDecision = Schema.Literals([
  "accept",
  "acceptForSession",
  "decline",
  "cancel",
]);
export type ProviderApprovalDecision = typeof ProviderApprovalDecision.Type;
export const ProviderUserInputAnswers = Schema.Record(Schema.String, Schema.Unknown);
export type ProviderUserInputAnswers = typeof ProviderUserInputAnswers.Type;
Read this as: The orchestration contract fixes four product-facing runtime modes and four approval decisions. The provider mapping remains a separate concern.
T3 mode Codex app-server mapping Claude SDK mapping Cursor ACP path Grok ACP path OpenCode SDK mapping
approval-required untrusted approval, read-only sandbox, user reviewer no explicit entry in the adapter’s mode map; base mode is left unset prefers a native approval-mode alias when offered; permission callbacks still wait no ACP session-mode mapping; a raised permission callback waits for an explicit response broad operations ask; question is allowed
auto-accept-edits on-request, workspace-write, user reviewer acceptEdits prefers an implement-mode alias when offered; permission callbacks still wait no ACP session-mode mapping; a raised permission callback waits for an explicit response broad operations ask; question is allowed
auto on-request, workspace-write, auto_review reviewer auto prefers an implement-mode alias when offered; permission callbacks still wait no ACP session-mode mapping; a raised permission callback waits for an explicit response broad operations ask; question is allowed
full-access never, danger-full-access, user reviewer bypassPermissions plus allowDangerouslySkipPermissions: true prefers an implement-mode alias and auto-selects an offered allow option; otherwise waits on the callback auto-selects an offered allow-always or allow-once option; otherwise waits on the callback allows all permission rules

The table records executed mappings at this pinned revision. It does not turn the blank Claude map entry, Cursor mode aliases, Grok callback policy, or OpenCode’s shared non-full-access ruleset into claims of equivalent safety. Codex, for example, also selects a native sandbox and reviewer for every mode. Claude intercepts AskUserQuestion before the full-access shortcut, so it still uses structured input; ExitPlanMode is intercepted separately.

apps/server/src/provider/Layers/CodexSessionRuntime.ts:268–340 ↗verbatim · typescript · bf3d7a10
function runtimeModeToThreadConfig(input: RuntimeMode): {
  readonly approvalPolicy: EffectCodexSchema.V2ThreadStartParams__AskForApproval;
  readonly sandbox: EffectCodexSchema.V2ThreadStartParams__SandboxMode;
  // Always explicit: omitting the field on resume keeps the thread's previous
  // reviewer, which would leave auto_review sticky after switching modes.
  readonly approvalsReviewer: EffectCodexSchema.V2ThreadStartParams__ApprovalsReviewer;
} {
  switch (input) {
    case "approval-required":
      return {
        approvalPolicy: "untrusted",
        sandbox: "read-only",
        approvalsReviewer: "user",
      };
    case "auto-accept-edits":
      return {
        approvalPolicy: "on-request",
        sandbox: "workspace-write",
        approvalsReviewer: "user",
      };
    case "auto":
      return {
        approvalPolicy: "on-request",
        sandbox: "workspace-write",
        approvalsReviewer: "auto_review",
      };
    case "full-access":
    default:
      return {
        approvalPolicy: "never",
        sandbox: "danger-full-access",
        approvalsReviewer: "user",
      };
  }
}
 
function buildThreadStartParams(input: {
  readonly cwd: string;
  readonly runtimeMode: RuntimeMode;
  readonly model: string | undefined;
  readonly serviceTier: CodexServiceTier | undefined;
}): EffectCodexSchema.V2ThreadStartParams {
  const config = runtimeModeToThreadConfig(input.runtimeMode);
  return {
    cwd: input.cwd,
    approvalPolicy: config.approvalPolicy,
    sandbox: config.sandbox,
    approvalsReviewer: config.approvalsReviewer,
    ...(input.model ? { model: input.model } : {}),
    ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
  };
}
 
function runtimeModeToTurnSandboxPolicy(
  input: RuntimeMode,
): EffectCodexSchema.V2TurnStartParams__SandboxPolicy {
  switch (input) {
    case "approval-required":
      return {
        type: "readOnly",
      };
    case "auto-accept-edits":
    case "auto":
      return {
        type: "workspaceWrite",
      };
    case "full-access":
    default:
      return {
        type: "dangerFullAccess",
      };
  }
}
Read this as: Codex receives an explicit approval policy, sandbox mode, and reviewer mode; the turn sandbox is derived again for each request.

Two interactive request shapes

An approval asks whether a provider may perform an action. Its canonical response contains a requestId and one decision: accept, acceptForSession, decline, or cancel. Runtime ingestion classifies known native request types into command, file-read, and file-change when it can; unknown native request types remain an approval without a fabricated subtype.

Structured input is different. It carries questions with ids, headers, options, and optional multi-select behavior. Its response is an answer record keyed by question id. An answer is not an approval decision, and an approval choice is not an answer map.

packages/contracts/src/providerRuntime.ts:430–469 ↗verbatim · typescript · 6b1a92d9
const RequestOpenedPayload = Schema.Struct({
  requestType: CanonicalRequestType,
  detail: Schema.optional(TrimmedNonEmptyStringSchema),
  args: Schema.optional(Schema.Unknown),
});
export type RequestOpenedPayload = typeof RequestOpenedPayload.Type;
 
const RequestResolvedPayload = Schema.Struct({
  requestType: CanonicalRequestType,
  decision: Schema.optional(TrimmedNonEmptyStringSchema),
  resolution: Schema.optional(Schema.Unknown),
});
export type RequestResolvedPayload = typeof RequestResolvedPayload.Type;
 
const UserInputQuestionOption = Schema.Struct({
  label: TrimmedNonEmptyStringSchema,
  description: TrimmedNonEmptyStringSchema,
});
export type UserInputQuestionOption = typeof UserInputQuestionOption.Type;
 
export const UserInputQuestion = Schema.Struct({
  id: TrimmedNonEmptyStringSchema,
  header: TrimmedNonEmptyStringSchema,
  question: TrimmedNonEmptyStringSchema,
  options: Schema.Array(UserInputQuestionOption),
  multiSelect: Schema.optional(Schema.Boolean).pipe(
    Schema.withConstructorDefault(Effect.succeed(false)),
  ),
});
export type UserInputQuestion = typeof UserInputQuestion.Type;
 
const UserInputRequestedPayload = Schema.Struct({
  questions: Schema.Array(UserInputQuestion),
});
export type UserInputRequestedPayload = typeof UserInputRequestedPayload.Type;
 
const UserInputResolvedPayload = Schema.Struct({
  answers: UnknownRecordSchema,
});
export type UserInputResolvedPayload = typeof UserInputResolvedPayload.Type;
Read this as: Canonical approvals carry a decision; structured input carries typed questions and an answer record. They are deliberately different payloads.
  • Codex maps item/tool/requestUserInput into user-input.requested and native approval methods into request.opened.
  • Claude handles AskUserQuestion through the input flow; other tool decisions use the approval flow unless its runtime mode is full access.
  • Cursor and Grok wait on ACP deferred values for permission callbacks and their question extensions.
  • OpenCode maps permission.asked and question.asked separately, then calls permission.reply or question.reply.

All five adapters implement both response operations in the current source. That does not promise every provider originates both request kinds in every configuration. The adapter capability surfaces are the positive evidence here; they are not a guarantee that a native runtime will open a given request type during every session.

Pending is partly durable and partly live

Activities record approval.requested/approval.resolved and user-input.requested/user-input.resolved. Web and mobile derive open requests from ordered activity history, allowing another client surface to discover what needs attention.

Approvals additionally receive a projected pending-approval row, keyed by request id, with pending/resolved status, decision, and timestamps. Structured input does not reuse that table; its pending state derives from activities. The native adapter’s Deferred, map entry, or SDK callback remains live memory. A durable request record cannot recreate that wait after a process or server restart.

One response crosses four responsibilities

A web or mobile client dispatches either thread.approval.respond or thread.user-input.respond. The decider records response-requested intent. The provider command reactor resolves the thread, checks for a non-stopped session, calls ProviderService, and that service routes to the bound adapter. The adapter replies to its native runtime; later canonical resolution events return through runtime ingestion and become durable activities.

Acceptance of the client command is not a synchronous certificate that a provider completed the action, and the hot provider bridge is not a replayable outbox.

Figure 24.1 · A request has a durable shell around a live callbackresolution state can survive longer than the provider callback that created it
Approval and structured-input request pathDiagram loading

Zoom with the controls, +/, or Ctrl/ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.

Approval and structured-input request path
Text equivalent

Provider runtime request flows to ingestion, durable activity and optional pending-approval projection, then a client response command that records durable response intent. A provider command reactor routes that intent to the live bound adapter callback, which may emit a later resolution activity. A restart boundary separates durable request records from the in-memory provider callback.

Figure 24.1. A provider adapter emits a canonical approval or structured-input request. Runtime ingestion flushes the current assistant segment and records request activity; approvals also receive a dedicated projection. A client later records durable response intent. The provider command reactor must still find the live bound session and callback before the adapter can reply. A later native resolution returns through runtime ingestion. Persistence therefore preserves the product conversation, not the callback closure itself.
Interactive request router

Trace a mode and a request without flattening provider behavior

Choose a provider, stored T3 mode, and request shape. The panel distinguishes durable response intent from a live native reply.

Stored T3 mode
Request shape

Codex, approval-required, approval.

Codexapproval-required

Native starting behavior: untrusted · read-only · user reviewer

  1. 1Provider emitsnative approval request → decision
  2. 2Runtime ingestioncanonical request activity; pauses the active assistant segment
  3. 3Client commandthread.approval.respond records response intent
  4. 4Reactor and serviceresolve the bound session and call the adapter
  5. 5Provider replynative resolution later returns as activity
Static provider × mode matrix
Providerapproval-requiredauto-accept-editsautofull-access
Codexuntrusted · read-only · user revieweron-request · workspace-write · user revieweron-request · workspace-write · auto_reviewnever · danger-full-access · user reviewer
Claudeno explicit base-map entryacceptEditsautobypassPermissions + allowDangerouslySkipPermissions; AskUserQuestion still routes to structured input
Cursor (ACP)prefer native approval-mode alias; permission callbacks still waitprefer implement-mode alias; permission callbacks still waitprefer implement-mode alias; permission callbacks still waitprefer implement-mode alias; auto-select offered allow option or wait
Grok (ACP)no session-mode mapping; raised callbacks waitno session-mode mapping; raised callbacks waitno session-mode mapping; raised callbacks waitauto-select offered allow-always/allow-once; otherwise wait
OpenCodebroad operations ask; question is allowedbroad operations ask; question is allowedbroad operations ask; question is allowedallow all permission rules

Surface resolution and security boundaries

Web session logic and mobile’s selected-thread hook both derive pending approvals and pending user input from thread activities, then dispatch distinct commands. Mobile scopes drafts by environment-plus-request id. The shared model is a request addressed to a thread; each client chooses its controls.

Boundary Owns Does not prove
Thread/orchestration selected mode, response intent, activities, projected approval state a live native request still exists or an action succeeded
Client request presentation and answer/decision collection sandbox enforcement or provider identity
Provider reactor/service exact bound-session route and response failures provider parity or crash-proof delivery
Adapter/native runtime reviewer, sandbox, callback, and reply semantics that a T3 label means the same thing elsewhere
Person operating the client the authorization decision that a broad mode replaces reviewing a specific request

Permission capability metadata could make these differences easier to negotiate in a UI, but none exists at this pinned revision. That is a future design direction, not current behavior.

T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.