mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:01:01 +00:00
Harden Codex harness control surfaces (#77459)
* fix(scripts): find codex protocol source from worktrees * fix(test): keep codex harness docker caches writable * fix(test): relax live codex cache mount permissions * test(codex): add live docker harness debug output * fix(test): detect numeric ci env in codex docker harness * fix(codex): skip duplicate agent-command telemetry * fix(tooling): skip sparse-missing oxlint tsconfig * fix(tooling): route changed checks through testbox * fix(qa): keep coverage json source-clean * fix(test): preflight codex docker auth * fix(codex): validate bind option values * fix(codex): parse quoted command arguments * fix(codex): reject extra control args * fix(codex): use content for blank bound prompts * fix(codex): decode local image file urls * fix(codex): treat local media urls as images * fix(codex): keep windows media paths local * fix(codex): reject malformed diagnostics confirmations * fix(codex): reject malformed resume commands * fix(codex): reject malformed thread actions * fix(codex): reject malformed turn controls * fix(codex): reject malformed model controls * fix(codex): resolve empty user input prompts * fix(codex): enforce user input options * fix(codex): reject ambiguous computer-use actions * fix(codex): ignore stale bound turn notifications * test(gateway): close task registries in gateway harness * test(gateway): route cleanup through task seams * fix(codex): describe current permission approvals * fix(codex): disclose command approval amendments * fix(codex): preserve approval detail under truncation * fix(codex): propagate dynamic tool failures * test(codex): align dynamic tool block contract * fix(codex): reject extra read-only command operands * fix(codex): escape command readout fields * fix(codex): escape status probe errors * fix(codex): narrow formatted thread details * fix(codex): escape successful status summaries * fix(codex): escape bound control replies * fix(codex): escape user input prompts * fix(codex): escape control failure replies * fix(codex): escape approval prompt text * test(codex): narrow escaped reply assertions * test(codex): complete strict reply fixtures * test(codex): preserve account fixture literals * test(codex): align status probe fixtures * fix(codex): satisfy sanitizer regex lint * fix(codex): harden command readouts * fix(codex): harden bound image inputs * fix(codex): sanitize command failure replies * test(codex): complete rate limit fixture * test(tooling): isolate postinstall compile cache fixture * fix(codex): keep app-server event ownership explicit --------- Co-authored-by: pashpashpash <nik@vault77.ai>
This commit is contained in:
@@ -8,8 +8,10 @@ import {
|
||||
isPackageScriptOnlyChange,
|
||||
} from "../../scripts/changed-lanes.mjs";
|
||||
import {
|
||||
buildChangedCheckTestboxArgs,
|
||||
createChangedCheckChildEnv,
|
||||
createChangedCheckPlan,
|
||||
shouldDelegateChangedCheckToTestbox,
|
||||
} from "../../scripts/check-changed.mjs";
|
||||
import { cleanupTempDirs, makeTempRepoRoot } from "../helpers/temp-repo.js";
|
||||
|
||||
@@ -215,6 +217,44 @@ describe("scripts/changed-lanes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("delegates local Testbox-mode changed gates before running locally", () => {
|
||||
expect(
|
||||
shouldDelegateChangedCheckToTestbox(["--base", "origin/main"], {
|
||||
OPENCLAW_TESTBOX: "1",
|
||||
PATH: "/usr/bin",
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(buildChangedCheckTestboxArgs(["--base", "origin/main", "--head", "HEAD"])).toEqual([
|
||||
"testbox:run",
|
||||
"--",
|
||||
"OPENCLAW_TESTBOX=1",
|
||||
"OPENCLAW_TESTBOX_REMOTE_RUN=1",
|
||||
"pnpm",
|
||||
"check:changed",
|
||||
"--base",
|
||||
"origin/main",
|
||||
"--head",
|
||||
"HEAD",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not delegate dry-run, CI, or already-remote changed gates", () => {
|
||||
expect(shouldDelegateChangedCheckToTestbox(["--dry-run"], { OPENCLAW_TESTBOX: "1" })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
shouldDelegateChangedCheckToTestbox([], { OPENCLAW_TESTBOX: "1", GITHUB_ACTIONS: "true" }),
|
||||
).toBe(false);
|
||||
expect(shouldDelegateChangedCheckToTestbox([], { OPENCLAW_TESTBOX: "1", CI: "1" })).toBe(false);
|
||||
expect(
|
||||
shouldDelegateChangedCheckToTestbox([], {
|
||||
OPENCLAW_TESTBOX: "1",
|
||||
OPENCLAW_TESTBOX_REMOTE_RUN: "1",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("runs changed-check lint lanes under the parent heavy-check lock", () => {
|
||||
const result = detectChangedLanes(["extensions/discord/src/index.ts"]);
|
||||
const plan = createChangedCheckPlan(result, { env: { PATH: "/usr/bin" } });
|
||||
|
||||
58
test/scripts/codex-app-server-protocol-source.test.ts
Normal file
58
test/scripts/codex-app-server-protocol-source.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { resolveCodexAppServerProtocolSource } from "../../scripts/lib/codex-app-server-protocol-source.js";
|
||||
import { createScriptTestHarness } from "./test-helpers.js";
|
||||
|
||||
const { createTempDir } = createScriptTestHarness();
|
||||
const originalOpenClawCodexRepo = process.env.OPENCLAW_CODEX_REPO;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalOpenClawCodexRepo === undefined) {
|
||||
delete process.env.OPENCLAW_CODEX_REPO;
|
||||
} else {
|
||||
process.env.OPENCLAW_CODEX_REPO = originalOpenClawCodexRepo;
|
||||
}
|
||||
});
|
||||
|
||||
describe("codex app-server protocol source resolver", () => {
|
||||
it("uses OPENCLAW_CODEX_REPO when provided", async () => {
|
||||
const root = createTempDir("openclaw-protocol-source-root-");
|
||||
const codexRepo = createTempDir("openclaw-protocol-source-codex-");
|
||||
createProtocolSchema(codexRepo);
|
||||
process.env.OPENCLAW_CODEX_REPO = codexRepo;
|
||||
|
||||
await expect(resolveCodexAppServerProtocolSource(root)).resolves.toEqual({
|
||||
codexRepo,
|
||||
sourceRoot: path.join(codexRepo, "codex-rs/app-server-protocol/schema"),
|
||||
});
|
||||
});
|
||||
|
||||
it("finds the primary checkout sibling from a git worktree", async () => {
|
||||
const parentDir = createTempDir("openclaw-protocol-source-parent-");
|
||||
const primaryOpenClaw = path.join(parentDir, "openclaw");
|
||||
const codexRepo = path.join(parentDir, "codex");
|
||||
const worktreeRoot = createTempDir("openclaw-protocol-source-worktree-");
|
||||
fs.mkdirSync(path.join(primaryOpenClaw, ".git", "worktrees", "codex-harness"), {
|
||||
recursive: true,
|
||||
});
|
||||
fs.mkdirSync(worktreeRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(worktreeRoot, ".git"),
|
||||
`gitdir: ${path.join(primaryOpenClaw, ".git", "worktrees", "codex-harness")}\n`,
|
||||
);
|
||||
createProtocolSchema(codexRepo);
|
||||
delete process.env.OPENCLAW_CODEX_REPO;
|
||||
|
||||
await expect(resolveCodexAppServerProtocolSource(worktreeRoot)).resolves.toMatchObject({
|
||||
codexRepo,
|
||||
sourceRoot: path.join(codexRepo, "codex-rs/app-server-protocol/schema"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createProtocolSchema(codexRepo: string): void {
|
||||
fs.mkdirSync(path.join(codexRepo, "codex-rs/app-server-protocol/schema/typescript"), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileSync as readFileSyncOriginal } from "node:fs";
|
||||
import { existsSync as existsSyncOriginal, readFileSync as readFileSyncOriginal } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -51,6 +51,13 @@ async function writePluginPackage(
|
||||
}
|
||||
|
||||
describe("bundled plugin postinstall", () => {
|
||||
function existsSyncWithoutGlobalCompileCache(value: string) {
|
||||
if (path.resolve(value) === path.join(tmpdir(), "node-compile-cache")) {
|
||||
return false;
|
||||
}
|
||||
return existsSyncOriginal(value);
|
||||
}
|
||||
|
||||
it("recognizes direct invocation through symlinked temp prefixes", () => {
|
||||
const realpathSync = vi.fn((value: string) =>
|
||||
value.replace(/^\/var\/folders\//u, "/private/var/folders/"),
|
||||
@@ -448,6 +455,7 @@ describe("bundled plugin postinstall", () => {
|
||||
STATE_DIRECTORY: systemState,
|
||||
},
|
||||
packageRoot,
|
||||
existsSync: existsSyncWithoutGlobalCompileCache,
|
||||
log,
|
||||
});
|
||||
|
||||
|
||||
@@ -49,6 +49,27 @@ describe("run-oxlint", () => {
|
||||
hadExplicitTargets: true,
|
||||
remainingExplicitTargets: 1,
|
||||
skippedTargets: ["ui", "packages"],
|
||||
skippedConfigs: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("filters tracked tsconfig files missing from sparse checkouts", () => {
|
||||
const result = filterSparseMissingOxlintTargets(
|
||||
["--tsconfig", "config/tsconfig/oxlint.core.json", "src"],
|
||||
{
|
||||
fileExists: (target: string) => target.endsWith("/src"),
|
||||
isSparseCheckoutEnabled: () => true,
|
||||
isTrackedPath: ({ target }: { target: string }) =>
|
||||
target === "config/tsconfig/oxlint.core.json",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
args: ["src"],
|
||||
hadExplicitTargets: true,
|
||||
remainingExplicitTargets: 1,
|
||||
skippedTargets: [],
|
||||
skippedConfigs: ["config/tsconfig/oxlint.core.json"],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +84,7 @@ describe("run-oxlint", () => {
|
||||
args: ["src", "typo"],
|
||||
remainingExplicitTargets: 2,
|
||||
skippedTargets: [],
|
||||
skippedConfigs: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
40
test/scripts/test-live-codex-harness-docker.test.ts
Normal file
40
test/scripts/test-live-codex-harness-docker.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const SCRIPT_PATH = path.resolve(
|
||||
import.meta.dirname,
|
||||
"../../scripts/test-live-codex-harness-docker.sh",
|
||||
);
|
||||
|
||||
describe("scripts/test-live-codex-harness-docker.sh", () => {
|
||||
it("mounts cache and npm tool dirs outside the bind-mounted Docker home", () => {
|
||||
const script = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
expect(script).toContain('DOCKER_CACHE_CONTAINER_DIR="/tmp/openclaw-cache"');
|
||||
expect(script).toContain('DOCKER_CLI_TOOLS_CONTAINER_DIR="/tmp/openclaw-npm-global"');
|
||||
expect(script).toContain("openclaw_live_codex_harness_is_ci()");
|
||||
expect(script).toContain('[[ -n "${CI:-}" && "${CI:-}" != "false" ]]');
|
||||
expect(script).toContain('-e XDG_CACHE_HOME="$DOCKER_CACHE_CONTAINER_DIR"');
|
||||
expect(script).toContain('-e NPM_CONFIG_PREFIX="$DOCKER_CLI_TOOLS_CONTAINER_DIR"');
|
||||
expect(script).toContain('chmod 0777 "$CLI_TOOLS_DIR" "$CACHE_HOME_DIR" || true');
|
||||
expect(script).toContain('-v "$CACHE_HOME_DIR":"$DOCKER_CACHE_CONTAINER_DIR"');
|
||||
expect(script).toContain('-v "$CLI_TOOLS_DIR":"$DOCKER_CLI_TOOLS_CONTAINER_DIR"');
|
||||
expect(script).not.toContain('-v "$CACHE_HOME_DIR":/home/node/.cache');
|
||||
expect(script).not.toContain('-v "$CLI_TOOLS_DIR":/home/node/.npm-global');
|
||||
});
|
||||
|
||||
it("fails before Docker build when codex-auth has no host auth file", () => {
|
||||
const script = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
expect(script).toContain(
|
||||
"OPENCLAW_LIVE_CODEX_HARNESS_AUTH=codex-auth requires ~/.codex/auth.json before building the live Docker image",
|
||||
);
|
||||
expect(script).toContain(
|
||||
"If this is a Testbox/API-key run, set OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key and run through openclaw-testbox-env.",
|
||||
);
|
||||
expect(script.indexOf("requires ~/.codex/auth.json before building")).toBeLessThan(
|
||||
script.indexOf('OPENCLAW_LIVE_DOCKER_REPO_ROOT="$ROOT_DIR"'),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user