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.
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;| 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.
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",
};
}
}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.
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;- Codex maps
item/tool/requestUserInputintouser-input.requestedand native approval methods intorequest.opened. - Claude handles
AskUserQuestionthrough 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.askedandquestion.askedseparately, then callspermission.replyorquestion.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.
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.
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.
packages/contracts/src/providerRuntime.ts:430–469 ↗apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:299–539 ↗apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:1707–1749 ↗apps/server/src/orchestration/decider.ts:1061–1110 ↗apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1199–1285 ↗apps/server/src/orchestration/Layers/ProjectionPipeline.ts:1540–1601 ↗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.