From 1e0449d0ef6f2def38d29ec763a8a222aa342389 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 15:57:59 +0000 Subject: [PATCH 1/4] fix(codex-cloud): unbreak CI and close open review findings on #3587 - Treat a blank HEALTH_DEEP_PROBE_SECRET as unset. The offline test wrapper now blanks it, and the strict min(16) rule made every route importing env throw, which failed the unit-coverage and Playwright build jobs. A non-blank value must still be 16+ characters, and an unset secret keeps the deep probe closed. - Shim installer: require Node major 24 (22.x and 20.x no longer pass the fallback check), and skip $HOME/.local/bin in every PATH lookup so a shim can never resolve to itself. - Profile relocation: drop the old separator so reruns do not add blank lines. - Bring the contract checker and tests in line with the shim and profile changes already on this branch. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_016NaMorGX7ibPrednN1p5qA --- scripts/check-codex-cloud-setup.mjs | 2 +- scripts/install-codex-cloud-command-shims.sh | 20 ++++++++++--------- scripts/setup-codex-cloud.sh | 2 ++ src/lib/env.ts | 4 +++- tests/codex-cloud-setup.test.ts | 11 ++++++++-- tests/env.test.ts | 21 ++++++++++++++++++++ 6 files changed, 47 insertions(+), 13 deletions(-) diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index 8c3996827f..030d7dc88a 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -1087,7 +1087,7 @@ function validateMaintenanceAndCommandShims(sources, errors) { ]) { 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."); } } diff --git a/scripts/install-codex-cloud-command-shims.sh b/scripts/install-codex-cloud-command-shims.sh index 461e891f21..5bffc96f26 100644 --- a/scripts/install-codex-cloud-command-shims.sh +++ b/scripts/install-codex-cloud-command-shims.sh @@ -24,12 +24,20 @@ node_version_supported() { 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 < 25 )) || return 1 - (( actual_major > 24 )) && return 0 + (( 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:" +while [[ "$fallback_path" == *":$HOME/.local/bin:"* ]]; do + fallback_path="${fallback_path//:$HOME\/.local\/bin:/:}" +done +fallback_path="${fallback_path#:}" +fallback_path="${fallback_path%:}" + resolved_node_version="" if command -v nvm >/dev/null 2>&1; then resolved_node_version="$(nvm version "$expected_node_major" 2>/dev/null || true)" @@ -37,12 +45,6 @@ fi if [[ -n "$resolved_node_version" && "$resolved_node_version" != "N/A" ]]; then node_bin="$NVM_DIR/versions/node/$resolved_node_version/bin" else - fallback_path=":$PATH:" - while [[ "$fallback_path" == *":$HOME/.local/bin:"* ]]; do - fallback_path="${fallback_path//:$HOME\/.local\/bin:/:}" - done - fallback_path="${fallback_path#:}" - fallback_path="${fallback_path%:}" 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)" @@ -60,7 +62,7 @@ for command_name in node npm npx; do if [[ -x "$node_bin/$command_name" ]]; then command_path="$node_bin/$command_name" else - command_path="$(PATH="${fallback_path:-$PATH}" command -v "$command_name" 2>/dev/null || true)" + command_path="$(PATH="$fallback_path" command -v "$command_name" 2>/dev/null || true)" fi fi [[ -x "$command_path" ]] || fail "${command_name} is unavailable in Node ${expected_node_major}." diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index ec0e108534..21f291a57b 100755 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -274,6 +274,8 @@ for shell_profile in "$HOME/.bashrc" "$HOME/.profile" "$HOME/.bash_profile"; do profile_existing="$(<"$shell_profile")" profile_existing="$(printf '%s\n' "$profile_existing" | grep -Fvx '# PsychSift Codex Cloud runtime' || true)" profile_existing="$(printf '%s\n' "$profile_existing" | grep -Fvx "$profile_source" || true)" + # Drop the separator a previous run wrote after the block so reruns stay idempotent. + profile_existing="$(printf '%s\n' "$profile_existing" | sed '/./,$!d')" profile_candidate="$(mktemp "${shell_profile}.XXXXXX")" if [[ -n "$profile_existing" ]]; then printf '# PsychSift Codex Cloud runtime\n%s\n\n%s\n' "$profile_source" "$profile_existing" > "$profile_candidate" 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-setup.test.ts b/tests/codex-cloud-setup.test.ts index 942b264a44..cea039e916 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -690,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'); @@ -778,7 +780,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( 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(); + }); +}); From 702b5b9a93b09b5f2363d492b9dd53ce0565a187 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 16:14:25 +0000 Subject: [PATCH 2/4] fix(codex-cloud): stop shim PATH cleanup hanging on home paths with glob characters The loop condition matched "$HOME/.local/bin" literally, but the replacement treated $HOME as a pattern, so a home like /tmp/user[1] left the entry in place and the loop never ended. Quote the pattern, and add a regression test that hangs on the old code. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_016NaMorGX7ibPrednN1p5qA --- scripts/install-codex-cloud-command-shims.sh | 2 +- tests/codex-cloud-setup.test.ts | 38 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/scripts/install-codex-cloud-command-shims.sh b/scripts/install-codex-cloud-command-shims.sh index 5bffc96f26..cd58b4905a 100644 --- a/scripts/install-codex-cloud-command-shims.sh +++ b/scripts/install-codex-cloud-command-shims.sh @@ -33,7 +33,7 @@ node_version_supported() { # $HOME/.local/bin. fallback_path=":$PATH:" while [[ "$fallback_path" == *":$HOME/.local/bin:"* ]]; do - fallback_path="${fallback_path//:$HOME\/.local\/bin:/:}" + fallback_path="${fallback_path//":$HOME/.local/bin:"/:}" done fallback_path="${fallback_path#:}" fallback_path="${fallback_path%:}" diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index cea039e916..8ae395dc9f 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -813,6 +813,44 @@ describe("Codex Cloud environment contract", () => { expect(resolvedPath).toEqual(expect.arrayContaining(["/usr/bin", "/bin"])); }, 120_000); + it("finishes installing shims when the home path contains glob characters", () => { + // A home path such as /tmp/user[1] made the PATH cleanup loop spin forever, because the + // replacement treated the brackets as a pattern while the loop condition matched them literally. + const home = temporaryDirectory("codex-cloud-[shims]-"); + const bashHome = bashPathEntry(home); + const nodeVersion = "v24.19.0"; + const nodeBin = path.join(home, ".nvm", "versions", "node", nodeVersion, "bin"); + mkdirSync(nodeBin, { recursive: true }); + for (const command of ["node", "npm", "npx"]) { + const executable = path.join(nodeBin, command); + writeFileSync( + executable, + command === "node" + ? `#!/usr/bin/env bash\nif [[ "\${1:-}" = "-p" ]]; then printf "%s\\n" "${nodeVersion.slice(1)}"; fi\n` + : "#!/usr/bin/env bash\n", + ); + chmodSync(executable, 0o755); + } + writeFileSync( + path.join(home, ".clinical-kb-codex-cloud.sh"), + [`export NVM_DIR="${bashHome}/.nvm"`, `nvm() { printf '${nodeVersion}\\n'; }`, ""].join("\n"), + ); + + const install = spawnSync(bashCommand, ["scripts/install-codex-cloud-command-shims.sh"], { + cwd: repoRoot, + encoding: "utf8", + timeout: 30_000, + env: { + ...process.env, + HOME: bashHome, + PATH: `${bashHome}/.local/bin:${bashPathList(process.env.PATH || "")}`, + }, + }); + + expect(install.error).toBeUndefined(); + expect(install.status, install.stderr || install.stdout).toBe(0); + }, 60_000); + it("prefers the exact Python 3.12 interpreter over an older python3 alias", () => { const home = temporaryDirectory("codex-cloud-python-home-"); const bin = temporaryDirectory("codex-cloud-python-"); From c1d0d73c737f166a161c1fa560c0af5791b4537d Mon Sep 17 00:00:00 2001 From: "qodo-code-review[bot]" <151058649+qodo-code-review[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:18:34 +0000 Subject: [PATCH 3/4] fix: Remove shim PATH entries using literal comparison --- scripts/install-codex-cloud-command-shims.sh | 32 +++++++++---- tests/codex-cloud-setup.test.ts | 48 +++----------------- 2 files changed, 28 insertions(+), 52 deletions(-) diff --git a/scripts/install-codex-cloud-command-shims.sh b/scripts/install-codex-cloud-command-shims.sh index cd58b4905a..1afa584ffc 100644 --- a/scripts/install-codex-cloud-command-shims.sh +++ b/scripts/install-codex-cloud-command-shims.sh @@ -31,12 +31,18 @@ node_version_supported() { # Existing shims must never be picked as their own targets, so every PATH lookup below skips # $HOME/.local/bin. -fallback_path=":$PATH:" -while [[ "$fallback_path" == *":$HOME/.local/bin:"* ]]; do - fallback_path="${fallback_path//":$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 -fallback_path="${fallback_path#:}" -fallback_path="${fallback_path%:}" +unset path_entries path_entry path_entry_seen resolved_node_version="" if command -v nvm >/dev/null 2>&1; then @@ -72,12 +78,18 @@ set -Eeuo pipefail # Generated by scripts/install-codex-cloud-command-shims.sh. Re-running setup # or maintenance replaces this wrapper through the same idempotent command. . "$runtime_profile" -clean_path=":\$PATH:" -while [[ "\$clean_path" == *":\$HOME/.local/bin:"* ]]; do - clean_path="\${clean_path//:\$HOME\/.local\/bin:/:}" +clean_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 + clean_path+=: + fi + clean_path+="\$path_entry" + path_entry_seen=true done -clean_path="\${clean_path#:}" -clean_path="\${clean_path%:}" +unset path_entries path_entry path_entry_seen if [[ -n "\$clean_path" ]]; then export PATH="$node_bin:\$clean_path" else diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index 8ae395dc9f..d67f9836c1 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -755,8 +755,8 @@ describe("Codex Cloud environment contract", () => { } 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"'); @@ -771,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"); @@ -813,44 +815,6 @@ describe("Codex Cloud environment contract", () => { expect(resolvedPath).toEqual(expect.arrayContaining(["/usr/bin", "/bin"])); }, 120_000); - it("finishes installing shims when the home path contains glob characters", () => { - // A home path such as /tmp/user[1] made the PATH cleanup loop spin forever, because the - // replacement treated the brackets as a pattern while the loop condition matched them literally. - const home = temporaryDirectory("codex-cloud-[shims]-"); - const bashHome = bashPathEntry(home); - const nodeVersion = "v24.19.0"; - const nodeBin = path.join(home, ".nvm", "versions", "node", nodeVersion, "bin"); - mkdirSync(nodeBin, { recursive: true }); - for (const command of ["node", "npm", "npx"]) { - const executable = path.join(nodeBin, command); - writeFileSync( - executable, - command === "node" - ? `#!/usr/bin/env bash\nif [[ "\${1:-}" = "-p" ]]; then printf "%s\\n" "${nodeVersion.slice(1)}"; fi\n` - : "#!/usr/bin/env bash\n", - ); - chmodSync(executable, 0o755); - } - writeFileSync( - path.join(home, ".clinical-kb-codex-cloud.sh"), - [`export NVM_DIR="${bashHome}/.nvm"`, `nvm() { printf '${nodeVersion}\\n'; }`, ""].join("\n"), - ); - - const install = spawnSync(bashCommand, ["scripts/install-codex-cloud-command-shims.sh"], { - cwd: repoRoot, - encoding: "utf8", - timeout: 30_000, - env: { - ...process.env, - HOME: bashHome, - PATH: `${bashHome}/.local/bin:${bashPathList(process.env.PATH || "")}`, - }, - }); - - expect(install.error).toBeUndefined(); - expect(install.status, install.stderr || install.stdout).toBe(0); - }, 60_000); - it("prefers the exact Python 3.12 interpreter over an older python3 alias", () => { const home = temporaryDirectory("codex-cloud-python-home-"); const bin = temporaryDirectory("codex-cloud-python-"); From 14aa2a5624e51d5c2fdb4b3e84ae61727c86ce32 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 16:25:03 +0000 Subject: [PATCH 4/4] fix(codex-cloud): match the literal-comparison PATH cleanup in the setup contract check The shim PATH cleanup now compares each entry literally (no glob replacement), so the static contract check looks for that form instead of the old replacement text. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_016NaMorGX7ibPrednN1p5qA --- scripts/check-codex-cloud-setup.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index 030d7dc88a..78dc144b46 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -1081,7 +1081,10 @@ 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."], ]) {