From ad7385b9a5f7c3e32d78a7a9c394e0d7fe355560 Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:33:25 +0300 Subject: [PATCH 1/2] fix(orchestration): use supported Codex worker command --- scripts/orchestrate-codex-worker.sh | 22 +++--- .../scripts/orchestrate-codex-worker.test.js | 76 +++++++++++++++++++ 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/scripts/orchestrate-codex-worker.sh b/scripts/orchestrate-codex-worker.sh index fde2485ff..a93caa7b2 100755 --- a/scripts/orchestrate-codex-worker.sh +++ b/scripts/orchestrate-codex-worker.sh @@ -48,20 +48,16 @@ fi write_status "running" "- Task file: \`$task_file\`" -# SECURITY: never auto-approve agent tool execution. The worker prompt is built -# from a task file that may contain LLM-generated or third-party content -# (indirect prompt injection). `codex exec -p yolo` would execute -# rm -rf / exfiltration commands without confirmation. -# Default to the most restrictive approval mode; allow an explicit operator -# override only via env (e.g. ECC_CODEX_APPROVAL_MODE=on-request for trusted runs). -# Codex profiles (-p) and approval policies (--ask-for-approval) are -# independent concepts. SECURITY: default to never approving untrusted -# tool execution; operators can override via env. +# Keep the configured sandbox; do not bypass approvals or sandboxing for task +# files that may contain third-party content. Non-interactive runs default to +# never prompting for approvals. This does not disable commands allowed by the +# sandbox. Operators can request approvals with ECC_CODEX_APPROVAL_POLICY. +# Approval policies (--ask-for-approval) are separate from Codex profiles (-p). APPROVAL_POLICY="${ECC_CODEX_APPROVAL_POLICY:-never}" case "$APPROVAL_POLICY" in - never|on-request|on-failure) ;; + never|on-request) ;; *) - echo "[ECC worker] Refusing to run: unsupported ECC_CODEX_APPROVAL_POLICY='$APPROVAL_POLICY' (expected never|on-request|on-failure)" >&2 + echo "[ECC worker] Refusing to run: unsupported ECC_CODEX_APPROVAL_POLICY='$APPROVAL_POLICY' (expected never|on-request)" >&2 write_status "failed" "- Error: unsupported approval policy" exit 1 ;; @@ -109,7 +105,9 @@ Task file: $task_file $(cat "$task_file") EOF -if codex exec --ask-for-approval "$APPROVAL_POLICY" -m gpt-5.4 --color never -C "$(pwd)" -o "$output_file" - < "$prompt_file"; then +# Approval is a global option. Inherit the operator's configured model instead +# of pinning a model that may be unavailable or retired. +if codex --ask-for-approval "$APPROVAL_POLICY" exec --color never -C "$(pwd)" -o "$output_file" - < "$prompt_file"; then { echo "# Handoff" echo diff --git a/tests/scripts/orchestrate-codex-worker.test.js b/tests/scripts/orchestrate-codex-worker.test.js index 9a9456227..6a8c5c17b 100644 --- a/tests/scripts/orchestrate-codex-worker.test.js +++ b/tests/scripts/orchestrate-codex-worker.test.js @@ -24,6 +24,82 @@ function test(desc, fn) { } } +function runWorker({ policy, exitCode = 0 } = {}) { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-worker-')); + const binDir = path.join(tempRoot, 'bin'); + const captureFile = path.join(tempRoot, 'args.json'); + const taskFile = path.join(tempRoot, 'task.md'); + const handoffFile = path.join(tempRoot, 'handoff.md'); + const statusFile = path.join(tempRoot, 'status.md'); + + try { + fs.mkdirSync(binDir); + fs.writeFileSync(taskFile, 'Make a focused change.'); + fs.writeFileSync(path.join(binDir, 'codex'), `#!${process.execPath} +const fs = require('fs'); +const args = process.argv.slice(2); +fs.writeFileSync(process.env.ECC_TEST_CAPTURE, JSON.stringify(args)); +// Approval is a global option, not an exec option in current Codex CLI. +const execIndex = args.indexOf('exec'); +if (execIndex < 0 || args.indexOf('--ask-for-approval') > execIndex) { + console.error("unexpected argument '--ask-for-approval' found"); + process.exit(2); +} +const outputIndex = args.indexOf('-o'); +fs.writeFileSync(args[outputIndex + 1], 'Completed the focused task.'); +process.exit(Number(process.env.ECC_TEST_EXIT)); +`, { mode: 0o755 }); + spawnSync('git', ['init'], { cwd: tempRoot, stdio: 'ignore' }); + const env = { ...process.env, + PATH: `${binDir}${path.delimiter}${process.env.PATH}`, + ECC_TEST_CAPTURE: captureFile, + ECC_TEST_EXIT: String(exitCode), + ECC_CODEX_APPROVAL_POLICY: policy || 'never' + }; + const result = spawnSync('bash', [SCRIPT, taskFile, handoffFile, statusFile], { + cwd: tempRoot, env, encoding: 'utf8', timeout: 10000 + }); + return { + ...result, + args: fs.existsSync(captureFile) ? JSON.parse(fs.readFileSync(captureFile, 'utf8')) : null, + handoff: fs.existsSync(handoffFile) ? fs.readFileSync(handoffFile, 'utf8') : '', + workerStatus: fs.readFileSync(statusFile, 'utf8') + }; + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } +} + +test('starts a worker with the current CLI grammar and configured model', () => { + const result = runWorker(); + assert.strictEqual(result.status, 0, result.stderr); + assert.ok(!result.args.includes('-m') && !result.args.includes('--model'), + 'Worker should inherit the configured model rather than pin a retired model'); + assert.ok(result.handoff.includes('Completed the focused task.')); + assert.ok(result.workerStatus.includes('- State: completed')); +}); + +test('forwards the explicit supported approval policy', () => { + const result = runWorker({ policy: 'on-request' }); + assert.strictEqual(result.status, 0, result.stderr); + assert.strictEqual(result.args[result.args.indexOf('--ask-for-approval') + 1], 'on-request'); +}); + +test('rejects the deprecated approval policy before invoking Codex', () => { + const result = runWorker({ policy: 'on-failure' }); + assert.notStrictEqual(result.status, 0); + assert.strictEqual(result.args, null); + assert.ok(result.workerStatus.includes('unsupported approval policy')); +}); + +test('records failure artifacts when a started worker exits nonzero', () => { + const result = runWorker({ exitCode: 1 }); + assert.strictEqual(result.status, 1); + assert.ok(result.args); + assert.ok(result.workerStatus.includes('- State: failed')); + assert.ok(result.handoff.includes('The Codex worker exited with a non-zero status.')); +}); + test('fails fast for an unreadable task file and records failure artifacts', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-worker-')); const handoffFile = path.join(tempRoot, '.orchestration', 'docs', 'handoff.md'); From 0609433033a13b1a4bd07e99b005bfc71c42b81e Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:43:19 +0300 Subject: [PATCH 2/2] test(orchestration): verify unset approval policy default --- tests/scripts/orchestrate-codex-worker.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/scripts/orchestrate-codex-worker.test.js b/tests/scripts/orchestrate-codex-worker.test.js index 6a8c5c17b..48a3ca6e0 100644 --- a/tests/scripts/orchestrate-codex-worker.test.js +++ b/tests/scripts/orchestrate-codex-worker.test.js @@ -53,9 +53,10 @@ process.exit(Number(process.env.ECC_TEST_EXIT)); const env = { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH}`, ECC_TEST_CAPTURE: captureFile, - ECC_TEST_EXIT: String(exitCode), - ECC_CODEX_APPROVAL_POLICY: policy || 'never' + ECC_TEST_EXIT: String(exitCode) }; + if (policy === undefined) delete env.ECC_CODEX_APPROVAL_POLICY; + else env.ECC_CODEX_APPROVAL_POLICY = policy; const result = spawnSync('bash', [SCRIPT, taskFile, handoffFile, statusFile], { cwd: tempRoot, env, encoding: 'utf8', timeout: 10000 }); @@ -73,6 +74,7 @@ process.exit(Number(process.env.ECC_TEST_EXIT)); test('starts a worker with the current CLI grammar and configured model', () => { const result = runWorker(); assert.strictEqual(result.status, 0, result.stderr); + assert.strictEqual(result.args[result.args.indexOf('--ask-for-approval') + 1], 'never'); assert.ok(!result.args.includes('-m') && !result.args.includes('--model'), 'Worker should inherit the configured model rather than pin a retired model'); assert.ok(result.handoff.includes('Completed the focused task.'));