fix(codex): keep authenticated user identity in agent turns (#121511)

* fix(codex): preserve authenticated sender context

* test(codex): type sender recorder fixture

* fix(codex): scope sender context to user turns
This commit is contained in:
Peter Steinberger
2026-08-10 02:54:04 -07:00
committed by GitHub
parent 27d2af2298
commit c034670b72
5 changed files with 140 additions and 0 deletions
@@ -365,6 +365,7 @@ type CodexTurnInterruptParams = JsonObject & {
export type CodexTurnStartParams = JsonObject & {
threadId: string;
input: CodexUserInput[];
additionalContext?: Record<string, { kind: "untrusted" | "application"; value: string }>;
cwd?: string;
model?: string;
approvalPolicy?: CodexApprovalPolicy | null;
@@ -5016,6 +5016,90 @@ describe("runCodexAppServerAttempt", () => {
const resumeRequestParams = resumeRequest?.params as Record<string, unknown> | undefined;
expect(resumeRequestParams?.developerInstructions).not.toContain(CODEX_GPT5_BEHAVIOR_CONTRACT);
});
it("sends the current recorded sender on successive turns of one resumed Codex thread", async () => {
const { sessionFile, workspaceDir } = createRunPaths();
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
const turnIds = ["turn-ada", "turn-grace"] as const;
let nextTurnIndex = 0;
const harness = createAppServerHarness(async (method, params) => {
if (method === "thread/resume") {
return threadStartResult((params as { threadId?: string }).threadId ?? "thread-existing");
}
if (method === "turn/start") {
const turnId = turnIds[nextTurnIndex++];
if (!turnId) {
throw new Error("unexpected extra turn/start");
}
return turnStartResult(turnId);
}
return {};
});
const runTurn = async (sender: { id: string; name: string }, prompt: string, runId: string) => {
const expectedTurnStarts =
harness.requests.filter((request) => request.method === "turn/start").length + 1;
const params = createParams(sessionFile, workspaceDir, { prompt, runId });
params.trigger = "user";
const message = {
role: "user" as const,
content: prompt,
timestamp: Date.now(),
__openclaw: { senderId: sender.id, senderName: sender.name },
};
params.userTurnTranscriptRecorder = {
message,
resolveMessage: async () => message,
getAdmissionReceipt: () => undefined,
markRuntimePersistencePending() {},
markRuntimePersisted() {},
} as unknown as EmbeddedRunAttemptParams["userTurnTranscriptRecorder"];
const run = runCodexAppServerAttempt(params);
await vi.waitFor(
() =>
expect(
harness.requests.filter((request) => request.method === "turn/start"),
).toHaveLength(expectedTurnStarts),
fastWait,
);
await harness.completeTurn({
threadId: "thread-existing",
turnId: `turn-${sender.name.toLowerCase()}`,
});
await run;
};
await runTurn({ id: "profile-ada", name: "Ada" }, "first request", "run-ada");
await runTurn({ id: "profile-grace", name: "Grace" }, "second request", "run-grace");
expect(
harness.requests
.filter((request) =>
["thread/start", "thread/resume", "turn/start"].includes(request.method),
)
.map((request) => request.method),
).toEqual(["thread/resume", "turn/start", "turn/start"]);
expect(
harness.requests
.filter((request) => request.method === "turn/start")
.map(
(request) =>
(
request.params as {
additionalContext?: Record<string, { kind: string; value: string }>;
}
).additionalContext?.openclaw_current_sender,
),
).toEqual([
{
kind: "untrusted",
value: '{"sender":{"id":"profile-ada","name":"Ada"}}',
},
{
kind: "untrusted",
value: '{"sender":{"id":"profile-grace","name":"Grace"}}',
},
]);
});
it("starts a fresh Codex thread before resume when the native rollout reaches the fallback fuse", async () => {
const { sessionFile, workspaceDir, agentDir } = createRunPaths();
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
@@ -1,5 +1,10 @@
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
import { GPT5_HEARTBEAT_PROMPT_OVERLAY as CODEX_GPT5_HEARTBEAT_PROMPT_OVERLAY } from "openclaw/plugin-sdk/provider-model-shared";
import {
asOptionalRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { codexSandboxPolicyForTurn, type CodexAppServerRuntimeOptions } from "./config.js";
import type {
CodexSandboxPolicy,
@@ -14,6 +19,37 @@ import {
} from "./thread-model-selection.js";
import { buildCodexUserInput } from "./user-input.js";
const CODEX_CURRENT_SENDER_FIELD_MAX_CHARS = 256;
function buildCodexCurrentSenderContextValue(params: EmbeddedRunAttemptParams): string | undefined {
const metadata = asOptionalRecord(
asOptionalRecord(params.userTurnTranscriptRecorder?.message as unknown)?.["__openclaw"],
);
const recorded = [
normalizeOptionalString(metadata?.["senderId"]),
normalizeOptionalString(metadata?.["senderName"]),
normalizeOptionalString(metadata?.["senderUsername"]),
] as const;
const [id, name, username] = recorded.some(Boolean)
? recorded
: [
normalizeOptionalString(params.senderId),
normalizeOptionalString(params.senderName),
normalizeOptionalString(params.senderUsername),
];
if (!id && !name && !username) {
return undefined;
}
const bound = (value: string) => truncateUtf16Safe(value, CODEX_CURRENT_SENDER_FIELD_MAX_CHARS);
return JSON.stringify({
sender: {
...(id ? { id: bound(id) } : {}),
...(name ? { name: bound(name) } : {}),
...(username ? { username: bound(username) } : {}),
},
});
}
export function buildTurnStartParams(
params: EmbeddedRunAttemptParams,
options: {
@@ -43,9 +79,16 @@ export function buildTurnStartParams(
config: params.config,
});
const useThreadPermissionProfile = options.appServer.networkProxy && !options.sandboxPolicy;
const currentSenderContext =
params.trigger === "user" ? buildCodexCurrentSenderContextValue(params) : undefined;
// Untrusted context exposes authenticated attribution without promoting human-controlled labels.
const additionalContext: CodexTurnStartParams["additionalContext"] = currentSenderContext
? { openclaw_current_sender: { kind: "untrusted", value: currentSenderContext } }
: undefined;
return {
threadId: options.threadId,
input: buildCodexUserInput(options.promptText ?? params.prompt, params.images),
...(additionalContext ? { additionalContext } : {}),
cwd: options.cwd,
approvalPolicy: options.appServer.approvalPolicy,
approvalsReviewer: options.appServer.approvalsReviewer,
@@ -132,6 +132,12 @@
```json
{
"additionalContext": {
"openclaw_current_sender": {
"kind": "untrusted",
"value": "{\"sender\":{\"id\":\"424242\",\"name\":\"Pash\",\"username\":\"pash\"}}"
}
},
"approvalPolicy": "never",
"approvalsReviewer": "user",
"collaborationMode": {
@@ -132,6 +132,12 @@
```json
{
"additionalContext": {
"openclaw_current_sender": {
"kind": "untrusted",
"value": "{\"sender\":{\"id\":\"1000001\",\"name\":\"Pash\",\"username\":\"pash\"}}"
}
},
"approvalPolicy": "never",
"approvalsReviewer": "user",
"collaborationMode": {