Skip to content

Commit 9b560b8

Browse files
steipeteEva (agent)
andauthored
fix: limit Codex attribution to local transcripts
Summary: - Limit canonical OpenAI Codex app-server attribution rewrites to local transcript and trajectory records. - Keep runtime/tool routing on the selected OpenAI model metadata, including OpenAI API-key backup profiles. - Fix the current gateway-readiness lint blocker that was red on main. Verification: - codex-review branch helper clean with focused Codex app-server tests. - pnpm lint --threads=8 - pnpm test src/commands/gateway-readiness.test.ts - GitHub CI run 25960997256 green. Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org>
1 parent c6af990 commit 9b560b8

6 files changed

Lines changed: 224 additions & 8 deletions

File tree

extensions/codex/src/app-server/event-projector.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,94 @@ describe("CodexAppServerEventProjector", () => {
307307
expect(result.replayMetadata.replaySafe).toBe(true);
308308
});
309309

310+
it("records canonical OpenAI Codex app-server turns with Codex local attribution", async () => {
311+
const params = await createParams();
312+
const projector = await createProjector({
313+
...params,
314+
provider: "openai",
315+
modelId: "gpt-5.5",
316+
model: {
317+
...createCodexTestModel("openai"),
318+
id: "gpt-5.5",
319+
name: "gpt-5.5",
320+
api: "openai-responses",
321+
} as EmbeddedRunAttemptParams["model"],
322+
runtimePlan: {
323+
auth: {},
324+
observability: {
325+
resolvedRef: "openai/gpt-5.5",
326+
provider: "openai",
327+
modelId: "gpt-5.5",
328+
harnessId: "codex",
329+
},
330+
prompt: {
331+
resolveSystemPromptContribution: () => undefined,
332+
},
333+
tools: {
334+
normalize: (tools: unknown[]) => tools,
335+
logDiagnostics: () => undefined,
336+
},
337+
} as unknown as EmbeddedRunAttemptParams["runtimePlan"],
338+
});
339+
340+
await projector.handleNotification(
341+
turnCompleted([{ type: "agentMessage", id: "msg-1", text: "done" }]),
342+
);
343+
344+
const result = projector.buildResult(buildEmptyToolTelemetry());
345+
346+
expect(result.lastAssistant?.provider).toBe("openai-codex");
347+
expect(result.lastAssistant?.api).toBe("openai-codex-responses");
348+
expect(result.lastAssistant?.model).toBe("gpt-5.5");
349+
});
350+
351+
it("preserves OpenAI attribution for Codex app-server OpenAI API-key fallback profiles", async () => {
352+
const params = await createParams();
353+
const projector = await createProjector({
354+
...params,
355+
provider: "openai",
356+
authProfileId: "openai:work",
357+
modelId: "gpt-5.5",
358+
model: {
359+
...createCodexTestModel("openai"),
360+
id: "gpt-5.5",
361+
name: "gpt-5.5",
362+
api: "openai-responses",
363+
} as EmbeddedRunAttemptParams["model"],
364+
runtimePlan: {
365+
auth: {
366+
providerForAuth: "openai",
367+
authProfileProviderForAuth: "openai",
368+
harnessAuthProvider: "openai-codex",
369+
forwardedAuthProfileId: "openai:work",
370+
},
371+
observability: {
372+
resolvedRef: "openai/gpt-5.5",
373+
provider: "openai",
374+
modelId: "gpt-5.5",
375+
harnessId: "codex",
376+
},
377+
prompt: {
378+
resolveSystemPromptContribution: () => undefined,
379+
},
380+
tools: {
381+
normalize: (tools: unknown[]) => tools,
382+
logDiagnostics: () => undefined,
383+
},
384+
} as unknown as EmbeddedRunAttemptParams["runtimePlan"],
385+
});
386+
387+
await projector.handleNotification(
388+
turnCompleted([{ type: "agentMessage", id: "msg-1", text: "done" }]),
389+
);
390+
391+
const result = projector.buildResult(buildEmptyToolTelemetry());
392+
393+
expect(result.lastAssistant?.provider).toBe("openai");
394+
expect(result.lastAssistant?.api).toBe("openai-responses");
395+
expect(result.lastAssistant?.model).toBe("gpt-5.5");
396+
});
397+
310398
it("preserves inbound sender metadata on the mirrored user prompt", async () => {
311399
const params = await createParams();
312400
const projector = await createProjector({

extensions/codex/src/app-server/event-projector.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
type ToolProgressDetailMode,
2222
} from "openclaw/plugin-sdk/agent-harness-runtime";
2323
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
24+
import { resolveCodexLocalRuntimeAttribution } from "./local-runtime-attribution.js";
2425
import { CodexNativeSubagentTaskMirror } from "./native-subagent-task-mirror.js";
2526
import { readCodexTurn } from "./protocol-validators.js";
2627
import {
@@ -1187,6 +1188,7 @@ export class CodexAppServerEventProjector {
11871188
}
11881189

11891190
private createAssistantMessage(text: string): AssistantMessage {
1191+
const attribution = resolveCodexLocalRuntimeAttribution(this.params);
11901192
const usage: Usage = this.tokenUsage
11911193
? {
11921194
input: this.tokenUsage.input ?? 0,
@@ -1205,8 +1207,8 @@ export class CodexAppServerEventProjector {
12051207
return {
12061208
role: "assistant",
12071209
content: [{ type: "text", text }],
1208-
api: this.params.model.api ?? "openai-codex-responses",
1209-
provider: this.params.provider,
1210+
api: attribution.api ?? "openai-codex-responses",
1211+
provider: attribution.provider,
12101212
model: this.params.modelId,
12111213
usage,
12121214
stopReason: this.aborted ? "aborted" : this.promptError ? "error" : "stop",
@@ -1216,11 +1218,12 @@ export class CodexAppServerEventProjector {
12161218
}
12171219

12181220
private createAssistantMirrorMessage(title: string, text: string): AssistantMessage {
1221+
const attribution = resolveCodexLocalRuntimeAttribution(this.params);
12191222
return {
12201223
role: "assistant",
12211224
content: [{ type: "text", text: `${title}:\n${text}` }],
1222-
api: this.params.model.api ?? "openai-codex-responses",
1223-
provider: this.params.provider,
1225+
api: attribution.api ?? "openai-codex-responses",
1226+
provider: attribution.provider,
12241227
model: this.params.modelId,
12251228
usage: ZERO_USAGE,
12261229
stopReason: "stop",
@@ -1230,6 +1233,7 @@ export class CodexAppServerEventProjector {
12301233

12311234
private createToolCallMessage(params: ToolTranscriptCallInput): AgentMessage {
12321235
const args = normalizeToolTranscriptArguments(params.arguments);
1236+
const attribution = resolveCodexLocalRuntimeAttribution(this.params);
12331237
return {
12341238
role: "assistant",
12351239
content: [
@@ -1241,8 +1245,8 @@ export class CodexAppServerEventProjector {
12411245
input: args,
12421246
},
12431247
],
1244-
api: this.params.model.api ?? "openai-codex-responses",
1245-
provider: this.params.provider,
1248+
api: attribution.api ?? "openai-codex-responses",
1249+
provider: attribution.provider,
12461250
model: this.params.modelId,
12471251
usage: ZERO_USAGE,
12481252
stopReason: "toolUse",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
2+
3+
const OPENAI_PROVIDER_ID = "openai";
4+
const OPENAI_RESPONSES_API = "openai-responses";
5+
const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
6+
const OPENAI_CODEX_RESPONSES_API = "openai-codex-responses";
7+
8+
export type CodexLocalRuntimeAttribution = {
9+
provider: string;
10+
api?: string;
11+
};
12+
13+
function normalizeRuntimeId(value: string | undefined): string {
14+
return value?.trim().toLowerCase() ?? "";
15+
}
16+
17+
export function resolveCodexLocalRuntimeAttribution(
18+
params: EmbeddedRunAttemptParams,
19+
): CodexLocalRuntimeAttribution {
20+
const authProfileProvider = normalizeRuntimeId(
21+
params.runtimePlan?.auth?.authProfileProviderForAuth,
22+
);
23+
if (
24+
normalizeRuntimeId(params.runtimePlan?.observability.harnessId) === "codex" &&
25+
authProfileProvider !== OPENAI_PROVIDER_ID &&
26+
normalizeRuntimeId(params.model.provider) === OPENAI_PROVIDER_ID &&
27+
normalizeRuntimeId(params.model.api) === OPENAI_RESPONSES_API
28+
) {
29+
return {
30+
provider: OPENAI_CODEX_PROVIDER_ID,
31+
api: OPENAI_CODEX_RESPONSES_API,
32+
};
33+
}
34+
35+
return {
36+
provider: params.provider,
37+
api: params.model.api,
38+
};
39+
}

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,52 @@ describe("runCodexAppServerAttempt", () => {
869869
);
870870
});
871871

872+
it("keeps canonical OpenAI Codex runs on OpenAI dynamic tool policy", async () => {
873+
const sessionFile = path.join(tempDir, "session.jsonl");
874+
const workspaceDir = path.join(tempDir, "workspace");
875+
const params = createParams(sessionFile, workspaceDir);
876+
params.disableTools = false;
877+
params.provider = "openai";
878+
params.modelId = "gpt-5.5";
879+
params.model = {
880+
...createCodexTestModel("openai"),
881+
id: "gpt-5.5",
882+
name: "gpt-5.5",
883+
api: "openai-responses",
884+
} as EmbeddedRunAttemptParams["model"];
885+
params.runtimePlan = {
886+
...createCodexRuntimePlanFixture(),
887+
observability: {
888+
resolvedRef: "openai/gpt-5.5",
889+
provider: "openai",
890+
modelId: "gpt-5.5",
891+
harnessId: "codex",
892+
},
893+
};
894+
895+
const factoryOptions: unknown[] = [];
896+
__testing.setOpenClawCodingToolsFactoryForTests((options) => {
897+
factoryOptions.push(options);
898+
return [];
899+
});
900+
901+
await __testing.buildDynamicTools({
902+
params,
903+
resolvedWorkspace: workspaceDir,
904+
effectiveWorkspace: workspaceDir,
905+
sandboxSessionKey: params.sessionKey!,
906+
sandbox: null as never,
907+
runAbortController: new AbortController(),
908+
sessionAgentId: "main",
909+
pluginConfig: {},
910+
onYieldDetected: () => undefined,
911+
});
912+
913+
expect(factoryOptions).toHaveLength(1);
914+
expect((factoryOptions[0] as { modelProvider?: unknown }).modelProvider).toBe("openai");
915+
expect((factoryOptions[0] as { modelApi?: unknown }).modelApi).toBe("openai-responses");
916+
});
917+
872918
it("normalizes Codex dynamic toolsAllow entries before filtering", () => {
873919
const tools = ["exec", "apply_patch", "read", "message"].map((name) => ({ name }));
874920

extensions/codex/src/app-server/trajectory.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,43 @@ describe("Codex trajectory recorder", () => {
8282
expect(fs.existsSync(path.join(tmpDir, "session.trajectory-path.json"))).toBe(true);
8383
});
8484

85+
it("records canonical OpenAI Codex app-server turns with Codex local attribution", async () => {
86+
const tmpDir = makeTempDir();
87+
const sessionFile = path.join(tmpDir, "session.jsonl");
88+
const recorder = createCodexTrajectoryRecorder({
89+
cwd: tmpDir,
90+
attempt: {
91+
sessionFile,
92+
sessionId: "session-1",
93+
sessionKey: "agent:main:session-1",
94+
runId: "run-1",
95+
provider: "openai",
96+
modelId: "gpt-5.5",
97+
model: { provider: "openai", api: "openai-responses" },
98+
runtimePlan: {
99+
observability: {
100+
resolvedRef: "openai/gpt-5.5",
101+
provider: "openai",
102+
modelId: "gpt-5.5",
103+
harnessId: "codex",
104+
},
105+
},
106+
} as never,
107+
env: {},
108+
});
109+
110+
const trajectoryRecorder = expectTrajectoryRecorder(recorder);
111+
trajectoryRecorder.recordEvent("session.started");
112+
await trajectoryRecorder.flush();
113+
114+
const parsed = JSON.parse(
115+
fs.readFileSync(path.join(tmpDir, "session.trajectory.jsonl"), "utf8"),
116+
);
117+
expect(parsed.provider).toBe("openai-codex");
118+
expect(parsed.modelApi).toBe("openai-codex-responses");
119+
expect(parsed.modelId).toBe("gpt-5.5");
120+
});
121+
85122
it("sanitizes session ids when resolving an override directory", async () => {
86123
const tmpDir = makeTempDir();
87124
const recorder = createCodexTrajectoryRecorder({

extensions/codex/src/app-server/trajectory.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
appendRegularFile,
1111
resolveRegularFileAppendFlags,
1212
} from "openclaw/plugin-sdk/security-runtime";
13+
import { resolveCodexLocalRuntimeAttribution } from "./local-runtime-attribution.js";
1314

1415
type CodexTrajectoryRecorder = {
1516
filePath: string;
@@ -163,6 +164,7 @@ export function createCodexTrajectoryRecorder(
163164
});
164165
let queue = Promise.resolve();
165166
let seq = 0;
167+
const attribution = resolveCodexLocalRuntimeAttribution(params.attempt);
166168

167169
return {
168170
filePath,
@@ -180,9 +182,9 @@ export function createCodexTrajectoryRecorder(
180182
sessionKey: params.attempt.sessionKey,
181183
runId: params.attempt.runId,
182184
workspaceDir: params.cwd,
183-
provider: params.attempt.provider,
185+
provider: attribution.provider,
184186
modelId: params.attempt.modelId,
185-
modelApi: params.attempt.model.api,
187+
modelApi: attribution.api,
186188
data: data ? sanitizeValue(data) : undefined,
187189
};
188190
const line = boundedTrajectoryLine(event);

0 commit comments

Comments
 (0)