Part VI · The work lifecyclePermissions and input
Chapter 39source 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 · 01c47633
        return Effect.succeed(base as typeof ModelSelectionSource.Encoded);
      },
    }),
  ),
);
export type ModelSelection = typeof ModelSelection.Type;
 
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",
  "mcp-elicitation",
]);
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 the executed mappings. 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 · 849989c2
  },
) {
  override get message(): string {
    return `Invalid Codex user input answers for question '${this.questionId}'`;
  }
}
 
export class CodexSessionRuntimeThreadIdMissingError extends Schema.TaggedError<CodexSessionRuntimeThreadIdMissingError>()(
  "CodexSessionRuntimeThreadIdMissingError",
  {
    threadId: Schema.String,
  },
) {
  override get message(): string {
    return `Codex session is missing a provider thread id for ${this.threadId}`;
  }
}
 
interface PendingApproval {
  readonly requestId: ApprovalRequestId;
  readonly jsonRpcId: string;
  readonly requestKind: ProviderRequestKind;
  readonly turnId: TurnId | undefined;
  readonly itemId: ProviderItemId | undefined;
  readonly decision: Deferred.Deferred<ProviderApprovalDecision>;
}
 
interface ApprovalCorrelation {
  readonly requestId: ApprovalRequestId;
  readonly requestKind: ProviderRequestKind;
  readonly turnId: TurnId | undefined;
  readonly itemId: ProviderItemId | undefined;
}
 
interface PendingUserInput {
  readonly requestId: ApprovalRequestId;
  readonly turnId: TurnId | undefined;
  readonly itemId: ProviderItemId | undefined;
  readonly answers: Deferred.Deferred<ProviderUserInputAnswers>;
}
 
type McpElicitationPersistenceDecision = Extract<
  ProviderApprovalDecision,
  "acceptForSession" | "acceptAlways"
>;
 
function mcpElicitationPersistenceDecision(
  value: string,
): McpElicitationPersistenceDecision | null {
  const normalized = value.toLowerCase();
  if (normalized.includes("session")) return "acceptForSession";
  if (
    normalized.includes("always") ||
    normalized.includes("permanent") ||
    normalized.includes("forever") ||
    normalized.includes("persistent")
  ) {
    return "acceptAlways";
  }
  return null;
}
 
function mcpElicitationFormFields(payload: EffectCodexSchema.McpServerElicitationRequestParams) {
  if (payload.mode === "url" || !isMcpElicitationForm(payload.requestedSchema)) {
    return undefined;
  }
  return payload.requestedSchema;
}
 
function mcpElicitationFieldOptions(field: typeof McpElicitationFormField.Type) {
  if (field.oneOf) {
    return field.oneOf.map((option) => ({ value: option.const, label: option.title }));
  }
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 · c229d8f7
  delta: Schema.String,
});
export type TurnProposedDeltaPayload = typeof TurnProposedDeltaPayload.Type;
 
const TurnProposedCompletedPayload = Schema.Struct({
  planMarkdown: TrimmedNonEmptyStringSchema,
});
export type TurnProposedCompletedPayload = typeof TurnProposedCompletedPayload.Type;
 
const TurnDiffUpdatedPayload = Schema.Struct({
  unifiedDiff: Schema.String,
});
export type TurnDiffUpdatedPayload = typeof TurnDiffUpdatedPayload.Type;
 
export const ToolActivitySurface = Schema.Literals(["browser", "computer"]);
export type ToolActivitySurface = typeof ToolActivitySurface.Type;
 
export const ToolActivityNativeAppReference = Schema.Union([
  Schema.TaggedStruct("app-id", {
    appId: TrimmedNonEmptyStringSchema.check(
      Schema.isMaxLength(512),
      Schema.isPattern(/^[A-Za-z0-9._-]+$/u),
    ),
  }),
  Schema.TaggedStruct("display-name", {
    displayName: TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(160)),
  }),
]);
export type ToolActivityNativeAppReference = typeof ToolActivityNativeAppReference.Type;
 
export const ToolActivityIcon = Schema.Union([
  Schema.TaggedStruct("website", {
    pageUrl: TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(4096)),
    faviconUrl: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(4096))),
    faviconUrlDark: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(4096))),
  }),
  Schema.TaggedStruct("native-app", {
    app: ToolActivityNativeAppReference,
  }),
  Schema.TaggedStruct("themed-logo", {
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 six adapters implement the shared response boundary, with provider-specific support for originating each request shape. 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 39.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 39.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 no central inbox is implemented. That is a future design direction, not current behavior.

T3
Source-locked editionRead against 859304b78 · 10 Sept 2026
Book search

Find a concept, module, or source path

Type two or more characters.