mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 06:22:54 +00:00
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>
206 lines
6.6 KiB
TypeScript
206 lines
6.6 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
createCodexTrajectoryRecorder,
|
|
resolveCodexTrajectoryAppendFlags,
|
|
resolveCodexTrajectoryPointerFlags,
|
|
} from "./trajectory.js";
|
|
|
|
type CodexTrajectoryRecorder = NonNullable<ReturnType<typeof createCodexTrajectoryRecorder>>;
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function makeTempDir(): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-trajectory-"));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function expectTrajectoryRecorder(
|
|
recorder: ReturnType<typeof createCodexTrajectoryRecorder>,
|
|
): CodexTrajectoryRecorder {
|
|
if (recorder === null) {
|
|
throw new Error("Expected Codex trajectory recorder");
|
|
}
|
|
expect(typeof recorder.recordEvent).toBe("function");
|
|
return recorder;
|
|
}
|
|
|
|
describe("Codex trajectory recorder", () => {
|
|
it("keeps write flags usable when O_NOFOLLOW is unavailable", () => {
|
|
const constants = {
|
|
O_APPEND: 0x01,
|
|
O_CREAT: 0x02,
|
|
O_TRUNC: 0x04,
|
|
O_WRONLY: 0x08,
|
|
};
|
|
|
|
expect(resolveCodexTrajectoryAppendFlags(constants)).toBe(0x0b);
|
|
expect(resolveCodexTrajectoryPointerFlags(constants)).toBe(0x0e);
|
|
});
|
|
|
|
it("records by default unless explicitly disabled", async () => {
|
|
const tmpDir = makeTempDir();
|
|
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
const recorder = createCodexTrajectoryRecorder({
|
|
cwd: tmpDir,
|
|
attempt: {
|
|
sessionFile,
|
|
sessionId: "session-1",
|
|
sessionKey: "agent:main:session-1",
|
|
runId: "run-1",
|
|
provider: "codex",
|
|
modelId: "gpt-5.4",
|
|
model: { api: "responses" },
|
|
} as never,
|
|
env: {},
|
|
});
|
|
|
|
const trajectoryRecorder = expectTrajectoryRecorder(recorder);
|
|
trajectoryRecorder.recordEvent("session.started", {
|
|
apiKey: "secret",
|
|
headers: [{ name: "Authorization", value: "Bearer sk-test-secret-token" }],
|
|
command: "curl -H 'Authorization: Bearer sk-other-secret-token'",
|
|
});
|
|
await trajectoryRecorder.flush();
|
|
|
|
const filePath = path.join(tmpDir, "session.trajectory.jsonl");
|
|
const content = fs.readFileSync(filePath, "utf8");
|
|
expect(content).toContain('"type":"session.started"');
|
|
expect(content).not.toContain("secret");
|
|
expect(content).not.toContain("sk-test-secret-token");
|
|
expect(content).not.toContain("sk-other-secret-token");
|
|
expect(fs.statSync(filePath).mode & 0o777).toBe(0o600);
|
|
expect(fs.existsSync(path.join(tmpDir, "session.trajectory-path.json"))).toBe(true);
|
|
});
|
|
|
|
it("records canonical OpenAI Codex app-server turns with Codex local attribution", async () => {
|
|
const tmpDir = makeTempDir();
|
|
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
const recorder = createCodexTrajectoryRecorder({
|
|
cwd: tmpDir,
|
|
attempt: {
|
|
sessionFile,
|
|
sessionId: "session-1",
|
|
sessionKey: "agent:main:session-1",
|
|
runId: "run-1",
|
|
provider: "openai",
|
|
modelId: "gpt-5.5",
|
|
model: { provider: "openai", api: "openai-responses" },
|
|
runtimePlan: {
|
|
observability: {
|
|
resolvedRef: "openai/gpt-5.5",
|
|
provider: "openai",
|
|
modelId: "gpt-5.5",
|
|
harnessId: "codex",
|
|
},
|
|
},
|
|
} as never,
|
|
env: {},
|
|
});
|
|
|
|
const trajectoryRecorder = expectTrajectoryRecorder(recorder);
|
|
trajectoryRecorder.recordEvent("session.started");
|
|
await trajectoryRecorder.flush();
|
|
|
|
const parsed = JSON.parse(
|
|
fs.readFileSync(path.join(tmpDir, "session.trajectory.jsonl"), "utf8"),
|
|
);
|
|
expect(parsed.provider).toBe("openai-codex");
|
|
expect(parsed.modelApi).toBe("openai-codex-responses");
|
|
expect(parsed.modelId).toBe("gpt-5.5");
|
|
});
|
|
|
|
it("sanitizes session ids when resolving an override directory", async () => {
|
|
const tmpDir = makeTempDir();
|
|
const recorder = createCodexTrajectoryRecorder({
|
|
cwd: tmpDir,
|
|
attempt: {
|
|
sessionFile: path.join(tmpDir, "session.jsonl"),
|
|
sessionId: "../evil/session",
|
|
model: { api: "responses" },
|
|
} as never,
|
|
env: { OPENCLAW_TRAJECTORY_DIR: tmpDir },
|
|
});
|
|
|
|
const trajectoryRecorder = expectTrajectoryRecorder(recorder);
|
|
trajectoryRecorder.recordEvent("session.started");
|
|
await trajectoryRecorder.flush();
|
|
|
|
expect(fs.existsSync(path.join(tmpDir, "___evil_session.jsonl"))).toBe(true);
|
|
});
|
|
|
|
it("honors explicit disablement", () => {
|
|
const tmpDir = makeTempDir();
|
|
const recorder = createCodexTrajectoryRecorder({
|
|
cwd: tmpDir,
|
|
attempt: {
|
|
sessionFile: path.join(tmpDir, "session.jsonl"),
|
|
sessionId: "session-1",
|
|
model: { api: "responses" },
|
|
} as never,
|
|
env: { OPENCLAW_TRAJECTORY: "0" },
|
|
});
|
|
|
|
expect(recorder).toBeNull();
|
|
});
|
|
|
|
it("refuses to append through a symlinked parent directory", async () => {
|
|
const tmpDir = makeTempDir();
|
|
const targetDir = path.join(tmpDir, "target");
|
|
const linkDir = path.join(tmpDir, "link");
|
|
fs.mkdirSync(targetDir);
|
|
fs.symlinkSync(targetDir, linkDir);
|
|
const recorder = createCodexTrajectoryRecorder({
|
|
cwd: tmpDir,
|
|
attempt: {
|
|
sessionFile: path.join(linkDir, "session.jsonl"),
|
|
sessionId: "session-1",
|
|
model: { api: "responses" },
|
|
} as never,
|
|
env: {},
|
|
});
|
|
|
|
const trajectoryRecorder = expectTrajectoryRecorder(recorder);
|
|
trajectoryRecorder.recordEvent("session.started");
|
|
await trajectoryRecorder.flush();
|
|
|
|
expect(fs.existsSync(path.join(targetDir, "session.trajectory.jsonl"))).toBe(false);
|
|
});
|
|
|
|
it("truncates events that exceed the runtime event byte limit", async () => {
|
|
const tmpDir = makeTempDir();
|
|
const recorder = createCodexTrajectoryRecorder({
|
|
cwd: tmpDir,
|
|
attempt: {
|
|
sessionFile: path.join(tmpDir, "session.jsonl"),
|
|
sessionId: "session-1",
|
|
model: { api: "responses" },
|
|
} as never,
|
|
env: {},
|
|
});
|
|
|
|
const trajectoryRecorder = expectTrajectoryRecorder(recorder);
|
|
trajectoryRecorder.recordEvent("context.compiled", {
|
|
fields: Object.fromEntries(
|
|
Array.from({ length: 100 }, (_, index) => [`field-${index}`, "x".repeat(3_000)]),
|
|
),
|
|
});
|
|
await trajectoryRecorder.flush();
|
|
|
|
const parsed = JSON.parse(
|
|
fs.readFileSync(path.join(tmpDir, "session.trajectory.jsonl"), "utf8"),
|
|
) as { data?: { truncated?: boolean; reason?: string } };
|
|
expect(parsed.data?.truncated).toBe(true);
|
|
expect(parsed.data?.reason).toBe("trajectory-event-size-limit");
|
|
});
|
|
});
|