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:
Vincent Koc
2026-05-04 15:23:41 -07:00
committed by GitHub
parent b3e42bf327
commit ac3cd1a0ca
42 changed files with 2672 additions and 245 deletions

View File

@@ -33,6 +33,52 @@ export function createChangedCheckChildEnv(baseEnv = process.env) {
};
}
function isTruthyEnvFlag(value) {
const normalized = String(value ?? "")
.trim()
.toLowerCase();
return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
}
export function shouldDelegateChangedCheckToTestbox(argv = [], env = process.env) {
if (!isTruthyEnvFlag(env.OPENCLAW_TESTBOX)) {
return false;
}
if (isTruthyEnvFlag(env.OPENCLAW_TESTBOX_REMOTE_RUN)) {
return false;
}
if (isTruthyEnvFlag(env.CI) || isTruthyEnvFlag(env.GITHUB_ACTIONS)) {
return false;
}
if (argv.includes("--dry-run")) {
return false;
}
return true;
}
export function buildChangedCheckTestboxArgs(argv = []) {
return [
"testbox:run",
"--",
"OPENCLAW_TESTBOX=1",
"OPENCLAW_TESTBOX_REMOTE_RUN=1",
"pnpm",
"check:changed",
...argv,
];
}
export async function runChangedCheckViaTestbox(argv = [], env = process.env) {
console.error(
"[check:changed] OPENCLAW_TESTBOX=1 set; delegating to Blacksmith Testbox via `pnpm testbox:run`.",
);
return await runManagedCommand({
bin: "pnpm",
args: buildChangedCheckTestboxArgs(argv),
env,
});
}
export function createChangedCheckPlan(result, options = {}) {
const commands = [];
const baseEnv = createChangedCheckChildEnv(options.env ?? process.env);
@@ -283,21 +329,26 @@ function isDirectRun() {
}
if (isDirectRun()) {
const args = parseArgs(process.argv.slice(2));
const paths =
args.paths.length > 0
? args.paths
: args.staged
? listStagedChangedPaths()
: listChangedPathsFromGit({ base: args.base, head: args.head });
const result = detectChangedLanesForPaths({
paths,
base: args.base,
head: args.head,
staged: args.staged,
});
process.exitCode = await runChangedCheck(result, {
...args,
explicitPaths: args.paths.length > 0,
});
const argv = process.argv.slice(2);
if (shouldDelegateChangedCheckToTestbox(argv, process.env)) {
process.exitCode = await runChangedCheckViaTestbox(argv, process.env);
} else {
const args = parseArgs(argv);
const paths =
args.paths.length > 0
? args.paths
: args.staged
? listStagedChangedPaths()
: listChangedPathsFromGit({ base: args.base, head: args.head });
const result = detectChangedLanesForPaths({
paths,
base: args.base,
head: args.head,
staged: args.staged,
});
process.exitCode = await runChangedCheck(result, {
...args,
explicitPaths: args.paths.length > 0,
});
}
}

View File

@@ -1,11 +1,9 @@
import fs from "node:fs/promises";
import path from "node:path";
import { resolveCodexAppServerProtocolSource } from "./lib/codex-app-server-protocol-source.js";
const codexRepo = process.env.OPENCLAW_CODEX_REPO
? path.resolve(process.env.OPENCLAW_CODEX_REPO)
: path.resolve(process.cwd(), "../codex");
const schemaRoot = path.join(codexRepo, "codex-rs/app-server-protocol/schema/typescript");
const sourceSchemaRoot = path.join(codexRepo, "codex-rs/app-server-protocol/schema");
const { sourceRoot: sourceSchemaRoot } = await resolveCodexAppServerProtocolSource(process.cwd());
const schemaRoot = path.join(sourceSchemaRoot, "typescript");
const generatedRoot = path.resolve(
process.cwd(),
"extensions/codex/src/app-server/protocol-generated",
@@ -104,12 +102,14 @@ if (failures.length > 0) {
for (const failure of failures) {
console.error(`- ${failure}`);
}
console.error("Run `pnpm codex-app-server:protocol:sync` after refreshing ../codex.");
console.error(
`Run \`pnpm codex-app-server:protocol:sync\` after refreshing the Codex checkout at ${path.resolve(sourceSchemaRoot, "../../..")}.`,
);
process.exit(1);
}
console.log(
`Codex app-server generated protocol matches OpenClaw bridge assumptions: ${schemaRoot}`,
`Codex app-server generated protocol matches OpenClaw bridge assumptions: ${sourceSchemaRoot}`,
);
async function compareGeneratedProtocolMirror(): Promise<void> {
@@ -130,14 +130,12 @@ async function compareGeneratedProtocolMirror(): Promise<void> {
);
const target = await fs.readFile(path.join(targetTsRoot, file), "utf8");
if (source !== target) {
failures.push(
`protocol-generated/typescript/${file}: differs from normalized ../codex schema`,
);
failures.push(`protocol-generated/typescript/${file}: differs from normalized source schema`);
}
}
for (const file of targetFiles) {
if (!sourceSet.has(file)) {
failures.push(`protocol-generated/typescript/${file}: no longer present in ../codex schema`);
failures.push(`protocol-generated/typescript/${file}: no longer present in source schema`);
}
}
@@ -161,7 +159,7 @@ async function compareGeneratedProtocolMirror(): Promise<void> {
continue;
}
if (source !== target) {
failures.push(`protocol-generated/json/${schema}: differs from ../codex schema`);
failures.push(`protocol-generated/json/${schema}: differs from source schema`);
}
}
}

View File

@@ -0,0 +1,74 @@
import fs from "node:fs/promises";
import path from "node:path";
const PROTOCOL_SCHEMA_RELATIVE_PATH = "codex-rs/app-server-protocol/schema";
export async function resolveCodexAppServerProtocolSource(repoRoot: string): Promise<{
codexRepo: string;
sourceRoot: string;
}> {
const candidates = await collectCodexRepoCandidates(repoRoot);
const checked: string[] = [];
for (const candidate of candidates) {
const codexRepo = path.resolve(candidate);
if (checked.includes(codexRepo)) {
continue;
}
checked.push(codexRepo);
const sourceRoot = path.join(codexRepo, PROTOCOL_SCHEMA_RELATIVE_PATH);
if (await isDirectory(path.join(sourceRoot, "typescript"))) {
return { codexRepo, sourceRoot };
}
}
throw new Error(
[
"Codex app-server protocol schema not found.",
"Set OPENCLAW_CODEX_REPO to a checkout of openai/codex, or keep a sibling `codex` checkout next to the primary OpenClaw checkout.",
`Checked: ${checked.join(", ") || "<none>"}`,
].join("\n"),
);
}
async function collectCodexRepoCandidates(repoRoot: string): Promise<string[]> {
const candidates = [
process.env.OPENCLAW_CODEX_REPO,
path.resolve(repoRoot, "../codex"),
await resolvePrimaryWorktreeSiblingCodex(repoRoot),
];
return candidates.filter((candidate): candidate is string => Boolean(candidate));
}
async function resolvePrimaryWorktreeSiblingCodex(repoRoot: string): Promise<string | undefined> {
const gitFilePath = path.join(repoRoot, ".git");
let gitFile: string;
try {
gitFile = await fs.readFile(gitFilePath, "utf8");
} catch {
return undefined;
}
const match = /^gitdir:\s*(.+)$/m.exec(gitFile);
if (!match) {
return undefined;
}
const gitDir = path.resolve(repoRoot, match[1].trim());
const worktreeMarker = `${path.sep}.git${path.sep}worktrees${path.sep}`;
const markerIndex = gitDir.indexOf(worktreeMarker);
if (markerIndex < 0) {
return undefined;
}
const primaryWorktreeRoot = gitDir.slice(0, markerIndex);
return path.join(path.dirname(primaryWorktreeRoot), "codex");
}
async function isDirectory(candidate: string): Promise<boolean> {
try {
return (await fs.stat(candidate)).isDirectory();
} catch {
return false;
}
}

View File

@@ -0,0 +1,56 @@
import { runQaCoverageReportCommand } from "../extensions/qa-lab/src/cli.runtime.ts";
type Options = {
json?: boolean;
output?: string;
repoRoot?: string;
};
function takeValue(args: string[], index: number, flag: string): string {
const value = args[index + 1];
if (!value || value.startsWith("-")) {
throw new Error(`${flag} requires a value.`);
}
return value;
}
function parseArgs(args: string[]): Options {
const opts: Options = {};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
switch (arg) {
case "--help":
case "-h":
process.stdout.write(`Usage: openclaw qa coverage [options]
Options:
--json Print machine-readable JSON
--output <path> Write the report to a file
--repo-root <path> Repository root to target
-h, --help Display help
`);
process.exit(0);
case "--json":
opts.json = true;
break;
case "--output":
opts.output = takeValue(args, index, arg);
index += 1;
break;
case "--repo-root":
opts.repoRoot = takeValue(args, index, arg);
index += 1;
break;
default:
throw new Error(`Unknown qa coverage option: ${arg}`);
}
}
return opts;
}
const opts = parseArgs(process.argv.slice(2));
await runQaCoverageReportCommand({
...(opts.json ? { json: true } : {}),
...(opts.output ? { output: opts.output } : {}),
...(opts.repoRoot ? { repoRoot: opts.repoRoot } : {}),
});

View File

@@ -796,6 +796,7 @@ const shouldUseExistingDistForGatewayClient = (deps, buildRequirement) =>
statMtime(deps.distEntry, deps.fs) != null;
const isQaParityReportCommand = (args) => args[0] === "qa" && args[1] === "parity-report";
const isQaCoverageReportCommand = (args) => args[0] === "qa" && args[1] === "coverage";
const shouldRunQaParityReportFromSource = (deps, buildRequirement) =>
buildRequirement.reason === "missing_private_qa_dist" &&
@@ -803,6 +804,12 @@ const shouldRunQaParityReportFromSource = (deps, buildRequirement) =>
deps.env.OPENCLAW_FORCE_BUILD !== "1" &&
statMtime(path.join(deps.cwd, "extensions", "qa-lab", "src", "cli.runtime.ts"), deps.fs) != null;
const shouldRunQaCoverageReportFromSource = (deps, buildRequirement) =>
buildRequirement.reason === "missing_private_qa_dist" &&
isQaCoverageReportCommand(deps.args) &&
deps.env.OPENCLAW_FORCE_BUILD !== "1" &&
statMtime(path.join(deps.cwd, "extensions", "qa-lab", "src", "cli.runtime.ts"), deps.fs) != null;
const runQaParityReportFromSource = async (deps) => {
const sourceEntrypoint = path.join(deps.cwd, "scripts", "qa-parity-report.ts");
const nodeProcess = deps.spawn(
@@ -823,6 +830,26 @@ const runQaParityReportFromSource = async (deps) => {
return res.exitCode ?? 1;
};
const runQaCoverageReportFromSource = async (deps) => {
const sourceEntrypoint = path.join(deps.cwd, "scripts", "qa-coverage-report.ts");
const nodeProcess = deps.spawn(
deps.execPath,
["--import", "tsx", sourceEntrypoint, ...deps.args.slice(2)],
{
cwd: deps.cwd,
env: deps.env,
stdio: deps.outputTee ? ["inherit", "pipe", "pipe"] : "inherit",
},
);
pipeSpawnedOutput(nodeProcess, deps);
const res = await waitForSpawnedProcess(nodeProcess, deps);
const interruptedExitCode = getInterruptedSpawnExitCode(res);
if (interruptedExitCode !== null) {
return interruptedExitCode;
}
return res.exitCode ?? 1;
};
export async function runNodeMain(params = {}) {
const deps = {
spawn: params.spawn ?? spawn,
@@ -862,6 +889,7 @@ export async function runNodeMain(params = {}) {
buildRequirement,
);
const useQaParityReportSource = shouldRunQaParityReportFromSource(deps, buildRequirement);
const useQaCoverageReportSource = shouldRunQaCoverageReportFromSource(deps, buildRequirement);
if (useExistingGatewayClientDist) {
buildRequirement = { shouldBuild: false, reason: "gateway_client_existing_dist" };
}
@@ -870,6 +898,11 @@ export async function runNodeMain(params = {}) {
exitCode = await runQaParityReportFromSource(deps);
return await closeRunNodeOutputTee(deps, exitCode);
}
if (useQaCoverageReportSource) {
logRunner("Running QA coverage report from source without rebuilding private QA dist.", deps);
exitCode = await runQaCoverageReportFromSource(deps);
return await closeRunNodeOutputTee(deps, exitCode);
}
if (!buildRequirement.shouldBuild) {
if (!useExistingGatewayClientDist) {
const runtimePostBuildRequirement = resolveRuntimePostBuildRequirement(deps);

View File

@@ -52,16 +52,24 @@ export function filterSparseMissingOxlintTargets(
} = {},
) {
if (!isSparseCheckoutEnabled({ cwd })) {
return { args, hadExplicitTargets: false, remainingExplicitTargets: 0, skippedTargets: [] };
return {
args,
hadExplicitTargets: false,
remainingExplicitTargets: 0,
skippedTargets: [],
skippedConfigs: [],
};
}
const filteredArgs = [];
const skippedTargets = [];
const skippedConfigs = [];
let hadExplicitTargets = false;
let remainingExplicitTargets = 0;
let consumeNextValue = false;
for (const arg of args) {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (consumeNextValue) {
filteredArgs.push(arg);
consumeNextValue = false;
@@ -74,6 +82,29 @@ export function filterSparseMissingOxlintTargets(
}
if (arg.startsWith("--")) {
if (arg === "--tsconfig") {
const value = args[index + 1];
if (value !== undefined) {
index += 1;
if (!fileExists(path.resolve(cwd, value)) && isTrackedPath({ cwd, target: value })) {
skippedConfigs.push(value);
continue;
}
filteredArgs.push(arg, value);
continue;
}
}
if (arg.startsWith("--tsconfig=")) {
const value = arg.slice("--tsconfig=".length);
if (
value &&
!fileExists(path.resolve(cwd, value)) &&
isTrackedPath({ cwd, target: value })
) {
skippedConfigs.push(value);
continue;
}
}
filteredArgs.push(arg);
if (!arg.includes("=") && OXLINT_VALUE_FLAGS.has(arg)) {
consumeNextValue = true;
@@ -97,7 +128,13 @@ export function filterSparseMissingOxlintTargets(
filteredArgs.push(arg);
}
return { args: filteredArgs, hadExplicitTargets, remainingExplicitTargets, skippedTargets };
return {
args: filteredArgs,
hadExplicitTargets,
remainingExplicitTargets,
skippedTargets,
skippedConfigs,
};
}
function getSparseCheckoutEnabled({ cwd }) {
@@ -159,6 +196,12 @@ export async function main(argv = process.argv.slice(2), runtimeEnv = process.en
`[oxlint] sparse checkout is missing tracked target(s); skipping ${sparseTargets.skippedTargets.join(", ")}`,
);
}
if (sparseTargets.skippedConfigs.length > 0) {
console.error(
`[oxlint] sparse checkout is missing tracked config(s); skipping oxlint: ${sparseTargets.skippedConfigs.join(", ")}`,
);
return;
}
if (sparseTargets.hadExplicitTargets && sparseTargets.remainingExplicitTargets === 0) {
console.error("[oxlint] no present sparse-checkout targets remain; skipping oxlint.");
return;

View File

@@ -1,11 +1,8 @@
import fs from "node:fs/promises";
import path from "node:path";
import { resolveCodexAppServerProtocolSource } from "./lib/codex-app-server-protocol-source.js";
const codexRepo = process.env.OPENCLAW_CODEX_REPO
? path.resolve(process.env.OPENCLAW_CODEX_REPO)
: path.resolve(process.cwd(), "../codex");
const sourceRoot = path.join(codexRepo, "codex-rs/app-server-protocol/schema");
const { sourceRoot } = await resolveCodexAppServerProtocolSource(process.cwd());
const targetRoot = path.resolve(
process.cwd(),
"extensions/codex/src/app-server/protocol-generated",

View File

@@ -22,9 +22,15 @@ DOCKER_USER="${OPENCLAW_DOCKER_USER:-node}"
DOCKER_HOME_MOUNT=()
DOCKER_TRUSTED_HARNESS_MOUNT=()
DOCKER_TRUSTED_HARNESS_CONTAINER_DIR=""
DOCKER_CACHE_CONTAINER_DIR="/tmp/openclaw-cache"
DOCKER_CLI_TOOLS_CONTAINER_DIR="/tmp/openclaw-npm-global"
DOCKER_EXTRA_ENV_FILES=()
DOCKER_AUTH_PRESTAGED=0
openclaw_live_codex_harness_is_ci() {
[[ -n "${CI:-}" && "${CI:-}" != "false" ]] || [[ -n "${GITHUB_ACTIONS:-}" && "${GITHUB_ACTIONS:-}" != "false" ]]
}
openclaw_live_codex_harness_append_build_extension() {
local extension="${1:?extension required}"
local current="${OPENCLAW_DOCKER_BUILD_EXTENSIONS:-${OPENCLAW_EXTENSIONS:-}}"
@@ -50,6 +56,13 @@ if [[ "$CODEX_HARNESS_AUTH_MODE" == "api-key" && -z "${OPENAI_API_KEY:-}" ]]; th
echo "ERROR: OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key requires OPENAI_API_KEY." >&2
exit 1
fi
if [[ "$CODEX_HARNESS_AUTH_MODE" != "api-key" && ! -s "$HOME/.codex/auth.json" ]]; then
echo "ERROR: OPENCLAW_LIVE_CODEX_HARNESS_AUTH=codex-auth requires ~/.codex/auth.json before building the live Docker image." >&2
if [[ -n "${OPENAI_API_KEY:-}" ]]; then
echo "If this is a Testbox/API-key run, set OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key and run through openclaw-testbox-env." >&2
fi
exit 1
fi
cleanup_temp_dirs() {
if ((${#TEMP_DIRS[@]} > 0)); then
@@ -60,7 +73,7 @@ trap cleanup_temp_dirs EXIT
if [[ -n "${OPENCLAW_DOCKER_CLI_TOOLS_DIR:-}" ]]; then
CLI_TOOLS_DIR="${OPENCLAW_DOCKER_CLI_TOOLS_DIR}"
elif [[ "${CI:-}" == "true" || "${GITHUB_ACTIONS:-}" == "true" ]]; then
elif openclaw_live_codex_harness_is_ci; then
CLI_TOOLS_DIR="$(mktemp -d "${RUNNER_TEMP:-/tmp}/openclaw-docker-cli-tools.XXXXXX")"
TEMP_DIRS+=("$CLI_TOOLS_DIR")
else
@@ -68,7 +81,7 @@ else
fi
if [[ -n "${OPENCLAW_DOCKER_CACHE_HOME_DIR:-}" ]]; then
CACHE_HOME_DIR="${OPENCLAW_DOCKER_CACHE_HOME_DIR}"
elif [[ "${CI:-}" == "true" || "${GITHUB_ACTIONS:-}" == "true" ]]; then
elif openclaw_live_codex_harness_is_ci; then
CACHE_HOME_DIR="$(mktemp -d "${RUNNER_TEMP:-/tmp}/openclaw-docker-cache.XXXXXX")"
TEMP_DIRS+=("$CACHE_HOME_DIR")
else
@@ -77,7 +90,10 @@ fi
mkdir -p "$CLI_TOOLS_DIR"
mkdir -p "$CACHE_HOME_DIR"
if [[ "${CI:-}" == "true" || "${GITHUB_ACTIONS:-}" == "true" ]]; then
if openclaw_live_codex_harness_is_ci; then
chmod 0777 "$CLI_TOOLS_DIR" "$CACHE_HOME_DIR" || true
fi
if openclaw_live_codex_harness_is_ci; then
DOCKER_USER="$(id -u):$(id -g)"
DOCKER_HOME_DIR="$(mktemp -d "${RUNNER_TEMP:-/tmp}/openclaw-docker-home.XXXXXX")"
TEMP_DIRS+=("$DOCKER_HOME_DIR")
@@ -146,6 +162,11 @@ export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
export COREPACK_HOME="${COREPACK_HOME:-$XDG_CACHE_HOME/node/corepack}"
export NPM_CONFIG_CACHE="${NPM_CONFIG_CACHE:-$XDG_CACHE_HOME/npm}"
export npm_config_cache="$NPM_CONFIG_CACHE"
if [ "${OPENCLAW_LIVE_CODEX_HARNESS_DEBUG:-}" = "1" ]; then
id
mount | grep -E 'openclaw-cache|openclaw-npm|/home/node' || true
ls -ld "$HOME" "$XDG_CACHE_HOME" "$NPM_CONFIG_PREFIX" 2>/dev/null || true
fi
# Force the Codex harness to use the staged `~/.codex` auth files. This lane
# is not meant to exercise raw OpenAI API-key routing unless the lane
# explicitly opts into API-key auth for CI.
@@ -254,6 +275,12 @@ DOCKER_RUN_ARGS=(docker run --rm -t \
--entrypoint bash \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e HOME=/home/node \
-e NPM_CONFIG_PREFIX="$DOCKER_CLI_TOOLS_CONTAINER_DIR" \
-e npm_config_prefix="$DOCKER_CLI_TOOLS_CONTAINER_DIR" \
-e XDG_CACHE_HOME="$DOCKER_CACHE_CONTAINER_DIR" \
-e COREPACK_HOME="$DOCKER_CACHE_CONTAINER_DIR/node/corepack" \
-e NPM_CONFIG_CACHE="$DOCKER_CACHE_CONTAINER_DIR/npm" \
-e npm_config_cache="$DOCKER_CACHE_CONTAINER_DIR/npm" \
-e NODE_OPTIONS=--disable-warning=ExperimentalWarning \
-e OPENCLAW_AGENT_HARNESS_FALLBACK=none \
-e OPENCLAW_DOCKER_AUTH_PRESTAGED="$DOCKER_AUTH_PRESTAGED" \
@@ -287,14 +314,22 @@ openclaw_live_append_array DOCKER_RUN_ARGS DOCKER_EXTRA_ENV_FILES
openclaw_live_append_array DOCKER_RUN_ARGS DOCKER_HOME_MOUNT
openclaw_live_append_array DOCKER_RUN_ARGS DOCKER_TRUSTED_HARNESS_MOUNT
DOCKER_RUN_ARGS+=(\
-v "$CACHE_HOME_DIR":/home/node/.cache \
-v "$CACHE_HOME_DIR":"$DOCKER_CACHE_CONTAINER_DIR" \
-v "$ROOT_DIR":/src:ro \
-v "$CONFIG_DIR":/home/node/.openclaw \
-v "$WORKSPACE_DIR":/home/node/.openclaw/workspace \
-v "$CLI_TOOLS_DIR":/home/node/.npm-global)
-v "$CLI_TOOLS_DIR":"$DOCKER_CLI_TOOLS_CONTAINER_DIR")
openclaw_live_append_array DOCKER_RUN_ARGS EXTERNAL_AUTH_MOUNTS
openclaw_live_append_array DOCKER_RUN_ARGS PROFILE_MOUNT
DOCKER_RUN_ARGS+=(\
"$LIVE_IMAGE_NAME" \
-lc "$LIVE_TEST_CMD")
if [[ "${OPENCLAW_LIVE_CODEX_HARNESS_DEBUG:-}" == "1" ]]; then
echo "==> Docker debug: host ids and mounted dirs"
id
ls -ld "$CACHE_HOME_DIR" "$CLI_TOOLS_DIR" "${DOCKER_HOME_DIR:-$HOME}" 2>/dev/null || true
printf '==> Docker debug args:'
printf ' %q' "${DOCKER_RUN_ARGS[@]}"
printf '\n'
fi
"${DOCKER_RUN_ARGS[@]}"