Skip to content

fix(operator): guard Codex background automation continuity #227

fix(operator): guard Codex background automation continuity

fix(operator): guard Codex background automation continuity #227

name: Operator continuity policy
on:
push:
branches: [dev]
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled, edited]
workflow_dispatch:
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout without persisted credentials
uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
- name: Validate operator routing
run: bun scripts/operator/validate-config.mjs
- name: Check operator script syntax
run: find scripts/operator -name '*.mjs' -print0 | xargs -0 -n1 node --check
- name: Smoke test same-root atomic edit fallback
shell: bash
run: |
set -euo pipefail
WORK="$RUNNER_TEMP/atomic-edit-workspace"
TARGET="$WORK/example.txt"
mkdir -p "$WORK"
printf 'before\n' > "$TARGET"
EXPECTED=$(node -e 'const fs=require("fs"),c=require("crypto");process.stdout.write(c.createHash("sha256").update(fs.readFileSync(process.argv[1])).digest("hex"))' "$TARGET")
CONTENT=$(node -e 'process.stdout.write(Buffer.from("after\n","utf8").toString("base64"))')
REQUEST=$(node -e 'process.stdout.write(JSON.stringify({path:process.argv[1],expected_sha256:process.argv[2],content_base64:process.argv[3]}))' "$TARGET" "$EXPECTED" "$CONTENT")
RESULT=$(cd "$WORK" && printf '%s' "$REQUEST" | bun "$GITHUB_WORKSPACE/scripts/operator/atomic-file-edit.mjs")
node -e 'const result=JSON.parse(process.argv[1]);if(result.verified!==true||result.same_directory_temp!==true)process.exit(1)' "$RESULT"
test "$(cat "$TARGET")" = "after"
STALE=$(node -e 'process.stdout.write(JSON.stringify({path:process.argv[1],expected_sha256:"0000000000000000000000000000000000000000000000000000000000000000",content_base64:process.argv[2]}))' "$TARGET" "$CONTENT")
if (cd "$WORK" && printf '%s' "$STALE" | bun "$GITHUB_WORKSPACE/scripts/operator/atomic-file-edit.mjs"); then
echo "Atomic edit accepted a stale expected hash" >&2
exit 1
fi
- name: Validate Codex cache, Code Mode, quota, and HTTP-SSE recovery guards
shell: bash
run: |
set -euo pipefail
EMPTY_CODEX_HOME="$RUNNER_TEMP/empty-codex-cache-home"
mkdir -p "$EMPTY_CODEX_HOME"
bun scripts/operator/codex-cache-audit.mjs \
--codex-home "$EMPTY_CODEX_HOME" \
--duration-ms 25 \
--interval-ms 25 \
--json
DRY_RUN=$(OPERATOR_CODEX_BINARY=codex bun scripts/operator/codex-cache-safe-launch.mjs --dry-run -- exec --ephemeral -)
node -e '
const result = JSON.parse(process.argv[1])
const disabled = new Set()
for (let index = 0; index < result.args.length - 1; index += 1) {
if (result.args[index] === "--disable") disabled.add(result.args[index + 1])
}
const modelIndex = result.args.findIndex((value) => value === "-m")
const agentsDisabled = result.args.some(
(value, index) => value === "-c" && result.args[index + 1] === "agents.enabled=false",
)
const oneThread = result.args.some(
(value, index) =>
value === "-c" && result.args[index + 1] === "agents.max_concurrent_threads_per_session=1",
)
if (
result.remote_plugin !== false ||
result.code_mode !== false ||
result.code_mode_only !== false ||
result.multi_agent_v2 !== false ||
result.agents_enabled !== false ||
result.max_concurrent_threads_per_session !== 1 ||
result.model !== "gpt-5.6-luna" ||
result.http_sse_recovery !== false ||
!disabled.has("remote_plugin") ||
!disabled.has("code_mode") ||
!disabled.has("code_mode_only") ||
!disabled.has("multi_agent_v2") ||
modelIndex === -1 ||
result.args[modelIndex + 1] !== "gpt-5.6-luna" ||
!agentsDisabled ||
!oneThread
) process.exit(1)
' "$DRY_RUN"
if OPERATOR_CODEX_BINARY=codex bun scripts/operator/codex-cache-safe-launch.mjs --dry-run -- --enable code_mode; then
echo "Code Mode guard accepted a prohibited override" >&2
exit 1
fi
if OPERATOR_CODEX_BINARY=codex bun scripts/operator/codex-cache-safe-launch.mjs --dry-run -- --enable multi_agent_v2; then
echo "MultiAgent V2 guard accepted a prohibited override" >&2
exit 1
fi
if OPERATOR_CODEX_BINARY=codex bun scripts/operator/codex-cache-safe-launch.mjs --dry-run -- -m gpt-5.6-sol; then
echo "Quota guard accepted a V2-forcing model" >&2
exit 1
fi
if OPERATOR_CODEX_BINARY=codex bun scripts/operator/codex-cache-safe-launch.mjs --dry-run -- -m gpt-5.5; then
echo "Quota guard accepted a release-dependent V2 model" >&2
exit 1
fi
if OPERATOR_CODEX_BINARY=codex bun scripts/operator/codex-cache-safe-launch.mjs --dry-run -- -c model_reasoning_effort=ultra; then
echo "Quota guard accepted automatic-delegation reasoning" >&2
exit 1
fi
HTTP_RECOVERY=$(OPERATOR_CODEX_BINARY=codex bun scripts/operator/codex-cache-safe-launch.mjs --dry-run --http-sse-recovery -- exec --ephemeral -)
node -e '
const result = JSON.parse(process.argv[1])
const configIndex = result.args.findIndex(
(value, index) => value === "-c" && result.args[index + 1] === "model_providers.openai.supports_websockets=false",
)
if (
result.http_sse_recovery !== true ||
result.openai_websockets !== false ||
result.model !== "gpt-5.6-luna" ||
configIndex === -1
) process.exit(1)
' "$HTTP_RECOVERY"
if OPERATOR_CODEX_BINARY=codex bun scripts/operator/codex-cache-safe-launch.mjs \
--dry-run \
--http-sse-recovery \
-- \
-c model_providers.openai.supports_websockets=true; then
echo "HTTP-SSE recovery guard accepted a WebSocket re-enable override" >&2
exit 1
fi
- name: Validate Codex Desktop and WSL guards
shell: pwsh
run: |
foreach ($file in @(
"scripts/operator/codex-desktop-guard.ps1",
"scripts/operator/codex-wsl-direct.ps1"
)) {
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
$file,
[ref]$tokens,
[ref]$errors
) | Out-Null
if ($errors.Count -gt 0) {
$errors | ForEach-Object { Write-Error "$file`: $($_.Message)" }
exit 1
}
}
$emptyCodexHome = Join-Path $env:RUNNER_TEMP "empty-codex-home"
New-Item -ItemType Directory -Path $emptyCodexHome -Force | Out-Null
& ./scripts/operator/codex-desktop-guard.ps1 -CodexHome $emptyCodexHome -Json
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Smoke test quota-safe OpenAI route and local failover
shell: bash
run: |
set -euo pipefail
PRIMARY_STATE="$RUNNER_TEMP/operator-primary-state"
FALLBACK_STATE="$RUNNER_TEMP/operator-fallback-state"
dump_state() {
echo "=== Operator smoke-test diagnostics ==="
find "$RUNNER_TEMP" -path '*/operator-*-state/*' -type f -maxdepth 6 -print -exec sed -n '1,220p' {} \; 2>/dev/null || true
}
trap dump_state ERR
export OPERATOR_OPENAI_COMMAND='["node","-e","process.stdin.resume();process.stdin.on(\"end\",()=>process.exit(0))"]'
export OPERATOR_LOCAL_COMMAND='["node","-e","process.stdin.resume();process.stdin.on(\"end\",()=>process.exit(0))"]'
PRIMARY_TASK=$(bun scripts/operator/init-task.mjs \
--state-dir "$PRIMARY_STATE" \
--objective "CI quota-safe OpenAI smoke test" \
--acceptance "GPT-5.6 Luna executes|State persists|Subagents remain disabled" \
--risk medium \
--write \
--handoff-safe true)
bun scripts/operator/run-with-failover.mjs --state-dir "$PRIMARY_STATE" --task "$PRIMARY_TASK" --profile continuity
grep -q '"status": "awaiting_verification"' "$(dirname "$PRIMARY_TASK")/current-state.json"
grep -q '"active_provider": "openai"' "$(dirname "$PRIMARY_TASK")/current-state.json"
grep -q '"active_model": "gpt-5.6-luna"' "$(dirname "$PRIMARY_TASK")/current-state.json"
node <<'NODE'
const fs = require('fs')
const file = 'config/operator-routing.json'
const config = JSON.parse(fs.readFileSync(file, 'utf8'))
config.execution.readRetries = 0
fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`)
NODE
export OPERATOR_OPENAI_COMMAND='["node","-e","process.stdin.resume();process.stdin.on(\"end\",()=>process.exit(1))"]'
FALLBACK_TASK=$(bun scripts/operator/init-task.mjs \
--state-dir "$FALLBACK_STATE" \
--objective "CI local continuity smoke test" \
--acceptance "Failed OpenAI route hands off|Local route executes" \
--risk read_only)
bun scripts/operator/run-with-failover.mjs --state-dir "$FALLBACK_STATE" --task "$FALLBACK_TASK" --profile continuity
grep -q '"active_provider": "local"' "$(dirname "$FALLBACK_TASK")/current-state.json"
grep -q '"active_model": "local-approved"' "$(dirname "$FALLBACK_TASK")/current-state.json"
- name: Smoke test durable queue
shell: bash
run: |
set -euo pipefail
STATE="$RUNNER_TEMP/operator-queue-state"
FIRST=$(bun scripts/operator/queue-action.mjs \
--state-dir "$STATE" \
--action ci_smoke_test \
--payload '{"ok":true}' \
--idempotency-key ci-smoke-test-v1)
SECOND=$(bun scripts/operator/queue-action.mjs \
--state-dir "$STATE" \
--action ci_smoke_test \
--payload '{"ok":true}' \
--idempotency-key ci-smoke-test-v1)
test "$FIRST" = "$SECOND"
export OPERATOR_ACTION_EXECUTOR_COMMAND='["node","-e","process.stdin.resume();process.stdin.on(\"end\",()=>process.stdout.write(JSON.stringify({verified:true})))"]'
bun scripts/operator/process-queue.mjs --state-dir "$STATE"
find "$STATE/queue/completed" -name '*.json' -print -quit | grep -q .
- name: Smoke test local Gmail attachment shim
shell: bash
run: |
set -euo pipefail
STATE="$RUNNER_TEMP/operator-gmail-state"
ATTACHMENTS="$RUNNER_TEMP/operator-gmail-attachments"
mkdir -p "$ATTACHMENTS"
printf 'continuity attachment\n' > "$ATTACHMENTS/continuity.txt"
PAYLOAD=$(node -e 'process.stdout.write(JSON.stringify({to:"test@example.com",subject:"Continuity test",body_text:"Dry-run only",attachments:[{path:process.argv[1]}]}))' "$ATTACHMENTS/continuity.txt")
bun scripts/operator/queue-action.mjs \
--state-dir "$STATE" \
--action gmail_send \
--payload "$PAYLOAD" \
--idempotency-key gmail-ci-smoke-v1
export OPERATOR_ACTION_EXECUTOR_COMMAND='["node","scripts/operator/gmail-send-local.mjs"]'
export OPERATOR_GMAIL_DRY_RUN=true
export OPERATOR_GMAIL_ATTACHMENT_ROOTS="$ATTACHMENTS"
bun scripts/operator/process-queue.mjs --state-dir "$STATE"
COMPLETED=$(find "$STATE/queue/completed" -name '*.json' -print -quit)
node -e '
const fs = require("fs")
const record = JSON.parse(fs.readFileSync(process.argv[1], "utf8"))
const result = JSON.parse(record.executor_result.stdout)
if (record.status !== "completed" || result.verified !== true || result.dry_run !== true) process.exit(1)
' "$COMPLETED"
- name: Enforce protected-path and agent-intake policy
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.base_ref }}
HEAD_REF: ${{ github.head_ref }}
PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }}
PR_BODY: ${{ github.event.pull_request.body }}
run: bun scripts/operator/check-pr-policy.mjs