fix(operator): guard Codex release compatibility #370
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |