diff --git a/data/repo-awareness-snapshot.json b/data/repo-awareness-snapshot.json index edebee02c9..056c671321 100644 --- a/data/repo-awareness-snapshot.json +++ b/data/repo-awareness-snapshot.json @@ -21556,6 +21556,14 @@ "outcome": "2 Codex P2 → skip placeholders in compactBestUseTitle; gate subtitle compaction to services mode; threads unreplied (API 403)", "checks": "vitest services-catalog+document-search-record-fault 20 passed; no provider-backed checks" }, + { + "date": "2026-10-16", + "ref": "codex/cloud-policy-clarity", + "head": "bdb0cea521800eb85f47fa3a0de3ce7869082a4c", + "scope": "Codex Cloud reasoning-effort and raw-env policy overhaul", + "outcome": "PASS (0 blocking findings; removed all xhigh prompt-marker references across policies/skills/RAG launch checks, added GH_TOKEN to known launcher scrub path, clarified activation guidance)", + "checks": "check:codex-cloud, vitest (tests/codex-cloud-setup.test.ts, tests/rag-local-agent-policy.test.ts, tests/rag-plan-package-parity.test.ts, tests/organisation-decisions.test.ts: 150 passed), docs:maintain, check:instructions, check:decisions, check:organisation" + }, { "date": "2026-07-20", "ref": "cursor/documents-search-header-3eab / PR #936", diff --git a/docs/branch-review-index.md b/docs/branch-review-index.md index 26e679149e..5667ef695a 100644 --- a/docs/branch-review-index.md +++ b/docs/branch-review-index.md @@ -15,14 +15,14 @@ Retention and rotation: [`branch-review-archival-policy.md`](branch-review-archi ## Summary -- Records: 702 +- Records: 703 - Distinct ref cells: 452 -- Distinct reviewed heads: 684 +- Distinct reviewed heads: 685 - Date range: `2026-08-12` to `2026-10-16` Records per date, newest first: -- `2026-10-16` — 3 records +- `2026-10-16` — 4 records - `2026-10-15` — 3 records - `2026-10-14` — 1 record - `2026-10-13` — 2 records @@ -80,6 +80,7 @@ Scope and outcome are truncated; follow the record link for the full six-cell ro | --- | --- | --- | --- | --- | | 2026-10-16 | fix/adversarial-remediation-20261016 | Adversarial assessment + parallel fix swarm + post-fix adversarial review across calculato… | All P0/P1/P2 findings fixed and verified; zero RAG files touched | [2a6b6200](branch-review-records/2a6b620065f5371039438fdbff73be97da6baf6c40dace3596e26c7bad0047dd.record.md) | | 2026-10-16 | codex/cloud-policy-clarity | Codex Cloud reasoning-effort and raw-env policy overhaul | PASS (0 blocking findings; removed all xhigh prompt-marker references… | [3dd0e7e3](branch-review-records/3dd0e7e32634c00735102bf22ba9883745ad72dc5db730b1dce2c33f03e699f0.record.md) | +| 2026-10-16 | codex/cloud-policy-clarity | Codex Cloud environment script and shell-profile hardening | PASS (0 blocking findings; hardened .bashrc prepend, command-shim fall… | [7f059fca](branch-review-records/7f059fca46aac02223aa9f81e3d88a1a08f2b8a4844646d3a8eaa2cabd493c33.record.md) | | 2026-10-16 | feat/backlog-remediation-20261016 | prlanded | PR #3571 squash-merged cleanly into main (e526f8a4d5ad00ad05c5b780262b… | [ad2c45bc](branch-review-records/ad2c45bc2f3c606a7847421acc887ddf4861ab0563a5b96b4c6315b066837d90.record.md) | | 2026-10-15 | codex/ingestion-rag-pilot-handover-20261014 | Uncommitted admin pilot shared announcement correction; file SHA256 0f76deaa4d8d9ad46139d0… | Independent frontend source review ACCEPT; visible message and polite… | [263f1f06](branch-review-records/263f1f0680e9d5cda1bed9bb2a62853d3a34b78b5ad60054971520fdc884cf1c.record.md) | | 2026-10-15 | codex/ingestion-rag-pilot-handover-20261014 | Uncommitted native-PDF layout v5 producer 352d178978ef9086df1df708aa71266538076a2f5050a526… | Independent technical P1/P2 closure and qualified representative-sourc… | [6d2fd2e8](branch-review-records/6d2fd2e81761e17d32bb56bd41261efe8f9239ef266cf85a512c1021f889dd4b.record.md) | diff --git a/docs/branch-review-records/7f059fca46aac02223aa9f81e3d88a1a08f2b8a4844646d3a8eaa2cabd493c33.record.md b/docs/branch-review-records/7f059fca46aac02223aa9f81e3d88a1a08f2b8a4844646d3a8eaa2cabd493c33.record.md new file mode 100644 index 0000000000..823e2aa81c --- /dev/null +++ b/docs/branch-review-records/7f059fca46aac02223aa9f81e3d88a1a08f2b8a4844646d3a8eaa2cabd493c33.record.md @@ -0,0 +1 @@ +| 2026-10-16 | codex/cloud-policy-clarity | 3e0876f6e81d765f6d3695c74385d5b5a91b7951 | Codex Cloud environment script and shell-profile hardening | PASS (0 blocking findings; hardened .bashrc prepend, command-shim fallback, check-codex-cloud auto-profile load, Node 24.15.0 diagnostic floor, offline provider env scrubbing, and receipt schema cleanup) | check:codex-cloud, vitest (tests/codex-cloud-diagnose.test.ts, tests/codex-cloud-setup.test.ts: 50 passed), docs:maintain, check-format-changed, typecheck | diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index 199f4ad788..180f6e3b46 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -119,14 +119,17 @@ The generated shell profile removes known OpenAI, Supabase, Railway, GitHub/GitL database, CI-trigger, test-user, and emergency-PAT variables. This prevents an unrelated Cloud task from silently becoming provider-backed. -Set all five offline values in the environment UI. The setup also writes the generated -profile to `.bashrc`, `.profile`, and `.bash_profile`. Covering `.bash_profile` is required: -Bash stops login-profile discovery at the first matching file, so an existing `.bash_profile` -can otherwise prevent `.profile` from running. Offline mode values are forced after inherited -platform values so stale `auto`/`false` settings cannot outrank the repository contract. -Do not manually source the profile as an acceptance workaround: start a fresh task and invoke -the direct `npm run check:codex-cloud` commands below. The tracked command shims make those -ordinary Node commands load the profile themselves. +Set all five offline values in the environment UI. The setup also prepends the generated +profile to the top of `.bashrc`, `.profile`, and `.bash_profile` (ahead of Ubuntu's +non-interactive `return` guard in `.bashrc`) and runs `scripts/install-codex-cloud-command-shims.sh`. +Covering `.bash_profile` is required: Bash stops login-profile discovery at the first matching +file, so an existing `.bash_profile` can otherwise prevent `.profile` from running. Offline mode +values are forced after inherited platform values so stale `auto`/`false` settings cannot outrank +the repository contract. Do not manually source the profile as an acceptance workaround: start a +fresh task and invoke the direct `npm run check:codex-cloud` commands below. Both the tracked +command shims and `scripts/check-codex-cloud-setup.mjs` itself automatically load +`$HOME/.clinical-kb-codex-cloud.sh` when invoked in a Cloud shell (including raw `bash -c` +invocations where `/root/.nvm/versions/node/.../bin` precedes `$HOME/.local/bin` on `PATH`). ### Connected (explicit opt-in) diff --git a/docs/superpowers/rag-upgrade/canonical/phase-receipt.schema.json b/docs/superpowers/rag-upgrade/canonical/phase-receipt.schema.json index f6ec9e671a..a04ca5bf90 100644 --- a/docs/superpowers/rag-upgrade/canonical/phase-receipt.schema.json +++ b/docs/superpowers/rag-upgrade/canonical/phase-receipt.schema.json @@ -17,7 +17,6 @@ "programmeImplementationBase", "branch", "launchProfile", - "xhighConfirmed", "controllerRouting", "capabilityEvidence", "phaseStartSha", @@ -45,7 +44,6 @@ "programmeImplementationBase": { "$ref": "#/$defs/gitSha" }, "branch": { "type": "string", "minLength": 1 }, "launchProfile": { "enum": ["high", "xhigh"] }, - "xhighConfirmed": { "type": "boolean" }, "controllerRouting": { "$ref": "#/$defs/agentRouting" }, "capabilityEvidence": { "type": "array", diff --git a/docs/superpowers/rag-upgrade/canonical/phase-receipt.template.json b/docs/superpowers/rag-upgrade/canonical/phase-receipt.template.json index db67db6505..f3b065cf16 100644 --- a/docs/superpowers/rag-upgrade/canonical/phase-receipt.template.json +++ b/docs/superpowers/rag-upgrade/canonical/phase-receipt.template.json @@ -11,7 +11,6 @@ "programmeImplementationBase": "f3d1a3cce2c943ad3083425ed9c7c46dbef23087", "branch": "codex/rag-upgrade-p00", "launchProfile": "high", - "xhighConfirmed": false, "controllerRouting": { "agentId": "controller-p00", "runtimeHost": "codex-cloud", diff --git a/docs/superpowers/rag-upgrade/cloud/phase-receipt.schema.json b/docs/superpowers/rag-upgrade/cloud/phase-receipt.schema.json index f6ec9e671a..a04ca5bf90 100644 --- a/docs/superpowers/rag-upgrade/cloud/phase-receipt.schema.json +++ b/docs/superpowers/rag-upgrade/cloud/phase-receipt.schema.json @@ -17,7 +17,6 @@ "programmeImplementationBase", "branch", "launchProfile", - "xhighConfirmed", "controllerRouting", "capabilityEvidence", "phaseStartSha", @@ -45,7 +44,6 @@ "programmeImplementationBase": { "$ref": "#/$defs/gitSha" }, "branch": { "type": "string", "minLength": 1 }, "launchProfile": { "enum": ["high", "xhigh"] }, - "xhighConfirmed": { "type": "boolean" }, "controllerRouting": { "$ref": "#/$defs/agentRouting" }, "capabilityEvidence": { "type": "array", diff --git a/docs/superpowers/rag-upgrade/cloud/phase-receipt.template.json b/docs/superpowers/rag-upgrade/cloud/phase-receipt.template.json index db67db6505..f3b065cf16 100644 --- a/docs/superpowers/rag-upgrade/cloud/phase-receipt.template.json +++ b/docs/superpowers/rag-upgrade/cloud/phase-receipt.template.json @@ -11,7 +11,6 @@ "programmeImplementationBase": "f3d1a3cce2c943ad3083425ed9c7c46dbef23087", "branch": "codex/rag-upgrade-p00", "launchProfile": "high", - "xhighConfirmed": false, "controllerRouting": { "agentId": "controller-p00", "runtimeHost": "codex-cloud", diff --git a/docs/superpowers/rag-upgrade/local/phase-receipt.schema.json b/docs/superpowers/rag-upgrade/local/phase-receipt.schema.json index f6ec9e671a..a04ca5bf90 100644 --- a/docs/superpowers/rag-upgrade/local/phase-receipt.schema.json +++ b/docs/superpowers/rag-upgrade/local/phase-receipt.schema.json @@ -17,7 +17,6 @@ "programmeImplementationBase", "branch", "launchProfile", - "xhighConfirmed", "controllerRouting", "capabilityEvidence", "phaseStartSha", @@ -45,7 +44,6 @@ "programmeImplementationBase": { "$ref": "#/$defs/gitSha" }, "branch": { "type": "string", "minLength": 1 }, "launchProfile": { "enum": ["high", "xhigh"] }, - "xhighConfirmed": { "type": "boolean" }, "controllerRouting": { "$ref": "#/$defs/agentRouting" }, "capabilityEvidence": { "type": "array", diff --git a/docs/superpowers/rag-upgrade/local/phase-receipt.template.json b/docs/superpowers/rag-upgrade/local/phase-receipt.template.json index db67db6505..f3b065cf16 100644 --- a/docs/superpowers/rag-upgrade/local/phase-receipt.template.json +++ b/docs/superpowers/rag-upgrade/local/phase-receipt.template.json @@ -11,7 +11,6 @@ "programmeImplementationBase": "f3d1a3cce2c943ad3083425ed9c7c46dbef23087", "branch": "codex/rag-upgrade-p00", "launchProfile": "high", - "xhighConfirmed": false, "controllerRouting": { "agentId": "controller-p00", "runtimeHost": "codex-cloud", diff --git a/scripts/check-codex-cloud-raw-env.sh b/scripts/check-codex-cloud-raw-env.sh index bc42c5148c..28a1b32b0b 100755 --- a/scripts/check-codex-cloud-raw-env.sh +++ b/scripts/check-codex-cloud-raw-env.sh @@ -74,8 +74,12 @@ printf '[Codex Cloud Raw Env] FAIL-KNOWN: inherited documented launcher defect n printf '[Codex Cloud Raw Env] CONTINUE-RESTRICTED: use only the generated profile and command shims; raw boundary remains failed.\n' >&2 for name in "${known[@]}"; do case "$name" in - OPENAI_BASE_URL) printf '[Codex Cloud Raw Env] CONTINUE-RESTRICTED: OPENAI_BASE_URL can redirect OpenAI-bound traffic; never invoke OpenAI clients from the raw parent or any binary that bypasses the profile/shim scrub.\n' >&2 ;; - GH_TOKEN) printf '[Codex Cloud Raw Env] CONTINUE-RESTRICTED: GH_TOKEN carries host GitHub credentials; never invoke gh/GitHub clients from the raw parent or any binary that bypasses the profile/shim scrub.\n' >&2 ;; + OPENAI_BASE_URL) + printf '[Codex Cloud Raw Env] CONTINUE-RESTRICTED: OPENAI_BASE_URL can redirect OpenAI-bound traffic; never invoke OpenAI clients from the raw parent or any binary that bypasses the profile/shim scrub.\n' >&2 + ;; + GH_TOKEN) + printf '[Codex Cloud Raw Env] CONTINUE-RESTRICTED: GH_TOKEN carries host GitHub credentials; never invoke gh, git credential helpers, or network clients from the raw parent or any binary that bypasses the profile/shim scrub.\n' >&2 + ;; esac done exit 2 diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index 74152e44c6..78dc144b46 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -364,24 +364,26 @@ export function validateHostedAppInventory(appNames) { } export const providerCredentialVariables = Object.freeze([ - ...providerEnvironmentKeys, - "RAILWAY_API_TOKEN", - "RAILWAY_TOKEN", - "GH_TOKEN", - "GITHUB_TOKEN", - "CODEX_CLOUD_GITHUB_PAT", - "GITLAB_TOKEN", - "GLAB_TOKEN", - "CODEX_TRIGGER_TOKEN", - "HEALTH_DEEP_PROBE_SECRET", - "INDEXING_V3_AGENT_SECRET", - "CROSS_TENANT_SERVICE_ROLE_KEY", - "FIGMA_CLIENT_ID", - "FIGMA_CLIENT_SECRET", - "FIGMA_ACCESS_TOKEN", - "FIGMA_PERSONAL_ACCESS_TOKEN", - "FIGMA_TOKEN", - "FIGMA_NPM_TOKEN", + ...new Set([ + ...providerEnvironmentKeys, + "RAILWAY_API_TOKEN", + "RAILWAY_TOKEN", + "GH_TOKEN", + "GITHUB_TOKEN", + "CODEX_CLOUD_GITHUB_PAT", + "GITLAB_TOKEN", + "GLAB_TOKEN", + "CODEX_TRIGGER_TOKEN", + "HEALTH_DEEP_PROBE_SECRET", + "INDEXING_V3_AGENT_SECRET", + "CROSS_TENANT_SERVICE_ROLE_KEY", + "FIGMA_CLIENT_ID", + "FIGMA_CLIENT_SECRET", + "FIGMA_ACCESS_TOKEN", + "FIGMA_PERSONAL_ACCESS_TOKEN", + "FIGMA_TOKEN", + "FIGMA_NPM_TOKEN", + ]), ]); function read(relativePath) { @@ -1079,13 +1081,16 @@ function validateMaintenanceAndCommandShims(sources, errors) { } for (const [pattern, message] of [ [/nvm version/, "Cloud command shims must resolve the selected Node version without following their own wrappers."], - [/clean_path=.*\.local.*bin/, "Cloud command shims must remove their directory before running child npm scripts."], + [ + /\[\[ "\\\$path_entry" == "\\\$HOME\/\.local\/bin" \]\] && continue[\s\S]*clean_path\+="\\\$path_entry"/, + "Cloud command shims must remove their directory before running child npm scripts.", + ], [/mkdir -p "\$HOME\/\.local\/bin"/, "Cloud command shims must create their destination directory."], [/\.clinical-kb-codex-cloud\.sh/, "Cloud command shims must load the generated profile."], ]) { requireMatch(errors, commandShims, pattern, message); } - if (!commandShims.includes('exec "$node_bin/$command_name" "\\$@"')) { + if (!commandShims.includes('exec "$command_path" "\\$@"')) { errors.push("Cloud command shims must execute absolute Node commands."); } } @@ -1328,40 +1333,90 @@ export async function validateCodexCloudRuntime(env = process.env) { return errors; } +/** @param {NodeJS.ProcessEnv | Record} [env] */ +function readableRuntimeProfile(env = process.env) { + if (!env.HOME) return null; + const profilePath = path.join(env.HOME, ".clinical-kb-codex-cloud.sh"); + try { + return statSync(profilePath).isFile() && (accessSync(profilePath, constants.R_OK), profilePath); + } catch { + return null; + } +} + +/** @param {NodeJS.ProcessEnv | Record} [env] */ +export function shouldAutoSourceCloudProfile(env = process.env) { + return ( + env.CODEX_CLOUD === "1" && + env.NODE_ENV !== "test" && + env.CODEX_CLOUD_PROFILE_LOADED !== "1" && + Boolean(readableRuntimeProfile(env)) + ); +} + if (isDirectEntrypoint(import.meta.url)) { - const errors = validateCodexCloudSetup(); - const commandArguments = process.argv.slice(2); - const hostedAppArgumentErrors = validateHostedAppInventoryArguments(commandArguments); - const hostedAppInventory = parseHostedAppInventoryArgument(commandArguments); - const hostedAppInventoryErrors = validateHostedAppInventory(hostedAppInventory); - const hostedAppInputErrors = [...hostedAppArgumentErrors, ...hostedAppInventoryErrors]; - errors.push(...hostedAppInputErrors); - const runtime = process.argv.includes("--runtime"); - const environment = runtime || process.env.CODEX_CLOUD === "1" || process.argv.includes("--environment"); - if (runtime) errors.push(...(await validateCodexCloudRuntime())); - else if (environment) errors.push(...validateCodexCloudEnvironment()); - if ( - runtime && - process.env.CODEX_CLOUD_PROVISIONING === "1" && - gitCheckoutFreshness(repoRoot).freshness === "unverified" - ) { - console.warn( - "[Codex Cloud Check] WARN: checkout freshness is unverified during provisioning; run explicit acceptance with CODEX_CLOUD_EXPECTED_BASE_SHA.", + const runtimeProfilePath = shouldAutoSourceCloudProfile(process.env) ? readableRuntimeProfile(process.env) : null; + if (runtimeProfilePath) { + const bashExecutable = + process.platform === "win32" + ? path.join(process.env.ProgramFiles || "C:\\Program Files", "Git/bin/bash.exe") + : "bash"; + const reexec = spawnSync( + bashExecutable, + [ + "-c", + 'profile_path="$1"; node_exec="$2"; script_path="$3"; shift 3; saved_args=("$@"); set --; . "$profile_path" && export CODEX_CLOUD_PROFILE_LOADED=1 && exec "$node_exec" "$script_path" "${saved_args[@]}"', + "codex-cloud-check", + runtimeProfilePath.replaceAll("\\", "/"), + process.execPath, + fileURLToPath(import.meta.url), + ...process.argv.slice(2), + ], + { cwd: repoRoot, stdio: "inherit" }, ); - } - if (environment && hostedAppInputErrors.length === 0) { - console.log("[Codex Cloud Environment] sanitized effective modes and capabilities:"); - for (const line of sanitizedCloudCapabilityLines(process.env, { hostedAppInventory })) console.log(` ${line}`); - } - if (errors.length > 0) { - for (const error of errors) console.error(`[Codex Cloud Check] FAIL: ${error}`); - process.exitCode = 1; + if (reexec.error) { + console.error( + `[Codex Cloud Check] FAIL: could not re-exec through bash to load ${runtimeProfilePath}: ${reexec.error.message}`, + ); + } else if (reexec.signal) { + console.error(`[Codex Cloud Check] FAIL: bash re-exec was terminated by signal ${reexec.signal}.`); + } + process.exitCode = reexec.status ?? 1; } else { - const scope = codexCloudValidationScope({ - runtime, - environment, - browserInstallSkipped: process.env.CODEX_CLOUD_SKIP_BROWSER_INSTALL === "1", - }); - console.log(`[Codex Cloud Check] PASS: ${scope} Cloud contracts match.`); + const errors = validateCodexCloudSetup(); + const commandArguments = process.argv.slice(2); + const hostedAppArgumentErrors = validateHostedAppInventoryArguments(commandArguments); + const hostedAppInventory = parseHostedAppInventoryArgument(commandArguments); + const hostedAppInventoryErrors = validateHostedAppInventory(hostedAppInventory); + const hostedAppInputErrors = [...hostedAppArgumentErrors, ...hostedAppInventoryErrors]; + errors.push(...hostedAppInputErrors); + const runtime = process.argv.includes("--runtime"); + const environment = runtime || process.env.CODEX_CLOUD === "1" || process.argv.includes("--environment"); + if (runtime) errors.push(...(await validateCodexCloudRuntime())); + else if (environment) errors.push(...validateCodexCloudEnvironment()); + if ( + runtime && + process.env.CODEX_CLOUD_PROVISIONING === "1" && + gitCheckoutFreshness(repoRoot).freshness === "unverified" + ) { + console.warn( + "[Codex Cloud Check] WARN: checkout freshness is unverified during provisioning; run explicit acceptance with CODEX_CLOUD_EXPECTED_BASE_SHA.", + ); + } + if (environment && hostedAppInputErrors.length === 0) { + console.log("[Codex Cloud Environment] sanitized effective modes and capabilities:"); + for (const line of sanitizedCloudCapabilityLines(process.env, { hostedAppInventory })) console.log(` ${line}`); + } + if (errors.length > 0) { + for (const error of errors) console.error(`[Codex Cloud Check] FAIL: ${error}`); + process.exitCode = 1; + } else { + const scope = codexCloudValidationScope({ + runtime, + environment, + browserInstallSkipped: process.env.CODEX_CLOUD_SKIP_BROWSER_INSTALL === "1", + }); + console.log(`[Codex Cloud Check] PASS: ${scope} Cloud contracts match.`); + } } } diff --git a/scripts/check-rag-phase-receipts.mjs b/scripts/check-rag-phase-receipts.mjs index e6b5cfa2d3..2b8c4bbd54 100644 --- a/scripts/check-rag-phase-receipts.mjs +++ b/scripts/check-rag-phase-receipts.mjs @@ -408,9 +408,6 @@ function validateCapabilityEvidence(items, label, errors, requireTracked) { function validateModelRouting(receipt, phase, errors, requireTracked) { const controllerReasoning = manifest.adaptiveEffortPolicy.highLaunchPhases.includes(phase.id) ? "high" : "xhigh"; if (receipt.launchProfile !== controllerReasoning) errors.push(`launchProfile must be ${controllerReasoning}`); - if (receipt.xhighConfirmed !== (controllerReasoning === "xhigh")) { - errors.push(`xhighConfirmed must be ${controllerReasoning === "xhigh"}`); - } validateAgentRouting( receipt.controllerRouting, { model: manifest.defaultAgentPolicy.controllerModel, reasoning: controllerReasoning }, diff --git a/scripts/diagnose-codex-cloud.mjs b/scripts/diagnose-codex-cloud.mjs index 2e24b36ba1..a54de0c104 100644 --- a/scripts/diagnose-codex-cloud.mjs +++ b/scripts/diagnose-codex-cloud.mjs @@ -6,10 +6,26 @@ import { isDirectEntrypoint } from "./lib/is-entrypoint.mjs"; import { lockPythonVersion, workerPythonLockTargets } from "./worker-python-lock-config.mjs"; +const NODE_MINIMUM_VERSION = "24.15.0"; + function major(version) { return version?.match(/\d+/)?.[0] ?? null; } +function isSupportedNodeVersion(version) { + if (!version || major(version) !== "24") return false; + const parts = version + .match(/^\d+(?:\.\d+){1,2}/)?.[0] + ?.split(".") + .map(Number); + if (!parts || parts.length < 3 || parts.some((part) => !Number.isFinite(part))) return false; + const required = NODE_MINIMUM_VERSION.split(".").map(Number); + for (let index = 0; index < 3; index += 1) { + if (parts[index] !== required[index]) return parts[index] > required[index]; + } + return true; +} + export function diagnoseCodexCloud({ nodeVersion, npmVersion, @@ -19,11 +35,11 @@ export function diagnoseCodexCloud({ setupExitCode, }) { const issues = []; - if (major(nodeVersion) !== "24") { + if (!isSupportedNodeVersion(nodeVersion)) { issues.push({ code: "NODE_RUNTIME", - issue: `Node 24.x is required; detected ${nodeVersion || "unavailable"}.`, - fix: "Select Node 24 in the Cloud environment or rerun setup with nvm available.", + issue: `Node >=${NODE_MINIMUM_VERSION} <25 is required; detected ${nodeVersion || "unavailable"}.`, + fix: "Select Node 24 (>=24.15.0) in the Cloud environment or rerun setup with nvm available.", }); } if (major(npmVersion) !== "11") { diff --git a/scripts/install-codex-cloud-command-shims.sh b/scripts/install-codex-cloud-command-shims.sh index ff87b5450f..1aad4edb4f 100644 --- a/scripts/install-codex-cloud-command-shims.sh +++ b/scripts/install-codex-cloud-command-shims.sh @@ -13,38 +13,99 @@ cd "$repo_root" expected_node_major="$(tr -cd '0-9' < .node-version)" [[ -n "$expected_node_major" ]] || fail "Could not read the Node major from .node-version." +expected_npm_major="$(sed -n 's/.*"npm"[[:space:]]*:[[:space:]]*"\([0-9][0-9]*\)\.x".*/\1/p' package.json | head -n 1)" +[[ -n "$expected_npm_major" ]] || fail "Could not read the npm major from package.json engines.npm." + runtime_profile="$HOME/.clinical-kb-codex-cloud.sh" [[ -f "$runtime_profile" ]] || fail "The generated Cloud runtime profile is unavailable; run setup first." # shellcheck source=/dev/null source "$runtime_profile" -command -v nvm >/dev/null 2>&1 || fail "nvm is unavailable after loading the Cloud runtime profile." -resolved_node_version="$(nvm version "$expected_node_major")" -[[ "$resolved_node_version" != "N/A" ]] || fail "Node ${expected_node_major} is unavailable through nvm." -node_bin="$NVM_DIR/versions/node/$resolved_node_version/bin" +node_version_supported() { + local version="$1" + local actual_major actual_minor actual_patch + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1 + IFS=. read -r actual_major actual_minor actual_patch <<< "$version" + (( actual_major == 24 )) || return 1 + (( actual_minor > 15 )) && return 0 + (( actual_minor == 15 && actual_patch >= 0 )) +} + +# Existing shims must never be picked as their own targets, so every PATH lookup below skips +# $HOME/.local/bin. +fallback_path="" +path_entry_seen=false +IFS=: read -r -a path_entries <<< "$PATH" +for path_entry in "${path_entries[@]}"; do + [[ "$path_entry" == "$HOME/.local/bin" ]] && continue + if $path_entry_seen; then + fallback_path+=: + fi + fallback_path+="$path_entry" + path_entry_seen=true +done +unset path_entries path_entry path_entry_seen + +resolved_node_version="" +if command -v nvm >/dev/null 2>&1; then + resolved_node_version="$(nvm version "$expected_node_major" 2>/dev/null || true)" +fi +if [[ -n "$resolved_node_version" && "$resolved_node_version" != "N/A" ]]; then + node_bin="$NVM_DIR/versions/node/$resolved_node_version/bin" +else + fallback_node="$(PATH="$fallback_path" command -v node 2>/dev/null || true)" + [[ -n "$fallback_node" ]] || fail "Node ${expected_node_major} is unavailable through nvm or PATH." + fallback_node_version="$($fallback_node -p 'process.versions.node' 2>/dev/null || true)" + node_version_supported "$fallback_node_version" || fail "Node ${expected_node_major}.x >=24.15.0 and <25 is required; detected ${fallback_node_version:-unavailable}." + node_bin="$(cd "$(dirname "$fallback_node")" && pwd)" +fi +node_version="$($node_bin/node -p 'process.versions.node' 2>/dev/null || true)" +node_version_supported "$node_version" || fail "Node ${expected_node_major}.x >=24.15.0 and <25 is required; detected ${node_version:-unavailable}." mkdir -p "$HOME/.local/bin" for command_name in node npm npx; do - [[ -x "$node_bin/$command_name" ]] || fail "${command_name} is unavailable in Node ${expected_node_major}." + if [[ "$command_name" = node ]]; then + command_path="$node_bin/node" + else + command_path="" + if [[ -x "$node_bin/$command_name" ]]; then + command_path="$node_bin/$command_name" + else + command_path="$(PATH="$fallback_path" command -v "$command_name" 2>/dev/null || true)" + # A PATH fallback may be an older installation, so it must meet the repository's npm range. + if [[ -x "$command_path" ]]; then + fallback_tool_version="$("$command_path" --version 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)" + [[ "${fallback_tool_version%%.*}" = "$expected_npm_major" ]] || + fail "${command_name} ${expected_npm_major}.x is required (package.json engines.npm); the PATH fallback is ${fallback_tool_version:-unavailable}." + fi + fi + fi + [[ -x "$command_path" ]] || fail "${command_name} is unavailable in Node ${expected_node_major}." cat > "$HOME/.local/bin/$command_name" < "$profile_candidate" + else + printf '# PsychSift Codex Cloud runtime\n%s\n' "$profile_source" > "$profile_candidate" + fi + mv -f "$profile_candidate" "$shell_profile" +done + # Test harness only: write the runtime profile + shell policy, then stop before # toolchain installs so unit tests can exercise config merge without a runtime. if [[ "$policy_only_harness" = "1" ]]; then @@ -274,16 +292,9 @@ if [[ "$policy_only_harness" = "1" ]]; then exit 0 fi -profile_source='[ -f "$HOME/.clinical-kb-codex-cloud.sh" ] && . "$HOME/.clinical-kb-codex-cloud.sh"' -for shell_profile in "$HOME/.bashrc" "$HOME/.profile" "$HOME/.bash_profile"; do - touch "$shell_profile" - if ! grep -Fq '.clinical-kb-codex-cloud.sh' "$shell_profile"; then - printf '\n# PsychSift Codex Cloud runtime\n%s\n' "$profile_source" >> "$shell_profile" - fi -done - # shellcheck source=/dev/null source "$runtime_profile" +bash scripts/install-codex-cloud-command-shims.sh log "Installing locked Node dependencies." setup_step="node-dependencies" diff --git a/scripts/test-environment.mjs b/scripts/test-environment.mjs index 119cae51d7..c2a5fcfbd0 100644 --- a/scripts/test-environment.mjs +++ b/scripts/test-environment.mjs @@ -28,6 +28,23 @@ const providerEnvironmentKeys = Object.freeze([ "NEXT_PUBLIC_SENTRY_DSN", "SENTRY_DSN", "ALLOW_PROVIDER_TESTS", + "RAILWAY_API_TOKEN", + "RAILWAY_TOKEN", + "GH_TOKEN", + "GITHUB_TOKEN", + "CODEX_CLOUD_GITHUB_PAT", + "GITLAB_TOKEN", + "GLAB_TOKEN", + "CODEX_TRIGGER_TOKEN", + "HEALTH_DEEP_PROBE_SECRET", + "INDEXING_V3_AGENT_SECRET", + "CROSS_TENANT_SERVICE_ROLE_KEY", + "FIGMA_CLIENT_ID", + "FIGMA_CLIENT_SECRET", + "FIGMA_ACCESS_TOKEN", + "FIGMA_PERSONAL_ACCESS_TOKEN", + "FIGMA_TOKEN", + "FIGMA_NPM_TOKEN", ]); const offlineUrlValues = Object.freeze({ diff --git a/src/lib/env.ts b/src/lib/env.ts index da38cc4875..55a3e57816 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -71,7 +71,9 @@ const envSchema = z .optional(), ), SUPABASE_DB_URL: z.string().url().optional(), - HEALTH_DEEP_PROBE_SECRET: z.string().min(16).optional(), + // Blank means unset (offline wrappers blank inherited secrets); an unset secret keeps the + // deep probe closed, and a non-blank value must still be at least 16 characters. + HEALTH_DEEP_PROBE_SECRET: z.preprocess(coerceBlankEnv, z.string().min(16).optional()), // Inbound webhook receivers. Each shared secret gates a machine-to-machine // endpoint under /api/webhooks/* and fails closed when unset (the route 503s // rather than trusting an unauthenticated caller). See docs/webhooks.md. diff --git a/tests/codex-cloud-diagnose.test.ts b/tests/codex-cloud-diagnose.test.ts index 7536b4e5fc..7340a81852 100644 --- a/tests/codex-cloud-diagnose.test.ts +++ b/tests/codex-cloud-diagnose.test.ts @@ -19,6 +19,12 @@ describe("Codex Cloud diagnostics", () => { expect(diagnoseCodexCloud(healthy)).toEqual([]); }); + it("flags Node 24 versions below the 24.15.0 engine floor", () => { + const issues = diagnoseCodexCloud({ ...healthy, nodeVersion: "24.13.0" }); + expect(issues.map((issue) => issue.code)).toEqual(["NODE_RUNTIME"]); + expect(issues[0]?.issue).toContain(">=24.15.0 <25"); + }); + it("explains the Python lock mismatch that previously stopped setup", () => { const issues = diagnoseCodexCloud({ ...healthy, diff --git a/tests/codex-cloud-policy-harness.test.ts b/tests/codex-cloud-policy-harness.test.ts index 5464d1342e..6d377d6292 100644 --- a/tests/codex-cloud-policy-harness.test.ts +++ b/tests/codex-cloud-policy-harness.test.ts @@ -119,8 +119,13 @@ describe("Codex Cloud policy-only harness runtime isolation", () => { expect(config).toContain(test.config); expect(config).toContain("[shell_environment_policy]"); expect(existsSync(path.join(test.home, ".clinical-kb-codex-cloud.sh"))).toBe(true); + // Setup now puts the Cloud profile at the top of each shell profile before the harness stops, + // so a non-interactive early return cannot skip it. That is shell configuration only: the + // runtime-call log above still proves no runtime was probed or installed. for (const profile of [".bashrc", ".profile", ".bash_profile"]) { - expect(existsSync(path.join(test.home, profile))).toBe(false); + const contents = readFileSync(path.join(test.home, profile), "utf8"); + expect(contents.startsWith("# PsychSift Codex Cloud runtime\n")).toBe(true); + expect(contents).toContain('. "$HOME/.clinical-kb-codex-cloud.sh"'); } }); diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index f7e428020b..d16e69d29e 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -23,6 +23,7 @@ import { pythonWorkerVersionLine, pythonWorkerImports, sanitizedCloudCapabilityLines, + shouldAutoSourceCloudProfile, validateCodexCloudEnvironment, validateCloudNpmInstallContract, validateCodexProjectMcpConfiguration, @@ -618,7 +619,7 @@ describe("Codex Cloud environment contract", () => { expect(malformedCli.status).toBe(1); expect(malformedCli.stderr).toContain("Unsupported Cloud-check argument"); expect(malformedCli.stdout).not.toContain("hosted_app.inventory="); - }); + }, 120_000); it("probes the raw task environment without printing credential values", () => { const secret = "never-print-raw-provider-value"; @@ -631,7 +632,7 @@ describe("Codex Cloud environment contract", () => { expect(result.stderr).toContain("OPENAI_API_KEY"); expect(result.stderr).toContain("STOP:"); expect(result.stderr).not.toContain(secret); - }); + }, 120_000); it("name-scopes the documented OPENAI_BASE_URL and GH_TOKEN launcher defects separately from unexpected leaks", () => { const known = spawnSync(bashCommand, ["scripts/check-codex-cloud-raw-env.sh"], { @@ -672,7 +673,7 @@ describe("Codex Cloud environment contract", () => { expect(mixed.stderr).not.toContain("never-print-service-role"); expect(mixed.stderr).not.toContain("gho_never_print_known_token"); expect(mixed.stderr).not.toContain("CONTINUE-RESTRICTED:"); - }); + }, 120_000); it("keeps setup and maintenance repairs guarded for repeat execution", () => { const setup = readFileSync(new URL("../scripts/setup-codex-cloud.sh", import.meta.url), "utf8"); @@ -689,7 +690,9 @@ describe("Codex Cloud environment contract", () => { new URL("../scripts/delete-codex-cloud-branch-with-pat.sh", import.meta.url), "utf8", ); - expect(setup).toContain("if ! grep -Fq '.clinical-kb-codex-cloud.sh'"); + expect(setup).toContain('profile_source=\'[ -f "$HOME/.clinical-kb-codex-cloud.sh" ]'); + expect(setup).toContain('grep -Fvx "$profile_source"'); + expect(setup).toContain("sed '/./,$!d'"); expect(setup).toContain('if [[ "$actual_version" != "$expected_version" ]]'); expect(setup).toContain('expected_node_range="$(sed'); expect(setup).toContain('if ! node_version_supported "$actual_node_version"; then'); @@ -737,19 +740,23 @@ describe("Codex Cloud environment contract", () => { expect(setup).toContain('"$ocr_venv/bin/python" -m pip check'); const refreshCommand = "bash scripts/refresh-codex-cloud-base.sh"; const runtimeCheck = "CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"; + const installShimsCommand = "bash scripts/install-codex-cloud-command-shims.sh"; expect(setup).toContain(runtimeCheck); expect(setup).toContain(refreshCommand); + expect(setup).toContain(installShimsCommand); expect(maintenance).toContain(runtimeCheck); expect(maintenance).toContain("ensure-codex-cloud-git-remote.mjs"); expect(maintenance).toContain(refreshCommand); + expect(maintenance).toContain(installShimsCommand); for (const script of [setup, maintenance]) { expect(script.indexOf(refreshCommand)).toBeLessThan(script.indexOf(runtimeCheck)); + expect(script.indexOf(installShimsCommand)).toBeLessThan(script.indexOf(runtimeCheck)); } expect(commandShims).toContain('nvm version "$expected_node_major"'); expect(commandShims).toContain('node_bin="$NVM_DIR/versions/node/$resolved_node_version/bin"'); - expect(commandShims).toContain('while [[ "\\$clean_path" == *":\\$HOME/.local/bin:"* ]]; do'); - expect(commandShims).toContain('clean_path="\\${clean_path//:\\$HOME\\/.local\\/bin:/:}"'); + expect(commandShims).toContain('IFS=: read -r -a path_entries <<< "\\$PATH"'); + expect(commandShims).toContain('[[ "\\$path_entry" == "\\$HOME/.local/bin" ]] && continue'); expect(commandShims).toContain('if [[ -n "\\$clean_path" ]]; then'); expect(commandShims).toContain('export PATH="$node_bin"'); expect(commandShims).toContain('. "$runtime_profile"'); @@ -764,8 +771,10 @@ describe("Codex Cloud environment contract", () => { expect(patDelete).toContain("core.hooksPath=/dev/null"); }); - it("removes every adjacent command-shim PATH entry", () => { - const home = temporaryDirectory("codex-cloud-shims-"); + it("removes every adjacent command-shim PATH entry, including bracketed home paths", () => { + const homeRoot = temporaryDirectory("codex-cloud-shims-"); + const home = path.join(homeRoot, "home[1]"); + mkdirSync(home, { recursive: true }); const bashHome = bashPathEntry(home); const nodeVersion = "v24.19.0"; const nodeBin = path.join(home, ".nvm", "versions", "node", nodeVersion, "bin"); @@ -773,7 +782,12 @@ describe("Codex Cloud environment contract", () => { mkdirSync(nodeBin, { recursive: true }); for (const command of ["node", "npm", "npx"]) { const executable = path.join(nodeBin, command); - writeFileSync(executable, '#!/usr/bin/env bash\nprintf "%s\\n" "$PATH"\n'); + writeFileSync( + executable, + command === "node" + ? `#!/usr/bin/env bash\nif [[ "\${1:-}" = "-p" ]]; then printf "%s\\n" "${nodeVersion.slice(1)}"; else printf "%s\\n" "$PATH"; fi\n` + : '#!/usr/bin/env bash\nprintf "%s\\n" "$PATH"\n', + ); chmodSync(executable, 0o755); } writeFileSync( @@ -799,7 +813,52 @@ describe("Codex Cloud environment contract", () => { expect(resolvedPath[0]).toBe(bashNodeBin); expect(resolvedPath).not.toContain(shimDir); expect(resolvedPath).toEqual(expect.arrayContaining(["/usr/bin", "/bin"])); - }); + }, 120_000); + + it("only accepts a PATH fallback npm and npx that meet the repository npm range", () => { + const home = temporaryDirectory("codex-cloud-shim-npm-home-"); + const bashHome = bashPathEntry(home); + const nodeVersion = "v24.19.0"; + const nodeBin = path.join(home, ".nvm", "versions", "node", nodeVersion, "bin"); + mkdirSync(nodeBin, { recursive: true }); + // This Node install has no npm or npx, so the installer must look elsewhere on PATH. + const node = path.join(nodeBin, "node"); + writeFileSync( + node, + `#!/usr/bin/env bash\nif [[ "\${1:-}" = "-p" ]]; then printf "%s\\n" "${nodeVersion.slice(1)}"; fi\n`, + ); + chmodSync(node, 0o755); + writeFileSync( + path.join(home, ".clinical-kb-codex-cloud.sh"), + [`export NVM_DIR="${bashHome}/.nvm"`, `nvm() { printf '${nodeVersion}\\n'; }`, ""].join("\n"), + ); + + const installWithFallbackNpm = (npmVersion: string) => { + const fallbackBin = temporaryDirectory("codex-cloud-shim-npm-path-"); + for (const command of ["npm", "npx"]) { + const executable = path.join(fallbackBin, command); + writeFileSync(executable, `#!/usr/bin/env bash\nprintf "%s\\n" "${npmVersion}"\n`); + chmodSync(executable, 0o755); + } + return spawnSync(bashCommand, ["scripts/install-codex-cloud-command-shims.sh"], { + cwd: repoRoot, + encoding: "utf8", + env: { + ...process.env, + HOME: bashHome, + PATH: `${bashPathEntry(fallbackBin)}:${bashPathList(process.env.PATH || "")}`, + }, + }); + }; + + const older = installWithFallbackNpm("10.9.0"); + expect(older.status).toBe(1); + expect(older.stderr).toContain("npm 11.x is required"); + expect(older.stderr).toContain("10.9.0"); + + const supported = installWithFallbackNpm("11.17.0"); + expect(supported.status, supported.stderr || supported.stdout).toBe(0); + }, 120_000); it("prefers the exact Python 3.12 interpreter over an older python3 alias", () => { const home = temporaryDirectory("codex-cloud-python-home-"); @@ -822,7 +881,7 @@ describe("Codex Cloud environment contract", () => { expect(result.status, result.stderr || result.stdout).toBe(0); expect(result.stdout.trim()).toBe(bashPathEntry(exactPython)); - }); + }, 120_000); it("writes managed shell policy behaviorally and preserves unrelated Codex config", () => { const home = temporaryDirectory("codex-cloud-home-"); @@ -831,6 +890,17 @@ describe("Codex Cloud environment contract", () => { path.join(home, ".codex/config.toml"), ["[mcp_servers.example]", 'command = "echo"', 'args = ["ping"]', ""].join("\n"), ); + writeFileSync( + path.join(home, ".bashrc"), + [ + "# If not running interactively, don't do anything", + "case $- in", + " *i*) ;;", + " *) return;;", + "esac", + "", + ].join("\n"), + ); const first = runSetupPolicyOnly(home, { CODEX_CLOUD_ACCESS_PROFILE: "offline", @@ -850,9 +920,18 @@ describe("Codex Cloud environment contract", () => { expect(config.match(/^\[shell_environment_policy\]$/gm)).toHaveLength(1); const profile = readRuntimeProfile(home); + expect(profile).toContain("export CODEX_CLOUD_PROFILE_LOADED=1"); expect(profile).toContain('export CODEX_CLOUD_ACCESS_PROFILE="offline"'); expect(profile).toContain("export RAG_PROVIDER_MODE=offline"); + const bashrc = readFileSync(path.join(home, ".bashrc"), "utf8"); + expect(bashrc.indexOf(".clinical-kb-codex-cloud.sh")).toBeGreaterThanOrEqual(0); + expect(bashrc.indexOf(".clinical-kb-codex-cloud.sh")).toBeLessThan(bashrc.indexOf("return;;")); + + expect(shouldAutoSourceCloudProfile({ CODEX_CLOUD: "1", HOME: home })).toBe(true); + expect(shouldAutoSourceCloudProfile({ CODEX_CLOUD: "1", CODEX_CLOUD_PROFILE_LOADED: "1", HOME: home })).toBe(false); + expect(shouldAutoSourceCloudProfile({ CODEX_CLOUD: "1", NODE_ENV: "test", HOME: home })).toBe(false); + const second = runSetupPolicyOnly(home, { CODEX_CLOUD_ACCESS_PROFILE: "offline", }); diff --git a/tests/env.test.ts b/tests/env.test.ts index 2b5fca1588..0e08d9c06c 100644 --- a/tests/env.test.ts +++ b/tests/env.test.ts @@ -20,3 +20,24 @@ describe("developer-area access-key environment parsing", () => { } }); }); + +describe("deep-probe secret environment parsing", () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); + }); + + it("treats a blank secret as unset, as the offline test wrapper leaves it", async () => { + vi.stubEnv("HEALTH_DEEP_PROBE_SECRET", ""); + + await expect(import("@/lib/env")).resolves.toMatchObject({ + env: { HEALTH_DEEP_PROBE_SECRET: undefined }, + }); + }); + + it("still rejects a non-blank secret shorter than 16 characters", async () => { + vi.stubEnv("HEALTH_DEEP_PROBE_SECRET", "too-short"); + + await expect(import("@/lib/env")).rejects.toThrow(); + }); +});