# Files changed · PR \#23631 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23631/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(server\): Codex keeps your permissions profile after Full access and back to Auto](https://git.cafe/pingdotgg/t3code/pull/23631.md)

Version: 4

Head commit: a9ae44f56c0947fd069ea3f741351e6431c18d98

Base commit: f054d1b8018721bd997d99cbb918a7b17bf098a4

Comparison base: f054d1b8018721bd997d99cbb918a7b17bf098a4

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts

Status: modified; +338; −0

```diff
@@ -173,6 +173,60 @@
     }),
   );
 
+  it.effect("lets an explicit sandbox policy override the user's permissions profile", () =>
+    Effect.gen(function* () {
+      const params = yield* CodexAdapterV2.buildCodexTurnStartParams({
+        nativeThreadId: "native-sandbox-override",
+        codexInput: [{ type: "text", text: "test" }],
+        runtimePolicy: {
+          runtimeMode: "auto",
+          interactionMode: "default",
+          cwd: null,
+          sandboxPolicy: { type: "workspaceWrite", networkAccess: true },
+        },
+        modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
+        permissionProfile: { id: "env-protected", sandboxType: "workspaceWrite" },
+      });
+      assert.deepEqual(params.sandboxPolicy, { type: "workspaceWrite", networkAccess: true });
+      assert.isUndefined(params.permissions);
+    }),
+  );
+
+  it.effect.each([
+    { runtimeMode: "approval-required", sandboxType: "readOnly" },
+    { runtimeMode: "full-access", sandboxType: "dangerFullAccess" },
+  ] as const)(
+    "sends $runtimeMode's own sandbox when the profile is for another sandbox",
+    ({ runtimeMode, sandboxType }) =>
+      Effect.gen(function* () {
+        const params = yield* CodexAdapterV2.buildCodexTurnStartParams({
+          nativeThreadId: `native-profile-${runtimeMode}`,
+          codexInput: [{ type: "text", text: "test" }],
+          runtimePolicy: { runtimeMode, interactionMode: "default", cwd: null },
+          modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
+          permissionProfile: { id: "env-protected", sandboxType: "workspaceWrite" },
+        });
+        assert.deepEqual(params.sandboxPolicy, { type: sandboxType });
+        assert.isUndefined(params.permissions);
+      }),
+  );
+
+  it("remembers no profile for Codex's built-in permission profiles", () => {
+    const sandbox = { type: "workspaceWrite", writableRoots: [], networkAccess: false };
+    for (const id of [":workspace", ":read-only"]) {
+      assert.isUndefined(
+        CodexAdapterV2.codexUserPermissionProfile({ activePermissionProfile: { id }, sandbox }),
+      );
+    }
+    assert.deepEqual(
+      CodexAdapterV2.codexUserPermissionProfile({
+        activePermissionProfile: { id: "env-protected" },
+        sandbox,
+      }),
+      { id: "env-protected", sandboxType: "workspaceWrite" },
+    );
+  });
+
   it.effect("sends MCP app model context as untrusted Codex context", () =>
     Effect.gen(function* () {
       const policy = { runtimeMode: "full-access", interactionMode: "default", cwd: null } as const;
@@ -518,6 +572,30 @@
   };
 }
 
+/** Makes the preamble's thread/start response report a user permissions profile. */
+function withActivePermissionProfile(
+  entries: ReadonlyArray<CodexReplay.CodexAppServerReplayEntry>,
+  id: string,
+): Array<CodexReplay.CodexAppServerReplayEntry> {
+  return entries.map((entry) =>
+    entry.type === "emit_inbound" &&
+    entry.label === "thread/start" &&
+    Predicate.isObject(entry.frame) &&
+    Predicate.isObject(entry.frame.result)
+      ? {
+          ...entry,
+          frame: {
+            ...entry.frame,
+            result: {
+              ...entry.frame.result,
+              activePermissionProfile: { id, extends: ":workspace" },
+            },
+          },
+        }
+      : entry,
+  );
+}
+
 function codexReplayPreamble(input: {
   readonly nativeThreadId: string;
   readonly nativeTurnId: string;
@@ -909,6 +987,13 @@
                   (client) =>
                     ({
                       ...client,
+                      raw: {
+                        ...client.raw,
+                        request: (method, params) =>
+                          onRequest(method, params).pipe(
+                            Effect.andThen(client.raw.request(method, params)),
+                          ),
+                      },
                       request: (method, params) =>
                         onRequest(method, params).pipe(
                           Effect.andThen(client.request(method, params)),
@@ -1684,6 +1769,259 @@
     ),
   );
 
+  it.effect(
+    "keeps the user's permissions profile across an Auto → Full access → Auto round trip",
+    () =>
+      Effect.scoped(
+        Effect.gen(function* () {
+          const nativeThreadId = "permissions-profile-thread";
+          const turnIds = ["profile-turn-1", "profile-turn-2", "profile-turn-3"];
+          // Codex only keeps a named profile while the turn's legacy sandboxPolicy
+          // equals its projection; selecting the profile by id restores its
+          // filesystem deny entries after a detour through Full access. T3
+          // resumes the loaded thread before each turn whose runtime settings
+          // changed, and those responses report the thread's current state:
+          // no active profile once a legacy sandboxPolicy turn has run.
+          const turnPolicies = {
+            auto: {
+              approvalPolicy: "on-request",
+              approvalsReviewer: "auto_review",
+              permissions: "env-protected",
+            },
+            "full-access": {
+              approvalPolicy: "never",
+              approvalsReviewer: "user",
+              sandboxPolicy: { type: "dangerFullAccess" },
+            },
+          } as const;
+          const turnModes = ["auto", "full-access", "auto"] as const;
+          const turnText = (index: number) => `Profile round ${index + 1}.`;
+          const turnEvent = (
+            method: "turn/started" | "turn/completed",
+            turnId: string,
+          ): CodexReplay.CodexAppServerReplayEntry => ({
+            type: "emit_inbound",
+            label: `${turnId} ${method}`,
+            frame: {
+              method,
+              params: {
+                threadId: nativeThreadId,
+                turn: makeCodexReplayTurn({
+                  id: turnId,
+                  status: method === "turn/started" ? "inProgress" : "completed",
+                }),
+              },
+            },
+          });
+          const runTurnEntries = (
+            index: number,
+          ): ReadonlyArray<CodexReplay.CodexAppServerReplayEntry> => [
+            {
+              type: "expect_outbound",
+              label: `turn ${index + 1} (${turnModes[index]}) turn/start`,
+              frame: {
+                id: 3 + index * 2,
+                method: "turn/start",
+                params: {
+                  threadId: nativeThreadId,
+                  input: [{ type: "text", text: turnText(index) }],
+                  cwd: "/workspace",
+                  model: "gpt-5.4",
+                  ...turnPolicies[turnModes[index]!],
+                  summary: "detailed",
+                },
+              },
+            },
+            {
+              type: "emit_inbound",
+              label: `turn ${index + 1} turn/start`,
+              frame: {
+                id: 3 + index * 2,
+                result: {
+                  turn: makeCodexReplayTurn({ id: turnIds[index]!, status: "inProgress" }),
+                },
+              },
+            },
+            turnEvent("turn/started", turnIds[index]!),
+            turnEvent("turn/completed", turnIds[index]!),
+          ];
+          const resumeEntries = (
+            index: number,
+            sandbox: Schema.Json,
+          ): ReadonlyArray<CodexReplay.CodexAppServerReplayEntry> => [
+            {
+              type: "expect_outbound",
+              label: `turn ${index + 1} (${turnModes[index]}) thread/resume`,
+              frame: {
+                id: 2 + index * 2,
+                method: "thread/resume",
+                params: {
+                  threadId: nativeThreadId,
+                  excludeTurns: true,
+                  cwd: "/workspace",
+                  model: "gpt-5.4",
+                  config: CodexAdapterV2.CODEX_THREAD_CONFIG,
+                },
+              },
+            },
+            {
+              type: "emit_inbound",
+              label: `turn ${index + 1} thread/resume`,
+              frame: {
+                id: 2 + index * 2,
+                result: {
+                  thread: { id: nativeThreadId, updatedAt: 1782622450 + index },
+                  activePermissionProfile: null,
+                  sandbox,
+                },
+              },
+            },
+          ];
+          const preamble = withActivePermissionProfile(
+            codexReplayPreamble({
+              nativeThreadId,
+              nativeTurnId: turnIds[0]!,
+              prompt: turnText(0),
+            }).slice(0, -3),
+            "env-protected",
+          );
+          const harness = yield* makeCodexReplayHarness(
+            makeCodexReplayTranscript({
+              scenario: "codex-permissions-profile-round-trip",
+              entries: [
+                ...preamble,
+                ...runTurnEntries(0),
+                ...resumeEntries(1, {
+                  type: "workspaceWrite",
+                  writableRoots: [],
+                  networkAccess: false,
+                }),
+                ...runTurnEntries(1),
+                ...resumeEntries(2, { type: "dangerFullAccess" }),
+                ...runTurnEntries(2),
+              ],
+            }),
+          );
+
+          for (const [index, runtimeMode] of turnModes.entries()) {
+            const runtimePolicy = { ...CODEX_TEST_RUNTIME_POLICY, runtimeMode };
+            if (index > 0) {
+              yield* harness.runtime.resumeThread({
+                providerThread: harness.providerThread,
+                modelSelection: CODEX_TEST_MODEL_SELECTION,
+                runtimePolicy,
+              });
+            }
+            yield* harness.runtime.startTurn({
+              ...makeCodexTestTurnInput({
+                threadId: harness.threadId,
+                providerThread: harness.providerThread,
+                now: yield* DateTime.now,
+                attemptId: RunAttemptId.make(`permissions-profile-${index + 1}`),
+                text: turnText(index),
+              }),
+              runOrdinal: index + 1,
+              providerTurnOrdinal: index + 1,
+              runtimePolicy,
+            });
+            yield* harness.awaitTerminal(index + 1);
+          }
+        }).pipe(
+          Effect.provide(
+            Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
+          ),
+        ),
+      ),
+  );
+
+  it.effect("falls back to the legacy sandbox policy when Codex no longer knows the profile", () =>
+    Effect.scoped(
+      Effect.gen(function* () {
+        const nativeThreadId = "stale-profile-thread";
+        const nativeTurnId = "stale-profile-turn";
+        const prompt = "Use the renamed profile.";
+        const turnStart = (id: number, policy: Record<string, Schema.Json>) => ({
+          type: "expect_outbound" as const,
+          label: `turn/start ${id}`,
+          frame: {
+            id,
+            method: "turn/start",
+            params: {
+              threadId: nativeThreadId,
+              input: [{ type: "text", text: prompt }],
+              cwd: "/workspace",
+              model: "gpt-5.4",
+              approvalPolicy: "on-request",
+              approvalsReviewer: "auto_review",
+              ...policy,
+              summary: "detailed",
+            },
+          },
+        });
+        const harness = yield* makeCodexReplayHarness(
+          makeCodexReplayTranscript({
+            scenario: "codex-stale-permissions-profile",
+            entries: [
+              ...withActivePermissionProfile(
+                codexReplayPreamble({ nativeThreadId, nativeTurnId, prompt }).slice(0, -3),
+                "env-protected",
+              ),
+              turnStart(3, { permissions: "env-protected" }),
+              {
+                type: "emit_inbound",
+                label: "turn/start rejected",
+                frame: {
+                  id: 3,
+                  error: {
+                    code: -32600,
+                    message:
+                      "failed to load configuration: default_permissions refers to undefined profile `env-protected`",
+                  },
+                },
+              },
+              turnStart(4, { sandboxPolicy: { type: "workspaceWrite" } }),
+              {
+                type: "emit_inbound",
+                label: "turn/start",
+                frame: {
+                  id: 4,
+                  result: { turn: makeCodexReplayTurn({ id: nativeTurnId, status: "inProgress" }) },
+                },
+              },
+              {
+                type: "emit_inbound",
+                label: "turn/completed",
+                frame: {
+                  method: "turn/completed",
+                  params: {
+                    threadId: nativeThreadId,
+                    turn: makeCodexReplayTurn({ id: nativeTurnId, status: "completed" }),
+                  },
+                },
+              },
+            ],
+          }),
+        );
+
+        yield* harness.runtime.startTurn({
+          ...makeCodexTestTurnInput({
+            threadId: harness.threadId,
+            providerThread: harness.providerThread,
+            now: yield* DateTime.now,
+            attemptId: RunAttemptId.make("stale-profile-1"),
+            text: prompt,
+          }),
+          runtimePolicy: { ...CODEX_TEST_RUNTIME_POLICY, runtimeMode: "auto" },
+        });
+        yield* harness.awaitTerminal(1);
+      }).pipe(
+        Effect.provide(
+          Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
+        ),
+      ),
+    ),
+  );
+
   it.effect("preserves T3 context on the wire and restores it after compaction", () =>
     Effect.scoped(
       Effect.gen(function* () {
```

## apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts

Status: modified; +116; −24

```diff
@@ -664,6 +664,7 @@
     additionalContext: Schema.optionalKey(
       Schema.Record(Schema.String, CodexSchema.V2TurnStartParams__AdditionalContextEntry),
     ),
+    permissions: Schema.optionalKey(Schema.String),
   }),
 );
 type CodexTurnStartParamsWithCollaborationMode =
@@ -675,6 +676,30 @@
   ProviderAdapter.ProviderAdapterRuntimeRequestResponseError,
 );
 
+/** A permission profile from the user's Codex config, as thread/start or thread/resume reports it. */
+interface CodexUserPermissionProfile {
+  readonly id: string;
+  readonly sandboxType: string;
+}
+
+// activePermissionProfile is not in the generated response schemas yet.
+const decodeCodexActivePermissionProfile = Schema.decodeUnknownOption(
+  Schema.Struct({
+    activePermissionProfile: Schema.Struct({ id: Schema.String }),
+    sandbox: Schema.Struct({ type: Schema.String }),
+  }),
+);
+
+export function codexUserPermissionProfile(
+  response: unknown,
+): CodexUserPermissionProfile | undefined {
+  const decoded = decodeCodexActivePermissionProfile(response);
+  if (Option.isNone(decoded)) return undefined;
+  const { id } = decoded.value.activePermissionProfile;
+  // Ids starting with ":" are Codex built-ins, which the legacy sandboxPolicy already expresses.
+  return id.startsWith(":") ? undefined : { id, sandboxType: decoded.value.sandbox.type };
+}
+
 function codexRuntimeModeTurnDefaults(runtimeMode: RuntimeMode): {
   readonly approvalPolicy: CodexSchema.V2TurnStartParams__AskForApproval;
   readonly approvalsReviewer: CodexSchema.V2TurnStartParams__ApprovalsReviewer;
@@ -728,16 +753,27 @@
     /** ChatGPT token sharing does not accept service tiers. */
     readonly omitServiceTier?: boolean;
     readonly appContext?: ProviderAdapter.ProviderAdapterV2TurnInput["appContext"];
+    readonly permissionProfile?: CodexUserPermissionProfile;
   }) {
     const runtimeModeDefaults = codexRuntimeModeTurnDefaults(input.runtimePolicy.runtimeMode);
+    // Codex drops a named profile, and its deny-read entries, once a turn's legacy
+    // sandboxPolicy stops matching it; selecting it by id brings it back after
+    // another mode's turn. Codex rejects `permissions` alongside sandboxPolicy.
+    const permissions =
+      input.runtimePolicy.sandboxPolicy === undefined &&
+      input.permissionProfile?.sandboxType === runtimeModeDefaults.sandboxPolicy.type
+        ? input.permissionProfile.id
+        : undefined;
     const approvalPolicy =
       input.runtimePolicy.approvalPolicy === undefined
         ? runtimeModeDefaults.approvalPolicy
         : yield* decodeTurnApprovalPolicy(input.runtimePolicy.approvalPolicy);
     const sandboxPolicy =
-      input.runtimePolicy.sandboxPolicy === undefined
+      permissions !== undefined
+        ? undefined
+        : input.runtimePolicy.sandboxPolicy === undefined
+          ? runtimeModeDefaults.sandboxPolicy
+          : yield* decodeTurnSandboxPolicy(input.runtimePolicy.sandboxPolicy);
-        ? runtimeModeDefaults.sandboxPolicy
-        : yield* decodeTurnSandboxPolicy(input.runtimePolicy.sandboxPolicy);
     const selectedEffort =
       getModelSelectionStringOptionValue(input.modelSelection, "reasoningEffort") ??
       codexDefaultReasoningEffortOverride(input.modelSelection.model);
@@ -803,6 +839,7 @@
       approvalsReviewer: runtimeModeDefaults.approvalsReviewer,
       ...(approvalPolicy === undefined ? {} : { approvalPolicy }),
       ...(sandboxPolicy === undefined ? {} : { sandboxPolicy }),
+      ...(permissions === undefined ? {} : { permissions }),
       ...(effort === undefined ? {} : { effort }),
       ...(serviceTier === undefined ? {} : { serviceTier }),
       ...(collaborationMode === undefined ? {} : { collaborationMode }),
@@ -1377,6 +1414,12 @@
   };
 }
 
+const decodeCodexThreadStartResponse = Schema.decodeUnknownEffect(
+  CodexSchema.V2ThreadStartResponse,
+);
+
+const decodeCodexThreadForkResponse = Schema.decodeUnknownEffect(CodexSchema.V2ThreadForkResponse);
+
 const decodeCodexResumeMetadata = Schema.decodeUnknownEffect(
   Schema.Struct({ thread: Schema.Struct({ id: Schema.String, updatedAt: Schema.Number }) }),
 );
@@ -1845,6 +1888,14 @@
         );
         const activeTurns = yield* Ref.make(new Map<string, ActiveCodexTurnContext>());
         const turnTokenUsageByThread = new Map<string, CodexTurnTokenUsageState>();
+        const permissionProfilesByThread = new Map<string, CodexUserPermissionProfile>();
+        // Resuming a loaded thread reports its current state, which has no
+        // active profile after any legacy sandboxPolicy turn, so only a
+        // reported profile replaces the remembered one.
+        const rememberPermissionProfile = (nativeThreadId: string, response: unknown) => {
+          const profile = codexUserPermissionProfile(response);
+          if (profile !== undefined) permissionProfilesByThread.set(nativeThreadId, profile);
+        };
         const usageStateForThread = (nativeThreadId: string) => {
           let state = turnTokenUsageByThread.get(nativeThreadId);
           if (!state) {
@@ -6356,17 +6407,20 @@
           ) {
             const threadId = yield* getNativeThreadId(turnInput.providerThread);
             const mcpSession = yield* mcpSessions.read(turnInput.threadId);
+            const buildParams = (permissionProfile: CodexUserPermissionProfile | undefined) =>
-            const turnStartParams = yield* buildCodexTurnStartParams({
-              nativeThreadId: threadId,
-              codexInput,
+              buildCodexTurnStartParams({
-              runtimePolicy: turnInput.runtimePolicy,
-              modelSelection: turnInput.modelSelection,
-              hasT3Mcp: mcpSession !== undefined,
-              browserToolsAvailable: mcpSession?.browserToolsAvailable ?? true,
-              deviceToolsAvailable: mcpSession?.capabilities?.has("device") ?? false,
-              omitServiceTier: adapterOptions.resolveRuntime !== undefined,
-              ...(turnInput.appContext === undefined ? {} : { appContext: turnInput.appContext }),
-            });
+                nativeThreadId: threadId,
+                codexInput,
+                runtimePolicy: turnInput.runtimePolicy,
+                modelSelection: turnInput.modelSelection,
+                hasT3Mcp: mcpSession !== undefined,
+                browserToolsAvailable: mcpSession?.browserToolsAvailable ?? true,
+                deviceToolsAvailable: mcpSession?.capabilities?.has("device") ?? false,
+                omitServiceTier: adapterOptions.resolveRuntime !== undefined,
+                ...(turnInput.appContext === undefined ? {} : { appContext: turnInput.appContext }),
+                ...(permissionProfile === undefined ? {} : { permissionProfile }),
+              });
+            const turnStartParams = yield* buildParams(permissionProfilesByThread.get(threadId));
             yield* Ref.update(pendingRootTurns, (current) => {
               const updated = new Map(current);
               updated.set(threadId, turnInput);
@@ -6379,7 +6433,25 @@
               else next.delete(threadId);
               return next;
             });
+            const started = yield* client.request("turn/start", turnStartParams).pipe(
+              Effect.catchTags({
+                // The profile may have been renamed or removed from config.toml
+                // since Codex reported it; fall back to the legacy sandboxPolicy.
+                // Codex names `default_permissions` in both errors it gives for that.
+                CodexAppServerRequestError: (error) => {
+                  if (
+                    turnStartParams.permissions === undefined ||
+                    !error.errorMessage.includes("default_permissions")
+                  ) {
+                    return Effect.fail(error);
+                  }
+                  permissionProfilesByThread.delete(threadId);
+                  return buildParams(undefined).pipe(
+                    Effect.flatMap((params) => client.request("turn/start", params)),
-            const started = yield* client.request("turn/start", turnStartParams);
+                  );
+                },
+              }),
+            );
             yield* registerRootTurn({
               turnInput,
               nativeTurnId: started.turn.id,
@@ -6641,7 +6713,7 @@
             ensureInitialized.pipe(
               Effect.andThen(mcpSessions.read(threadInput.threadId)),
               Effect.flatMap((mcpSession) =>
-                client.request(
+                client.raw.request(
                   "thread/start",
                   codexThreadRuntimeParams({
                     mcpSession,
@@ -6650,6 +6722,13 @@
                   }),
                 ),
               ),
+              Effect.flatMap((raw) =>
+                decodeCodexThreadStartResponse(raw).pipe(
+                  Effect.tap((response) =>
+                    Effect.sync(() => rememberPermissionProfile(response.thread.id, raw)),
+                  ),
+                ),
+              ),
               Effect.map((response): OrchestrationV2ProviderThread =>
                 providerThreadFromCodexThread({
                   appThreadId: threadInput.threadId,
@@ -6706,6 +6785,9 @@
                       },
                     }),
                   ),
+                ),
+                Effect.tap((raw) =>
+                  Effect.sync(() => rememberPermissionProfile(nativeThreadId, raw)),
                 ),
                 Effect.flatMap(decodeCodexResumeMetadata),
               );
@@ -7413,15 +7495,18 @@
               // process. After a restart or idle release, load it the same way
               // the next turn would before reverting.
               if (!loaded) {
-                yield* client.raw.request("thread/resume", {
+                rememberPermissionProfile(
                   threadId,
+                  yield* client.raw.request("thread/resume", {
+                    threadId,
+                    excludeTurns: true,
+                    ...codexThreadRuntimeParams({
+                      mcpSession: yield* readMcpSession(threadInput.providerThread.appThreadId),
+                      modelSelection: input.modelSelection,
+                      runtimePolicy: input.runtimePolicy,
-                  excludeTurns: true,
+                    }),
-                  ...codexThreadRuntimeParams({
-                    mcpSession: yield* readMcpSession(threadInput.providerThread.appThreadId),
-                    modelSelection: input.modelSelection,
-                    runtimePolicy: input.runtimePolicy,
                   }),
-                });
+                );
               }
               const response = yield* ensureInitialized.pipe(
                 Effect.andThen(revertCodexThread(client, threadId, numTurns)),
@@ -7463,7 +7548,7 @@
               const boundary = yield* resolveCodexForkBoundary(threadInput);
               const response = yield* ensureInitialized.pipe(
                 Effect.andThen(
+                  client.raw.request("thread/fork", {
-                  client.request("thread/fork", {
                     threadId,
                     ...(boundary.lastTurnId === undefined
                       ? {}
@@ -7479,6 +7564,13 @@
                     }),
                   }),
                 ),
+                Effect.flatMap((raw) =>
+                  decodeCodexThreadForkResponse(raw).pipe(
+                    Effect.tap((response) =>
+                      Effect.sync(() => rememberPermissionProfile(response.thread.id, raw)),
+                    ),
+                  ),
+                ),
                 Effect.mapError(
                   (cause) =>
                     new ProviderAdapter.ProviderAdapterForkThreadError({
```

## packages/effect-codex-app-server/src/client.ts

Status: modified; +1; −0

```diff
@@ -91,6 +91,7 @@
     additionalContext: Schema.optionalKey(
       Schema.Record(Schema.String, CodexSchema.V2TurnStartParams__AdditionalContextEntry),
     ),
+    permissions: Schema.optionalKey(Schema.String),
   }),
 );
 
```
