From 2ab6b2f5c63841f0d82fc35223f5d7f62a69717f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:07:31 +0800 Subject: [PATCH 1/6] fix(codex): authenticate cloud GitHub workflow --- docs/codex-cloud.md | 152 ++++++++++-------- docs/scripts-index.md | 2 +- scripts/check-codex-cloud-setup.mjs | 63 +++++++- scripts/configure-codex-cloud-github-shell.sh | 74 +++++++++ scripts/install-codex-cloud-command-shims.sh | 10 +- scripts/maintain-codex-cloud.sh | 6 + scripts/refresh-codex-cloud-base.sh | 31 ++++ scripts/setup-codex-cloud.sh | 16 ++ tests/codex-cloud-setup.test.ts | 30 +++- 9 files changed, 314 insertions(+), 70 deletions(-) create mode 100755 scripts/configure-codex-cloud-github-shell.sh create mode 100755 scripts/refresh-codex-cloud-base.sh diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index cc925e54b..94a707fc1 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -39,12 +39,13 @@ contract: | Maintenance command | `bash scripts/maintain-codex-cloud.sh && bash scripts/install-codex-cloud-command-shims.sh` | | Environment variables | Use the complete profile below | -Keep agent internet access off for this repository's ordinary Cloud environments. Package -installation happens during setup; ordinary structure-only work, including the RAG -decomposition prompt below, remains offline. The appended command-shim installer is required: +Keep agent internet access off for the offline environment. In `Database - connected`, allow only +`github.com` and `api.github.com` so authenticated GitHub CLI and Git transport can reach the +repository without opening unrelated provider access. Package installation happens during setup. +The appended command-shim installer is required: it makes every normal `node`, `npm`, and `npx` invocation load the generated sanitized -profile before starting Node. It is idempotent and uses `nvm which` rather than -`command -v node`, so maintenance cannot accidentally wrap an earlier wrapper. +profile before starting Node. It is idempotent and builds the executable path from `nvm version` +instead of resolving through `PATH`, so maintenance cannot accidentally wrap an earlier wrapper. The setup command fails if the required Cloud toolchain cannot be installed. It intentionally does not install Railway CLI: hosted Railway access comes from the authenticated workspace app, and the @@ -63,26 +64,26 @@ development, but it cannot reproduce capabilities owned by the host, an operator hardware. Use this matrix when deciding whether a failed task needs a repository fix or a different execution environment. -| Capability | Codex Cloud | Local/Desktop difference | -| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| Node and npm | Exact Node 24 range and npm 11 version; locked dev install | Desktop can reuse a byte-identical `node_modules` tree from another worktree, while a fresh Cloud image must populate its own cache | -| Dependency install | `npm ci --include=dev --prefer-offline --no-audit --no-fund`; integrity is checked after install | Local reuse is faster when another complete worktree exists; Cloud cache availability depends on the disposable host image | -| Git hooks and repository checks | Installed by npm postinstall; the same format, lint, type, unit, build, and repository gates are available | Desktop shell Git credentials may support direct push; Cloud publication normally uses the native GitHub connector or PR controls | -| Browser testing | Matching Chromium, Firefox, and WebKit are installed and launch-tested unless the environment is explicitly source-only | Cloud can run Playwright, but it cannot replace physical Safari, installed-PWA, camera, touch, GPU, or device-specific acceptance | -| Worker/OCR tooling | Python 3.12 hashed lock, PyMuPDF, Pillow, pytesseract, medspaCy, spaCy, and Tesseract are installed and checked | Production workers use their separate Python 3.11 lock; local operators may have additional native inspection tools | -| Deno and Codex CLI | Deno 2 and the reviewed Codex CLI version are installed and checked | Local CLI configuration can enable operator-owned MCP servers; tracked Cloud setup deliberately does not copy or enable them | -| Application runtime | Demo/offline app and browser journeys can run through `npm run ensure` | Authenticated or production-like behavior needs approved provider access; Cloud demo health is not production readiness | -| Provider access | Offline by default; connected capability comes from separately installed host OAuth apps and explicit approvals | A trusted local operator can use separately managed provider CLIs or credentials, subject to the same approval and safety rules | -| GitHub operations | Repository reads/publication depend on the Codex GitHub installation and the tools exposed to the task | Review-thread management, Actions reruns, admin APIs, and shell Git authentication may be unavailable even when connector publication works | -| Containers and privileged host changes | Repository checks do not assume a durable Docker daemon, nested virtualization, swap, or persistent system state | A local workstation or CI runner can provide Docker, larger disks, persistent caches, device access, and operator-managed capacity | -| Persistence | Repository commits survive when published; home-directory caches and installed tools may be discarded with the environment | Local worktrees, caches, browser state, OAuth sessions, and tool configuration can persist between sessions | +| Capability | Codex Cloud | Local/Desktop difference | +| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Node and npm | Exact Node 24 range and npm 11 version; locked dev install | Desktop can reuse a byte-identical `node_modules` tree from another worktree, while a fresh Cloud image must populate its own cache | +| Dependency install | `npm ci --include=dev --prefer-offline --no-audit --no-fund`; integrity is checked after install | Local reuse is faster when another complete worktree exists; Cloud cache availability depends on the disposable host image | +| Git hooks and repository checks | Installed by npm postinstall; the same format, lint, type, unit, build, and repository gates are available | Desktop shell Git credentials may support direct push; connected Cloud uses the native connector when available or the authenticated `gh` fallback | +| Browser testing | Matching Chromium, Firefox, and WebKit are installed and launch-tested unless the environment is explicitly source-only | Cloud can run Playwright, but it cannot replace physical Safari, installed-PWA, camera, touch, GPU, or device-specific acceptance | +| Worker/OCR tooling | Python 3.12 hashed lock, PyMuPDF, Pillow, pytesseract, medspaCy, spaCy, and Tesseract are installed and checked | Production workers use their separate Python 3.11 lock; local operators may have additional native inspection tools | +| Deno and Codex CLI | Deno 2 and the reviewed Codex CLI version are installed and checked | Local CLI configuration can enable operator-owned MCP servers; tracked Cloud setup deliberately does not copy or enable them | +| Application runtime | Demo/offline app and browser journeys can run through `npm run ensure` | Authenticated or production-like behavior needs approved provider access; Cloud demo health is not production readiness | +| Provider access | Offline by default; connected capability comes from separately installed host OAuth apps and explicit approvals | A trusted local operator can use separately managed provider CLIs or credentials, subject to the same approval and safety rules | +| GitHub operations | Native connector first; the connected environment provisions authenticated `gh` when required APIs are not injected | The fallback covers PR/review-thread/checks/Actions APIs and ordinary non-force feature-branch Git transport; repository policy still prohibits protected-branch and PR lifecycle mutations | +| Containers and privileged host changes | Repository checks do not assume a durable Docker daemon, nested virtualization, swap, or persistent system state | A local workstation or CI runner can provide Docker, larger disks, persistent caches, device access, and operator-managed capacity | +| Persistence | Repository commits survive when published; home-directory caches and installed tools may be discarded with the environment | Local worktrees, caches, browser state, OAuth sessions, and tool configuration can persist between sessions | The remaining parity gaps cannot safely be fixed by placing more credentials or mutable provider state in the Cloud shell. The practical improvement path is: 1. Keep repository-owned runtimes, locks, browsers, diagnostics, and checks reproducible here. -2. Use native GitHub publication and verify the exact remote branch and commit rather than assuming - a local commit or metadata-only PR response was published. +2. Prefer native GitHub publication. When the fresh-task tool inventory lacks required APIs, use + the connected environment's authenticated `gh` fallback and verify every remote mutation. 3. Add provider capabilities only through a separate connected environment with least-privileged OAuth and explicit task authorization. 4. Keep authenticated Supabase/OpenAI tests and write-capable Railway operations in protected @@ -154,13 +155,14 @@ PLAYWRIGHT_OFFLINE_MODE=true Keep OpenAI disabled unless a later task explicitly authorizes it. Do not add provider keys, tokens, database URLs, service-role values, E2E credentials, or `ALLOW_PROVIDER_TESTS` to this -environment. The generated agent profile removes the complete provider-variable inventory in +environment as ordinary variables. The sole GitHub fallback exception is the encrypted, +setup-only Secret `CODEX_CLOUD_GITHUB_PAT`, described below. The generated agent profile removes +the complete provider-variable inventory in both access profiles. Connected access records authorization intent and keeps the GitHub boundary explicit, but it does not register hosted MCP apps, expose raw credentials to the shell, or guarantee -that every GitHub capability appears as a direct agent tool. For ordinary Cloud task publishing, -use the native Cloud diff/PR controls and verify the resulting GitHub branch and PR link. A -metadata-only `make_pr` response is not publication evidence. If a requested GitHub API is not -available, report that limitation rather than using shell credentials as a workaround. +that every GitHub capability appears as a direct agent tool. Prefer native Cloud tools when they are +actually present. Otherwise use the verified GitHub CLI fallback and verify the resulting remote +state. A metadata-only `make_pr` response is not publication evidence. Codex Cloud secrets and ordinary environment variables have different exposure and lifecycle properties. This repository has no mechanism that promotes setup-only OpenAI, Supabase, E2E, @@ -184,15 +186,37 @@ diff/PR controls and verify the returned GitHub branch and pull-request link. Do that GitHub is unavailable merely because `gh`, shell Git credentials, or a particular direct agent tool are absent. The intended GitHub identity is `BigSimmo`. Use repository write access for branch and pull-request publication; reserve administrator access for separately -approved operations. Some GitHub APIs, including review-thread or Actions management, -may not be exposed in every Cloud task; use an approved GitHub-connected workflow for those -operations or report the unavailable capability. Do not use shell credentials as a workaround. - -GitHub connector permission is separate from credentials inside the agent shell. The connector, -native Push control, and GitHub UI are the supported Cloud publication and cleanup paths. Cloud -secrets are setup-only, so `CODEX_CLOUD_GITHUB_PAT` cannot safely support an agent-phase helper; -the name is explicitly excluded by the shell policy and tested with the rest of the credential -inventory. +approved operations. Some GitHub APIs, including review-thread or Actions management, may not be +exposed in every Cloud task. When the fresh-task inventory proves that gap, the connected +environment uses the supported GitHub CLI fallback below rather than treating an unverified +connector as authenticated. + +GitHub connector permission is separate from credentials inside the agent shell. Configure the +native connector first. If a fresh task receives no connector tools for review threads, failed-job +logs, Actions reruns, or branch publication, add `CODEX_CLOUD_GITHUB_PAT` as an encrypted **Secret** +in `Database - connected`, never as an ordinary environment variable. Prefer a fine-grained token +owned by `BigSimmo`, restricted to `BigSimmo/Database`, with only Metadata read, Contents read/write, +Pull requests read/write, Issues read/write, Actions read/write, and Checks read. Do not grant +Administration, Deployments, Environments, Secrets, Webhooks, organization administration, or +unrelated repositories. + +[GitHub CLI documents](https://cli.github.com/manual/gh_auth_login) that fine-grained tokens passed +through `--with-token` can behave confusingly outside their selected resources. That is why setup +validates this exact repository and the fresh task must exercise the complete read surface before +any mutation; `gh auth status` alone is not acceptance evidence. + +Codex exposes Secrets only during setup. `configure-codex-cloud-github-shell.sh` sends the secret to +`gh auth login` over standard input, immediately unsets all token variables, restricts the standard +GitHub CLI credential store to the current user, verifies identity `BigSimmo` and repository push +permission, and configures the token-free Git credential helper. The generated runtime profile and +Codex shell policy continue to exclude the original secret and token variables. No credential is +written to the checkout, remote URL, task prompt, or logs. Changing a Secret invalidates the setup +cache; start a new task and run the acceptance checks below. + +GitHub's repository permission model does not provide separate token switches for every prohibited +workflow action. The `Run PR` policy therefore remains authoritative: never merge or close a PR, +push to `main`, `master`, `develop`, or `release/*`, delete or rename a branch, force-push, rewrite +history, or run deployment/provider operations. Branch protections remain the remote backstop. `bash scripts/delete-codex-cloud-branch-with-pat.sh ` is retained only for an explicitly authorised operator running outside Codex Cloud. It rejects `CODEX_CLOUD=1`, @@ -202,24 +226,24 @@ remote URL, cache, or log. Setup restores a missing `origin` to the credential-free URL `https://github.com/BigSimmo/Database.git`; it preserves an existing correct remote and fails -instead of overwriting a wrong or credential-bearing remote. When GitHub CLI authentication is -already available, setup asks `gh auth setup-git` to install its token-free helper command. It -never embeds a token or invents a PAT. `git ls-remote` and a dry-run push remain separate -acceptance checks; if the connector does not expose shell Git authentication, report that -platform capability gap. +instead of overwriting a wrong or credential-bearing remote. Setup installs `gh` only when the +connected fallback needs it and asks `gh auth setup-git` to install its token-free helper command. +It also fetches `origin/main`, stores the current task's merge base outside the checkout, and exports +that exact 40-character SHA as `CODEX_CLOUD_EXPECTED_BASE_SHA` in subsequent agent shells. `git +ls-remote` and a non-mutating dry-run push remain separate acceptance checks. Suggested GitHub acceptance task: ```text -Read AGENTS.md and docs/codex-cloud.md. Create a task-specific branch, add one harmless -documentation-only line, commit it, and record its branch name and full 40-character HEAD SHA. -Use the Codex GitHub workflow to publish that exact existing branch and create a draft pull -request. Do not recreate, rename, amend, rebase, or rebuild the branch or commit. Do not merge. -Report whether repository clone, branch publication, and draft PR creation each succeeded, then -report the expected and published branch names and full HEAD SHAs and whether they match exactly. -If branch publication or draft PR creation succeeds, include its link. For each failed write, -report the failure and state that no link is available. Remove the draft branch/PR only after I -approve cleanup. +Read AGENTS.md, docs/codex-cloud.md, docs/codex-review-protocol.md, and the Run PR skill. Confirm +the GitHub identity and BigSimmo/Database permission, list open PRs, and inspect one PR's exact +head SHA, labels, draft/mergeability state, checks, reviews, comments, unresolved-thread count, +workflow jobs, and bounded failed-job logs when applicable. Confirm reply/resolve and failed-job +rerun API availability without synthetic mutations. Run git ls-remote and an ordinary push +--dry-run against the current authorized non-protected branch; do not create an artificial commit. +Run npm run check:github-shell-access:live and verify every remote mutation that is genuinely +needed. Never merge/close a PR, push a protected branch, delete/rename a branch, rebase, rewrite +history, force-push, deploy, or call OpenAI/Supabase. ``` ## Setup and maintenance @@ -270,8 +294,8 @@ Run this in a fresh Cloud task before relying on the environment: Read all applicable AGENTS.md files and docs/codex-cloud.md. State whether this is the offline or connected profile. Report tool versions without printing environment values. Run npm run check:codex-cloud, npm run check:runtime, -npm run check:installed-lock-parity, and set CODEX_CLOUD_EXPECTED_BASE_SHA to the intended -merge/base commit before running npm run check:codex-cloud -- --runtime. Do not call a +npm run check:installed-lock-parity, and npm run check:codex-cloud -- --runtime. Confirm that +setup exported CODEX_CLOUD_EXPECTED_BASE_SHA as a verified 40-character ancestor. Do not call a provider unless this task explicitly names and authorizes that provider. Report the decisive line from every command and any unrun check. ``` @@ -295,7 +319,8 @@ the full current HEAD, local main and origin/main when present, expected base, a and a separate freshness state. Setup and maintenance use the process-local `CODEX_CLOUD_PROVISIONING=1` flag so an unavoidable task-only checkout reports `freshness=unverified` without entering a repair loop. The explicit acceptance command does -not set that flag and fails until `CODEX_CLOUD_EXPECTED_BASE_SHA` proves the intended base. +not set that flag and fails unless the setup-generated `CODEX_CLOUD_EXPECTED_BASE_SHA` proves the +intended base. MCP inspection emits server names, commands, and environment variable names only. A repository cannot remove a variable already inherited by the top-level task process. Before @@ -450,13 +475,14 @@ copying credentials into the checkout. 4. **Prove the shell boundary before providers.** First run the direct raw-shell command above before profiles or command shims. Then run `npm run check:codex-cloud`, `npm run check:codex-cloud -- --runtime`, `npm run check:runtime`, and - `npm run check:installed-lock-parity`. Set `CODEX_CLOUD_EXPECTED_BASE_SHA` to the intended - merged base commit. Require the raw PASS line, both Cloud PASS lines, correct runtime/lock + `npm run check:installed-lock-parity`. Confirm the setup-generated + `CODEX_CLOUD_EXPECTED_BASE_SHA` is a full intended merge-base commit. Require the raw PASS line, + both Cloud PASS lines, correct runtime/lock parity, `CODEX_CLOUD_ACCESS_PROFILE=connected`, no provider variable reported present, and a credential-free matching origin. Repository MCP metadata is configuration evidence only. -5. **Prove each provider read-only.** Use the tools exposed by the fresh host session, not shell - tokens. For GitHub, read repository metadata and confirm `BigSimmo/Database` plus the intended - identity. For Railway, read workspace/project/service metadata and confirm the IDs above without +5. **Prove each provider with its approved route.** Prefer tools exposed by the fresh host session. + If GitHub tools are absent, use the authenticated `gh` fallback and confirm `BigSimmo/Database` + plus the intended identity and repository permission. For Railway, read workspace/project/service metadata and confirm the IDs above without triggering a deployment. Record the exact Railway inventory. Set the app to allow reads and ask before changes. The Personal Pro global read-versus-change control does not provide tool-level RBAC. If the account later moves to Enterprise/Edu, an admin may additionally allow only @@ -466,13 +492,13 @@ copying credentials into the checkout. without querying clinical row contents. Report only non-secret identity and status metadata. OpenAI has no generic connected-profile credential: leave `RAG_PROVIDER_MODE=offline` until a separately approved paid canary or protected workflow supplies its own credential boundary. -6. **Publish a task branch safely.** Work on a task-specific non-protected branch. Format, stage, and commit - the intended repository change, then publish that exact existing commit through the native GitHub - connector/Cloud PR workflow, and verify the remote branch and PR link. If shell Git - authentication is intentionally available, `git push --set-upstream origin ` is - acceptable after confirming the credential-free origin; otherwise a failed `git ls-remote` is - not repaired with an embedded PAT. Do not publish a generic detached `work` branch merely to - remove an informational "no upstream" warning. +6. **Publish a task branch safely.** Work on a task-specific non-protected branch. Format, stage, + and commit the intended repository change, then publish that exact existing commit through the + native GitHub connector or authenticated GitHub CLI fallback, and verify the remote branch and + PR link. `git push --set-upstream origin ` is acceptable only after confirming the + credential-free origin and protected-branch exclusions. Never embed a PAT in a remote or command. + Do not publish a generic detached `work` branch merely to remove an informational "no upstream" + warning. 7. **Separate local app proof from live readiness.** Run `npm run ensure`, use the printed URL, and confirm `/api/local-project-id` before checking `/api/health`. In demo mode, an HTTP 503 with missing Supabase configuration and skipped OpenAI is expected and must not be relabelled healthy. diff --git a/docs/scripts-index.md b/docs/scripts-index.md index 4ed722104..c6204837a 100644 --- a/docs/scripts-index.md +++ b/docs/scripts-index.md @@ -1,6 +1,6 @@ # Scripts index -Curated map of `scripts/` (225 files) and the `package.json` script surface (236 entries), +Curated map of `scripts/` (227 files) and the `package.json` script surface (236 entries), grouped by purpose. This is orientation, not an exhaustive per-file listing — the authoritative command list is `package.json`, and `npm run docs:check-scripts` verifies every `npm run ` referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above. diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index 2b05e564c..b0ddc42c4 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -555,7 +555,7 @@ export function sanitizedCloudCapabilityLines(env = process.env, options = {}) { ]; for (const name of providerCredentialVariables) lines.push(`${name}.present=${Boolean(env[name])}`); lines.push(hostedAppInventoryCapabilityLine(options.hostedAppInventory ?? null)); - lines.push("provider_route.github=codex-native-connector"); + lines.push("provider_route.github=codex-native-connector-or-authenticated-gh"); lines.push("provider_route.railway=chatgpt-official-app"); lines.push("provider_route.supabase=chatgpt-project-scoped-read-only-app"); lines.push(`codex.cli_available=${codexCliAvailable}`); @@ -752,6 +752,8 @@ export function validateCodexCloudSetup() { const setup = read("scripts/setup-codex-cloud.sh"); const maintenance = read("scripts/maintain-codex-cloud.sh"); const commandShims = read("scripts/install-codex-cloud-command-shims.sh"); + const githubShellSetup = read("scripts/configure-codex-cloud-github-shell.sh"); + const checkoutBaseRefresh = read("scripts/refresh-codex-cloud-base.sh"); const rawEnvironmentProbe = read("scripts/check-codex-cloud-raw-env.sh"); const patDelete = read("scripts/delete-codex-cloud-branch-with-pat.sh"); const guide = read("docs/codex-cloud.md"); @@ -798,7 +800,12 @@ export function validateCodexCloudSetup() { [/unset OPENAI_API_KEY/, "Cloud setup must remove raw provider variables from the agent shell."], [/\.bash_profile/, "Cloud setup must cover Bash login-profile precedence."], [/@openai\/codex/, "Cloud setup must install the Codex CLI."], + [ + /configure-codex-cloud-github-shell\.sh/, + "Cloud setup must provision the authenticated GitHub shell fallback before scrubbing setup secrets.", + ], [/ensure-codex-cloud-git-remote\.mjs/, "Cloud setup must restore a safe origin remote."], + [/refresh-codex-cloud-base\.sh/, "Cloud setup must refresh and pin the task checkout base."], [/check:codex-cloud -- --runtime/, "Cloud setup must run runtime acceptance."], [ /BEGIN clinical-kb-codex-cloud shell policy/, @@ -868,11 +875,29 @@ export function validateCodexCloudSetup() { /ensure-codex-cloud-git-remote\.mjs/, "Maintenance must preserve the safe origin remote.", ); + requireMatch( + errors, + maintenance, + /configure-codex-cloud-github-shell\.sh/, + "Maintenance must repair authenticated GitHub shell access before scrubbing setup secrets.", + ); + requireMatch( + errors, + maintenance, + /refresh-codex-cloud-base\.sh/, + "Maintenance must refresh and pin the task checkout base.", + ); + requireMatch( + errors, + commandShims, + /nvm version/, + "Cloud command shims must resolve the selected Node version without following their own wrappers.", + ); requireMatch( errors, commandShims, - /nvm which/, - "Cloud command shims must resolve the selected Node version through nvm.", + /clean_path=.*\.local.*bin/, + "Cloud command shims must remove their directory before running child npm scripts.", ); requireMatch( errors, @@ -889,6 +914,38 @@ export function validateCodexCloudSetup() { if (!commandShims.includes('exec "$node_bin/$command_name" "\\$@"')) { errors.push("Cloud command shims must execute absolute Node commands."); } + for (const [pattern, message] of [ + [/set \+x/, "GitHub shell setup must disable shell tracing before reading the setup credential."], + [/--with-token/, "GitHub shell setup must pass the setup credential through GitHub CLI standard input."], + [ + /GH_CONFIG_DIR="\$HOME\/\.config\/gh"/, + "GitHub shell setup must keep its credential store outside the repository.", + ], + [ + /unset CODEX_CLOUD_GITHUB_PAT GH_TOKEN GITHUB_TOKEN/, + "GitHub shell setup must scrub token variables immediately after staging authentication input.", + ], + [/gh api user --jq \.login/, "GitHub shell setup must verify the authenticated identity."], + [/repos\/\$repository/, "GitHub shell setup must verify repository-specific push permission."], + [/gh auth setup-git/, "GitHub shell setup must configure a token-free Git credential helper."], + ]) { + requireMatch(errors, githubShellSetup, pattern, message); + } + if (/printf[^\n]*(?:CODEX_CLOUD_GITHUB_PAT|GH_TOKEN|GITHUB_TOKEN)/.test(githubShellSetup)) { + errors.push("GitHub shell setup must never print a credential variable."); + } + requireMatch( + errors, + checkoutBaseRefresh, + /git merge-base HEAD refs\/remotes\/origin\/main/, + "Checkout-base refresh must pin the merge base shared by the task and origin/main.", + ); + requireMatch( + errors, + checkoutBaseRefresh, + /cloud-expected-base-sha/, + "Checkout-base refresh must persist the verified base outside the repository.", + ); requireMatch( errors, patDelete, diff --git a/scripts/configure-codex-cloud-github-shell.sh b/scripts/configure-codex-cloud-github-shell.sh new file mode 100755 index 000000000..3031217cd --- /dev/null +++ b/scripts/configure-codex-cloud-github-shell.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail +set +x + +log() { + printf '[codex-cloud:github] %s\n' "$*" +} + +fail() { + printf '[codex-cloud:github] ERROR: %s\n' "$*" >&2 + exit 1 +} + +repository="BigSimmo/Database" +expected_identity="BigSimmo" +export GH_CONFIG_DIR="$HOME/.config/gh" + +[[ "${CODEX_CLOUD:-0}" = "1" ]] || fail "This helper is only supported during Codex Cloud setup or maintenance." + +install_github_cli() { + command -v apt-get >/dev/null 2>&1 || fail "GitHub CLI is unavailable and apt-get cannot install it." + log "Installing GitHub CLI from the Cloud image package repository." + if [[ "$(id -u)" -eq 0 ]]; then + apt-get update + apt-get install -y --no-install-recommends gh + elif command -v sudo >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y --no-install-recommends gh + else + fail "GitHub CLI installation requires root or sudo." + fi +} + +validate_github_access() { + local identity repository_permission + identity="$(gh api user --jq .login 2>/dev/null)" || fail "GitHub identity verification failed." + [[ "$identity" = "$expected_identity" ]] || fail "GitHub identity must be ${expected_identity}; detected ${identity:-unavailable}." + + repository_permission="$(gh api "repos/$repository" --jq '.permissions.push // false' 2>/dev/null)" || + fail "GitHub repository permission verification failed for $repository." + [[ "$repository_permission" = "true" ]] || fail "GitHub access to $repository does not permit ordinary feature-branch pushes." + + gh auth setup-git --hostname github.com >/dev/null || fail "GitHub CLI could not configure its token-free Git credential helper." + log "PASS: identity=$identity repository=$repository feature_branch_push=true" +} + +if [[ -z "${CODEX_CLOUD_GITHUB_PAT:-}" ]]; then + if ! command -v gh >/dev/null 2>&1 || ! gh auth status --hostname github.com >/dev/null 2>&1; then + log "SKIP: no setup-only GitHub credential is available; a separately verified native connector may still provide access." + exit 0 + fi + validate_github_access + exit 0 +fi + +[[ "${CODEX_CLOUD_ACCESS_PROFILE:-offline}" = "connected" ]] || + fail "CODEX_CLOUD_GITHUB_PAT may only be consumed by the connected Cloud access profile." + +command -v gh >/dev/null 2>&1 || install_github_cli + +github_auth_status=0 +gh auth login --hostname github.com --git-protocol https --with-token <<< "$CODEX_CLOUD_GITHUB_PAT" >/dev/null || + github_auth_status=$? +unset CODEX_CLOUD_GITHUB_PAT GH_TOKEN GITHUB_TOKEN +[[ "$github_auth_status" -eq 0 ]] || fail "GitHub CLI authentication failed." +unset github_auth_status + +if [[ -d "$GH_CONFIG_DIR" ]]; then + chmod 0700 "$GH_CONFIG_DIR" + [[ ! -f "$GH_CONFIG_DIR/hosts.yml" ]] || chmod 0600 "$GH_CONFIG_DIR/hosts.yml" +fi + +validate_github_access diff --git a/scripts/install-codex-cloud-command-shims.sh b/scripts/install-codex-cloud-command-shims.sh index 6a473bb4b..7eda67545 100644 --- a/scripts/install-codex-cloud-command-shims.sh +++ b/scripts/install-codex-cloud-command-shims.sh @@ -20,7 +20,9 @@ runtime_profile="$HOME/.clinical-kb-codex-cloud.sh" source "$runtime_profile" command -v nvm >/dev/null 2>&1 || fail "nvm is unavailable after loading the Cloud runtime profile." -node_bin="$(dirname "$(nvm which "$expected_node_major")")" +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" 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}." @@ -30,6 +32,12 @@ 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:" +clean_path="\${clean_path//:\$HOME\/.local\/bin:/:}" +clean_path="\${clean_path#:}" +clean_path="\${clean_path%:}" +export PATH="$node_bin:\$clean_path" +unset clean_path exec "$node_bin/$command_name" "\$@" EOF chmod 0755 "$HOME/.local/bin/$command_name" diff --git a/scripts/maintain-codex-cloud.sh b/scripts/maintain-codex-cloud.sh index c85fdb152..48d5c06ce 100644 --- a/scripts/maintain-codex-cloud.sh +++ b/scripts/maintain-codex-cloud.sh @@ -8,6 +8,9 @@ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || { } cd "$repo_root" +export CODEX_CLOUD=1 +bash scripts/configure-codex-cloud-github-shell.sh + if [[ -f "$HOME/.clinical-kb-codex-cloud.sh" ]]; then # shellcheck source=/dev/null source "$HOME/.clinical-kb-codex-cloud.sh" @@ -19,6 +22,9 @@ if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then fi node scripts/ensure-codex-cloud-git-remote.mjs --configure-gh-helper +bash scripts/refresh-codex-cloud-base.sh +# shellcheck source=/dev/null +source "$HOME/.clinical-kb-codex-cloud.sh" npm run check:codex-cloud if ! CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime; then printf '[codex-cloud:maintenance] Runtime or toolchain drift detected; rerunning full setup.\n' diff --git a/scripts/refresh-codex-cloud-base.sh b/scripts/refresh-codex-cloud-base.sh new file mode 100755 index 000000000..524947fb9 --- /dev/null +++ b/scripts/refresh-codex-cloud-base.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +fail() { + printf '[codex-cloud:base] ERROR: %s\n' "$*" >&2 + exit 1 +} + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || fail "Run this script from the Database repository." +cd "$repo_root" + +git fetch --quiet --no-tags origin '+refs/heads/main:refs/remotes/origin/main' || + fail "Could not refresh origin/main." + +expected_base="$(git merge-base HEAD refs/remotes/origin/main 2>/dev/null)" || + fail "Could not determine the checkout merge base with origin/main." +[[ "$expected_base" =~ ^[0-9a-f]{40}$ ]] || fail "The checkout merge base is not a full Git commit SHA." + +cache_dir="$HOME/.cache/clinical-kb-codex" +expected_base_file="$cache_dir/cloud-expected-base-sha" +mkdir -p "$cache_dir" +chmod 0700 "$cache_dir" +expected_base_candidate="$(mktemp "$cache_dir/.cloud-expected-base-sha.XXXXXX")" +trap 'rm -f "$expected_base_candidate"' EXIT +printf '%s\n' "$expected_base" > "$expected_base_candidate" +chmod 0600 "$expected_base_candidate" +mv -f "$expected_base_candidate" "$expected_base_file" +trap - EXIT + +printf '[codex-cloud:base] PASS: expected_base=%s origin_main_refreshed=true\n' "$expected_base" diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index 80c88911b..562e9504a 100644 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -135,6 +135,14 @@ export CODEX_CLOUD=1 export CODEX_CLOUD_ACCESS_PROFILE="${access_profile}" export NEXT_PUBLIC_DEMO_MODE="\${NEXT_PUBLIC_DEMO_MODE:-true}" export PLAYWRIGHT_OFFLINE_MODE="\${PLAYWRIGHT_OFFLINE_MODE:-true}" +cloud_expected_base_file="\$HOME/.cache/clinical-kb-codex/cloud-expected-base-sha" +if [ -r "\$cloud_expected_base_file" ]; then + IFS= read -r cloud_expected_base < "\$cloud_expected_base_file" || true + if [[ "\$cloud_expected_base" =~ ^[0-9a-f]{40}\$ ]]; then + export CODEX_CLOUD_EXPECTED_BASE_SHA="\$cloud_expected_base" + fi +fi +unset cloud_expected_base cloud_expected_base_file unset npm_config_http_proxy npm_config_https_proxy npm_config_proxy # Connected access is provided by host-installed, OAuth-backed apps, never by # repository MCP registration or raw provider variables in the agent shell. @@ -252,6 +260,9 @@ for shell_profile in "$HOME/.bashrc" "$HOME/.profile" "$HOME/.bash_profile"; do fi done +setup_step="github-shell-auth" +bash scripts/configure-codex-cloud-github-shell.sh + # shellcheck source=/dev/null source "$runtime_profile" @@ -265,6 +276,11 @@ install_npm_cli "@openai/codex" "$codex_cli_version" "codex" setup_step="git-remote" node scripts/ensure-codex-cloud-git-remote.mjs --configure-gh-helper +setup_step="checkout-base" +bash scripts/refresh-codex-cloud-base.sh +# shellcheck source=/dev/null +source "$runtime_profile" + setup_step="deno-runtime" if ! command -v deno >/dev/null 2>&1 || [[ "$(deno --version 2>/dev/null | sed -n '1s/^deno \([0-9]*\).*/\1/p')" != "2" ]]; then log "Installing Deno 2.x." diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index fb990d838..36fae647b 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -265,7 +265,7 @@ describe("Codex Cloud environment contract", () => { expect(report).toContain("hosted_workspace.class_documented=personal-pro"); expect(report).toContain("OPENAI_API_KEY.present=true"); expect(report).toContain("hosted_app.inventory=external-unverified-until-fresh-task"); - expect(report).toContain("provider_route.github=codex-native-connector"); + expect(report).toContain("provider_route.github=codex-native-connector-or-authenticated-gh"); expect(report).toContain("provider_route.railway=chatgpt-official-app"); expect(report).toContain("provider_route.supabase=chatgpt-project-scoped-read-only-app"); expect(report).toContain( @@ -576,6 +576,14 @@ describe("Codex Cloud environment contract", () => { new URL("../scripts/install-codex-cloud-command-shims.sh", import.meta.url), "utf8", ); + const githubShellSetup = readFileSync( + new URL("../scripts/configure-codex-cloud-github-shell.sh", import.meta.url), + "utf8", + ); + const checkoutBaseRefresh = readFileSync( + new URL("../scripts/refresh-codex-cloud-base.sh", import.meta.url), + "utf8", + ); const patDelete = readFileSync( new URL("../scripts/delete-codex-cloud-branch-with-pat.sh", import.meta.url), "utf8", @@ -623,11 +631,29 @@ describe("Codex Cloud environment contract", () => { expect(setup).toContain("--require-hashes -r worker/python/requirements-cloud.txt"); expect(setup).toContain('"$ocr_venv/bin/python" -m pip check'); expect(setup).toContain("CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"); + expect(setup).toContain("bash scripts/configure-codex-cloud-github-shell.sh"); + expect(setup).toContain("bash scripts/refresh-codex-cloud-base.sh"); + expect(setup.indexOf("bash scripts/configure-codex-cloud-github-shell.sh")).toBeLessThan( + setup.indexOf('source "$runtime_profile"'), + ); expect(maintenance).toContain("CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"); expect(maintenance).toContain("ensure-codex-cloud-git-remote.mjs"); - expect(commandShims).toContain('nvm which "$expected_node_major"'); + expect(maintenance).toContain("bash scripts/configure-codex-cloud-github-shell.sh"); + expect(maintenance).toContain("bash scripts/refresh-codex-cloud-base.sh"); + expect(commandShims).toContain('nvm version "$expected_node_major"'); + expect(commandShims).toContain('node_bin="$NVM_DIR/versions/node/$resolved_node_version/bin"'); + expect(commandShims).toContain('clean_path="${clean_path//:$HOME\\/.local\\/bin:/:}"'); expect(commandShims).toContain('. "$runtime_profile"'); expect(commandShims).toContain('mkdir -p "$HOME/.local/bin"'); + expect(githubShellSetup).toContain("set +x"); + expect(githubShellSetup).toContain("gh auth login --hostname github.com --git-protocol https --with-token"); + expect(githubShellSetup).toContain('export GH_CONFIG_DIR="$HOME/.config/gh"'); + expect(githubShellSetup).toContain("unset CODEX_CLOUD_GITHUB_PAT GH_TOKEN GITHUB_TOKEN"); + expect(githubShellSetup).toContain("gh api user --jq .login"); + expect(githubShellSetup).toContain('gh api "repos/$repository"'); + expect(githubShellSetup).not.toMatch(/printf[^\n]*(?:CODEX_CLOUD_GITHUB_PAT|GH_TOKEN|GITHUB_TOKEN)/u); + expect(checkoutBaseRefresh).toContain("git merge-base HEAD refs/remotes/origin/main"); + expect(checkoutBaseRefresh).toContain("cloud-expected-base-sha"); expect(patDelete).toContain('[[ "${CODEX_CLOUD:-0}" != "1" ]]'); expect(patDelete).toContain('[[ "$branch" != -* ]]'); expect(patDelete).toContain("git check-ref-format --branch"); From 84a3da05dcc348a156c98b07f3487e6df3a8bb64 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:42:34 +0800 Subject: [PATCH 2/6] test(codex): match escaped shim path --- tests/codex-cloud-setup.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index 36fae647b..15747f454 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -642,7 +642,7 @@ describe("Codex Cloud environment contract", () => { expect(maintenance).toContain("bash scripts/refresh-codex-cloud-base.sh"); expect(commandShims).toContain('nvm version "$expected_node_major"'); expect(commandShims).toContain('node_bin="$NVM_DIR/versions/node/$resolved_node_version/bin"'); - expect(commandShims).toContain('clean_path="${clean_path//:$HOME\\/.local\\/bin:/:}"'); + expect(commandShims).toContain('clean_path="\\${clean_path//:\\$HOME\\/.local\\/bin:/:}"'); expect(commandShims).toContain('. "$runtime_profile"'); expect(commandShims).toContain('mkdir -p "$HOME/.local/bin"'); expect(githubShellSetup).toContain("set +x"); From 34e5581c4418ef8a08909dff9ecf91d4a8f622de Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:55:06 +0800 Subject: [PATCH 3/6] fix(codex): reject plaintext GitHub auth --- docs/codex-cloud.md | 14 ++++++++------ scripts/check-codex-cloud-setup.mjs | 5 +++++ scripts/configure-codex-cloud-github-shell.sh | 5 +++++ tests/codex-cloud-setup.test.ts | 3 +++ 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index 94a707fc1..dd3272ce9 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -206,12 +206,14 @@ validates this exact repository and the fresh task must exercise the complete re any mutation; `gh auth status` alone is not acceptance evidence. Codex exposes Secrets only during setup. `configure-codex-cloud-github-shell.sh` sends the secret to -`gh auth login` over standard input, immediately unsets all token variables, restricts the standard -GitHub CLI credential store to the current user, verifies identity `BigSimmo` and repository push -permission, and configures the token-free Git credential helper. The generated runtime profile and -Codex shell policy continue to exclude the original secret and token variables. No credential is -written to the checkout, remote URL, task prompt, or logs. Changing a Secret invalidates the setup -cache; start a new task and run the acceptance checks below. +`gh auth login` over standard input, immediately unsets all token variables, and requires GitHub CLI +to use an OS-backed secure credential store. If `gh` falls back to an `oauth_token` entry in its +plaintext `hosts.yml`, setup logs out, removes that file, and fails closed. A successful setup then +verifies identity `BigSimmo` and repository push permission and configures the token-free Git +credential helper. The generated runtime profile and Codex shell policy continue to exclude the +original secret and token variables. No credential is written to the checkout, remote URL, task +prompt, logs, or a plaintext home-directory file. Changing a Secret invalidates the setup cache; +start a new task and run the acceptance checks below. GitHub's repository permission model does not provide separate token switches for every prohibited workflow action. The `Run PR` policy therefore remains authoritative: never merge or close a PR, diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index b0ddc42c4..400e8b2c2 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -928,6 +928,11 @@ export function validateCodexCloudSetup() { [/gh api user --jq \.login/, "GitHub shell setup must verify the authenticated identity."], [/repos\/\$repository/, "GitHub shell setup must verify repository-specific push permission."], [/gh auth setup-git/, "GitHub shell setup must configure a token-free Git credential helper."], + [/grep -Eq '[^']*oauth_token:/, "GitHub shell setup must detect GitHub CLI plaintext credential fallback."], + [ + /GitHub CLI fell back to plaintext credential storage/, + "GitHub shell setup must fail closed when OS-backed secure storage is unavailable.", + ], ]) { requireMatch(errors, githubShellSetup, pattern, message); } diff --git a/scripts/configure-codex-cloud-github-shell.sh b/scripts/configure-codex-cloud-github-shell.sh index 3031217cd..b4f4c7823 100755 --- a/scripts/configure-codex-cloud-github-shell.sh +++ b/scripts/configure-codex-cloud-github-shell.sh @@ -70,5 +70,10 @@ if [[ -d "$GH_CONFIG_DIR" ]]; then chmod 0700 "$GH_CONFIG_DIR" [[ ! -f "$GH_CONFIG_DIR/hosts.yml" ]] || chmod 0600 "$GH_CONFIG_DIR/hosts.yml" fi +if [[ -f "$GH_CONFIG_DIR/hosts.yml" ]] && grep -Eq '^[[:space:]]*oauth_token:' "$GH_CONFIG_DIR/hosts.yml"; then + gh auth logout --hostname github.com --user "$expected_identity" >/dev/null 2>&1 || true + rm -f "$GH_CONFIG_DIR/hosts.yml" + fail "GitHub CLI fell back to plaintext credential storage; configure the native connector or a Cloud image with OS-backed secure storage." +fi validate_github_access diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index 15747f454..f6b0f2bbd 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -651,6 +651,9 @@ describe("Codex Cloud environment contract", () => { expect(githubShellSetup).toContain("unset CODEX_CLOUD_GITHUB_PAT GH_TOKEN GITHUB_TOKEN"); expect(githubShellSetup).toContain("gh api user --jq .login"); expect(githubShellSetup).toContain('gh api "repos/$repository"'); + expect(githubShellSetup).toContain("grep -Eq '^[[:space:]]*oauth_token:'"); + expect(githubShellSetup).toContain("GitHub CLI fell back to plaintext credential storage"); + expect(githubShellSetup).toContain('rm -f "$GH_CONFIG_DIR/hosts.yml"'); expect(githubShellSetup).not.toMatch(/printf[^\n]*(?:CODEX_CLOUD_GITHUB_PAT|GH_TOKEN|GITHUB_TOKEN)/u); expect(checkoutBaseRefresh).toContain("git merge-base HEAD refs/remotes/origin/main"); expect(checkoutBaseRefresh).toContain("cloud-expected-base-sha"); From cc0b68e460f2fddae08e63263544a1e40fd3dda5 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:16:09 +0800 Subject: [PATCH 4/6] fix(codex): keep Cloud PAT out of agent credentials --- docs/branch-review-ledger.md | 1 + docs/codex-cloud.md | 138 ++++++++---------- docs/scripts-index.md | 2 +- scripts/check-codex-cloud-setup.mjs | 43 +----- scripts/configure-codex-cloud-github-shell.sh | 79 ---------- scripts/maintain-codex-cloud.sh | 1 - scripts/setup-codex-cloud.sh | 3 - tests/codex-cloud-setup.test.ts | 25 +--- 8 files changed, 70 insertions(+), 222 deletions(-) delete mode 100755 scripts/configure-codex-cloud-github-shell.sh diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 7ee6ef98c..869910fa6 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -901,3 +901,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-12 | 1815 | 27ce96e1755055ceee2eeae02d6efdf11259fcde | babysit | fixed | Unit coverage: targeted vitest passed: tests/shared-home-empty-state.dom.test.tsx (17 passed). PR required still blocked on pre-existing check failure at old remote head before sync. | | 2026-08-12 | claude/rag-canary-test-review-seprbt | bcf357a96fde74d39fc4726ffabb5079a744ef28 | eval-canary review: workflow, compare tooling, snapshot builder, alias tiering, rag-behaviour docs | PR #1843 opened; no retrieval behaviour change; snapshot refresh handed off as /issues #304 | verify:pr-local (green except env-only #296), eval:rag:offline 574/574, focused suites 40/40 | | 2026-08-12 | codex/pr-workflow-safety-230-296 | bc0a491fdf4146775629f9b2b03e2a2cc61bd7cb | pr-1830 unblock | unblocked: merged origin/main (outstanding-issues conflict), PR body RAG impact + governance, resolved Copilot thread; merge-tree clean; required CI in progress | check:outstanding-issues pass; evaluatePullRequestPolicy ok; merge-tree clean; PR policy/mergeability/Change scope in progress | +| 2026-08-12 | 1849 | 34e5581c4418ef8a08909dff9ecf91d4a8f622de | full PR diff and unresolved review feedback | P1 setup-only PAT remained accessible through gh credential storage; removed agent-phase PAT persistence and retained safe base/shim changes | check:codex-cloud PASS; docs:check-inventory PASS; focused Vitest blocked by active repository lease | diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index dd3272ce9..d7c7c0999 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -39,10 +39,9 @@ contract: | Maintenance command | `bash scripts/maintain-codex-cloud.sh && bash scripts/install-codex-cloud-command-shims.sh` | | Environment variables | Use the complete profile below | -Keep agent internet access off for the offline environment. In `Database - connected`, allow only -`github.com` and `api.github.com` so authenticated GitHub CLI and Git transport can reach the -repository without opening unrelated provider access. Package installation happens during setup. -The appended command-shim installer is required: +Keep agent internet access off for this repository's ordinary Cloud environments. Package +installation happens during setup; ordinary structure-only work, including the RAG +decomposition prompt below, remains offline. The appended command-shim installer is required: it makes every normal `node`, `npm`, and `npx` invocation load the generated sanitized profile before starting Node. It is idempotent and builds the executable path from `nvm version` instead of resolving through `PATH`, so maintenance cannot accidentally wrap an earlier wrapper. @@ -64,26 +63,26 @@ development, but it cannot reproduce capabilities owned by the host, an operator hardware. Use this matrix when deciding whether a failed task needs a repository fix or a different execution environment. -| Capability | Codex Cloud | Local/Desktop difference | -| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Node and npm | Exact Node 24 range and npm 11 version; locked dev install | Desktop can reuse a byte-identical `node_modules` tree from another worktree, while a fresh Cloud image must populate its own cache | -| Dependency install | `npm ci --include=dev --prefer-offline --no-audit --no-fund`; integrity is checked after install | Local reuse is faster when another complete worktree exists; Cloud cache availability depends on the disposable host image | -| Git hooks and repository checks | Installed by npm postinstall; the same format, lint, type, unit, build, and repository gates are available | Desktop shell Git credentials may support direct push; connected Cloud uses the native connector when available or the authenticated `gh` fallback | -| Browser testing | Matching Chromium, Firefox, and WebKit are installed and launch-tested unless the environment is explicitly source-only | Cloud can run Playwright, but it cannot replace physical Safari, installed-PWA, camera, touch, GPU, or device-specific acceptance | -| Worker/OCR tooling | Python 3.12 hashed lock, PyMuPDF, Pillow, pytesseract, medspaCy, spaCy, and Tesseract are installed and checked | Production workers use their separate Python 3.11 lock; local operators may have additional native inspection tools | -| Deno and Codex CLI | Deno 2 and the reviewed Codex CLI version are installed and checked | Local CLI configuration can enable operator-owned MCP servers; tracked Cloud setup deliberately does not copy or enable them | -| Application runtime | Demo/offline app and browser journeys can run through `npm run ensure` | Authenticated or production-like behavior needs approved provider access; Cloud demo health is not production readiness | -| Provider access | Offline by default; connected capability comes from separately installed host OAuth apps and explicit approvals | A trusted local operator can use separately managed provider CLIs or credentials, subject to the same approval and safety rules | -| GitHub operations | Native connector first; the connected environment provisions authenticated `gh` when required APIs are not injected | The fallback covers PR/review-thread/checks/Actions APIs and ordinary non-force feature-branch Git transport; repository policy still prohibits protected-branch and PR lifecycle mutations | -| Containers and privileged host changes | Repository checks do not assume a durable Docker daemon, nested virtualization, swap, or persistent system state | A local workstation or CI runner can provide Docker, larger disks, persistent caches, device access, and operator-managed capacity | -| Persistence | Repository commits survive when published; home-directory caches and installed tools may be discarded with the environment | Local worktrees, caches, browser state, OAuth sessions, and tool configuration can persist between sessions | +| Capability | Codex Cloud | Local/Desktop difference | +| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| Node and npm | Exact Node 24 range and npm 11 version; locked dev install | Desktop can reuse a byte-identical `node_modules` tree from another worktree, while a fresh Cloud image must populate its own cache | +| Dependency install | `npm ci --include=dev --prefer-offline --no-audit --no-fund`; integrity is checked after install | Local reuse is faster when another complete worktree exists; Cloud cache availability depends on the disposable host image | +| Git hooks and repository checks | Installed by npm postinstall; the same format, lint, type, unit, build, and repository gates are available | Desktop shell Git credentials may support direct push; Cloud publication normally uses the native GitHub connector or PR controls | +| Browser testing | Matching Chromium, Firefox, and WebKit are installed and launch-tested unless the environment is explicitly source-only | Cloud can run Playwright, but it cannot replace physical Safari, installed-PWA, camera, touch, GPU, or device-specific acceptance | +| Worker/OCR tooling | Python 3.12 hashed lock, PyMuPDF, Pillow, pytesseract, medspaCy, spaCy, and Tesseract are installed and checked | Production workers use their separate Python 3.11 lock; local operators may have additional native inspection tools | +| Deno and Codex CLI | Deno 2 and the reviewed Codex CLI version are installed and checked | Local CLI configuration can enable operator-owned MCP servers; tracked Cloud setup deliberately does not copy or enable them | +| Application runtime | Demo/offline app and browser journeys can run through `npm run ensure` | Authenticated or production-like behavior needs approved provider access; Cloud demo health is not production readiness | +| Provider access | Offline by default; connected capability comes from separately installed host OAuth apps and explicit approvals | A trusted local operator can use separately managed provider CLIs or credentials, subject to the same approval and safety rules | +| GitHub operations | Repository reads/publication depend on the Codex GitHub installation and the tools exposed to the task | Review-thread management, Actions reruns, admin APIs, and shell Git authentication may be unavailable even when connector publication works | +| Containers and privileged host changes | Repository checks do not assume a durable Docker daemon, nested virtualization, swap, or persistent system state | A local workstation or CI runner can provide Docker, larger disks, persistent caches, device access, and operator-managed capacity | +| Persistence | Repository commits survive when published; home-directory caches and installed tools may be discarded with the environment | Local worktrees, caches, browser state, OAuth sessions, and tool configuration can persist between sessions | The remaining parity gaps cannot safely be fixed by placing more credentials or mutable provider state in the Cloud shell. The practical improvement path is: 1. Keep repository-owned runtimes, locks, browsers, diagnostics, and checks reproducible here. -2. Prefer native GitHub publication. When the fresh-task tool inventory lacks required APIs, use - the connected environment's authenticated `gh` fallback and verify every remote mutation. +2. Use native GitHub publication and verify the exact remote branch and commit rather than assuming + a local commit or metadata-only PR response was published. 3. Add provider capabilities only through a separate connected environment with least-privileged OAuth and explicit task authorization. 4. Keep authenticated Supabase/OpenAI tests and write-capable Railway operations in protected @@ -155,14 +154,13 @@ PLAYWRIGHT_OFFLINE_MODE=true Keep OpenAI disabled unless a later task explicitly authorizes it. Do not add provider keys, tokens, database URLs, service-role values, E2E credentials, or `ALLOW_PROVIDER_TESTS` to this -environment as ordinary variables. The sole GitHub fallback exception is the encrypted, -setup-only Secret `CODEX_CLOUD_GITHUB_PAT`, described below. The generated agent profile removes -the complete provider-variable inventory in +environment. The generated agent profile removes the complete provider-variable inventory in both access profiles. Connected access records authorization intent and keeps the GitHub boundary explicit, but it does not register hosted MCP apps, expose raw credentials to the shell, or guarantee -that every GitHub capability appears as a direct agent tool. Prefer native Cloud tools when they are -actually present. Otherwise use the verified GitHub CLI fallback and verify the resulting remote -state. A metadata-only `make_pr` response is not publication evidence. +that every GitHub capability appears as a direct agent tool. For ordinary Cloud task publishing, +use the native Cloud diff/PR controls and verify the resulting GitHub branch and PR link. A +metadata-only `make_pr` response is not publication evidence. If a requested GitHub API is not +available, report that limitation rather than using shell credentials as a workaround. Codex Cloud secrets and ordinary environment variables have different exposure and lifecycle properties. This repository has no mechanism that promotes setup-only OpenAI, Supabase, E2E, @@ -186,39 +184,15 @@ diff/PR controls and verify the returned GitHub branch and pull-request link. Do that GitHub is unavailable merely because `gh`, shell Git credentials, or a particular direct agent tool are absent. The intended GitHub identity is `BigSimmo`. Use repository write access for branch and pull-request publication; reserve administrator access for separately -approved operations. Some GitHub APIs, including review-thread or Actions management, may not be -exposed in every Cloud task. When the fresh-task inventory proves that gap, the connected -environment uses the supported GitHub CLI fallback below rather than treating an unverified -connector as authenticated. - -GitHub connector permission is separate from credentials inside the agent shell. Configure the -native connector first. If a fresh task receives no connector tools for review threads, failed-job -logs, Actions reruns, or branch publication, add `CODEX_CLOUD_GITHUB_PAT` as an encrypted **Secret** -in `Database - connected`, never as an ordinary environment variable. Prefer a fine-grained token -owned by `BigSimmo`, restricted to `BigSimmo/Database`, with only Metadata read, Contents read/write, -Pull requests read/write, Issues read/write, Actions read/write, and Checks read. Do not grant -Administration, Deployments, Environments, Secrets, Webhooks, organization administration, or -unrelated repositories. - -[GitHub CLI documents](https://cli.github.com/manual/gh_auth_login) that fine-grained tokens passed -through `--with-token` can behave confusingly outside their selected resources. That is why setup -validates this exact repository and the fresh task must exercise the complete read surface before -any mutation; `gh auth status` alone is not acceptance evidence. - -Codex exposes Secrets only during setup. `configure-codex-cloud-github-shell.sh` sends the secret to -`gh auth login` over standard input, immediately unsets all token variables, and requires GitHub CLI -to use an OS-backed secure credential store. If `gh` falls back to an `oauth_token` entry in its -plaintext `hosts.yml`, setup logs out, removes that file, and fails closed. A successful setup then -verifies identity `BigSimmo` and repository push permission and configures the token-free Git -credential helper. The generated runtime profile and Codex shell policy continue to exclude the -original secret and token variables. No credential is written to the checkout, remote URL, task -prompt, logs, or a plaintext home-directory file. Changing a Secret invalidates the setup cache; -start a new task and run the acceptance checks below. - -GitHub's repository permission model does not provide separate token switches for every prohibited -workflow action. The `Run PR` policy therefore remains authoritative: never merge or close a PR, -push to `main`, `master`, `develop`, or `release/*`, delete or rename a branch, force-push, rewrite -history, or run deployment/provider operations. Branch protections remain the remote backstop. +approved operations. Some GitHub APIs, including review-thread or Actions management, +may not be exposed in every Cloud task; use an approved GitHub-connected workflow for those +operations or report the unavailable capability. Do not use shell credentials as a workaround. + +GitHub connector permission is separate from credentials inside the agent shell. The connector, +native Push control, and GitHub UI are the supported Cloud publication and cleanup paths. Cloud +secrets are setup-only, so `CODEX_CLOUD_GITHUB_PAT` cannot safely support an agent-phase helper; +the name is explicitly excluded by the shell policy and tested with the rest of the credential +inventory. `bash scripts/delete-codex-cloud-branch-with-pat.sh ` is retained only for an explicitly authorised operator running outside Codex Cloud. It rejects `CODEX_CLOUD=1`, @@ -228,24 +202,26 @@ remote URL, cache, or log. Setup restores a missing `origin` to the credential-free URL `https://github.com/BigSimmo/Database.git`; it preserves an existing correct remote and fails -instead of overwriting a wrong or credential-bearing remote. Setup installs `gh` only when the -connected fallback needs it and asks `gh auth setup-git` to install its token-free helper command. -It also fetches `origin/main`, stores the current task's merge base outside the checkout, and exports +instead of overwriting a wrong or credential-bearing remote. When GitHub CLI authentication is +already available, setup asks `gh auth setup-git` to install its token-free helper command. It +never embeds a token or invents a PAT. It also fetches `origin/main`, stores the current task's merge +base outside the checkout, and exports that exact 40-character SHA as `CODEX_CLOUD_EXPECTED_BASE_SHA` in subsequent agent shells. `git -ls-remote` and a non-mutating dry-run push remain separate acceptance checks. +ls-remote` and a dry-run push remain separate acceptance checks; if the connector does not expose +shell Git authentication, report that platform capability gap. Suggested GitHub acceptance task: ```text -Read AGENTS.md, docs/codex-cloud.md, docs/codex-review-protocol.md, and the Run PR skill. Confirm -the GitHub identity and BigSimmo/Database permission, list open PRs, and inspect one PR's exact -head SHA, labels, draft/mergeability state, checks, reviews, comments, unresolved-thread count, -workflow jobs, and bounded failed-job logs when applicable. Confirm reply/resolve and failed-job -rerun API availability without synthetic mutations. Run git ls-remote and an ordinary push ---dry-run against the current authorized non-protected branch; do not create an artificial commit. -Run npm run check:github-shell-access:live and verify every remote mutation that is genuinely -needed. Never merge/close a PR, push a protected branch, delete/rename a branch, rebase, rewrite -history, force-push, deploy, or call OpenAI/Supabase. +Read AGENTS.md and docs/codex-cloud.md. Create a task-specific branch, add one harmless +documentation-only line, commit it, and record its branch name and full 40-character HEAD SHA. +Use the Codex GitHub workflow to publish that exact existing branch and create a draft pull +request. Do not recreate, rename, amend, rebase, or rebuild the branch or commit. Do not merge. +Report whether repository clone, branch publication, and draft PR creation each succeeded, then +report the expected and published branch names and full HEAD SHAs and whether they match exactly. +If branch publication or draft PR creation succeeds, include its link. For each failed write, +report the failure and state that no link is available. Remove the draft branch/PR only after I +approve cleanup. ``` ## Setup and maintenance @@ -482,9 +458,9 @@ copying credentials into the checkout. both Cloud PASS lines, correct runtime/lock parity, `CODEX_CLOUD_ACCESS_PROFILE=connected`, no provider variable reported present, and a credential-free matching origin. Repository MCP metadata is configuration evidence only. -5. **Prove each provider with its approved route.** Prefer tools exposed by the fresh host session. - If GitHub tools are absent, use the authenticated `gh` fallback and confirm `BigSimmo/Database` - plus the intended identity and repository permission. For Railway, read workspace/project/service metadata and confirm the IDs above without +5. **Prove each provider read-only.** Use the tools exposed by the fresh host session, not shell + tokens. For GitHub, read repository metadata and confirm `BigSimmo/Database` plus the intended + identity. For Railway, read workspace/project/service metadata and confirm the IDs above without triggering a deployment. Record the exact Railway inventory. Set the app to allow reads and ask before changes. The Personal Pro global read-versus-change control does not provide tool-level RBAC. If the account later moves to Enterprise/Edu, an admin may additionally allow only @@ -494,13 +470,13 @@ copying credentials into the checkout. without querying clinical row contents. Report only non-secret identity and status metadata. OpenAI has no generic connected-profile credential: leave `RAG_PROVIDER_MODE=offline` until a separately approved paid canary or protected workflow supplies its own credential boundary. -6. **Publish a task branch safely.** Work on a task-specific non-protected branch. Format, stage, - and commit the intended repository change, then publish that exact existing commit through the - native GitHub connector or authenticated GitHub CLI fallback, and verify the remote branch and - PR link. `git push --set-upstream origin ` is acceptable only after confirming the - credential-free origin and protected-branch exclusions. Never embed a PAT in a remote or command. - Do not publish a generic detached `work` branch merely to remove an informational "no upstream" - warning. +6. **Publish a task branch safely.** Work on a task-specific non-protected branch. Format, stage, and commit + the intended repository change, then publish that exact existing commit through the native GitHub + connector/Cloud PR workflow, and verify the remote branch and PR link. If shell Git + authentication is intentionally available, `git push --set-upstream origin ` is + acceptable after confirming the credential-free origin; otherwise a failed `git ls-remote` is + not repaired with an embedded PAT. Do not publish a generic detached `work` branch merely to + remove an informational "no upstream" warning. 7. **Separate local app proof from live readiness.** Run `npm run ensure`, use the printed URL, and confirm `/api/local-project-id` before checking `/api/health`. In demo mode, an HTTP 503 with missing Supabase configuration and skipped OpenAI is expected and must not be relabelled healthy. diff --git a/docs/scripts-index.md b/docs/scripts-index.md index c6204837a..30e887818 100644 --- a/docs/scripts-index.md +++ b/docs/scripts-index.md @@ -1,6 +1,6 @@ # Scripts index -Curated map of `scripts/` (227 files) and the `package.json` script surface (236 entries), +Curated map of `scripts/` (226 files) and the `package.json` script surface (236 entries), grouped by purpose. This is orientation, not an exhaustive per-file listing — the authoritative command list is `package.json`, and `npm run docs:check-scripts` verifies every `npm run ` referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above. diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index 400e8b2c2..3163d3f25 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -555,7 +555,7 @@ export function sanitizedCloudCapabilityLines(env = process.env, options = {}) { ]; for (const name of providerCredentialVariables) lines.push(`${name}.present=${Boolean(env[name])}`); lines.push(hostedAppInventoryCapabilityLine(options.hostedAppInventory ?? null)); - lines.push("provider_route.github=codex-native-connector-or-authenticated-gh"); + lines.push("provider_route.github=codex-native-connector"); lines.push("provider_route.railway=chatgpt-official-app"); lines.push("provider_route.supabase=chatgpt-project-scoped-read-only-app"); lines.push(`codex.cli_available=${codexCliAvailable}`); @@ -752,7 +752,6 @@ export function validateCodexCloudSetup() { const setup = read("scripts/setup-codex-cloud.sh"); const maintenance = read("scripts/maintain-codex-cloud.sh"); const commandShims = read("scripts/install-codex-cloud-command-shims.sh"); - const githubShellSetup = read("scripts/configure-codex-cloud-github-shell.sh"); const checkoutBaseRefresh = read("scripts/refresh-codex-cloud-base.sh"); const rawEnvironmentProbe = read("scripts/check-codex-cloud-raw-env.sh"); const patDelete = read("scripts/delete-codex-cloud-branch-with-pat.sh"); @@ -800,10 +799,6 @@ export function validateCodexCloudSetup() { [/unset OPENAI_API_KEY/, "Cloud setup must remove raw provider variables from the agent shell."], [/\.bash_profile/, "Cloud setup must cover Bash login-profile precedence."], [/@openai\/codex/, "Cloud setup must install the Codex CLI."], - [ - /configure-codex-cloud-github-shell\.sh/, - "Cloud setup must provision the authenticated GitHub shell fallback before scrubbing setup secrets.", - ], [/ensure-codex-cloud-git-remote\.mjs/, "Cloud setup must restore a safe origin remote."], [/refresh-codex-cloud-base\.sh/, "Cloud setup must refresh and pin the task checkout base."], [/check:codex-cloud -- --runtime/, "Cloud setup must run runtime acceptance."], @@ -850,6 +845,11 @@ export function validateCodexCloudSetup() { if (setup.includes("@railway/cli") || setup.includes('setup_step="railway-cli"')) { errors.push("Cloud setup must not install or invoke Railway CLI; hosted access comes from the authenticated app."); } + if (/gh auth login|configure-codex-cloud-github-shell\.sh/.test(`${setup}\n${maintenance}`)) { + errors.push( + "Cloud lifecycle scripts must not persist setup-only GitHub credentials for the agent phase; use the native connector.", + ); + } const providerScrubIndex = setup.indexOf("unset OPENAI_API_KEY"); const accessProfileBranchIndex = setup.indexOf('if [ "\\$CODEX_CLOUD_ACCESS_PROFILE" = "connected" ]'); if (providerScrubIndex < 0 || accessProfileBranchIndex < 0 || providerScrubIndex > accessProfileBranchIndex) { @@ -875,12 +875,6 @@ export function validateCodexCloudSetup() { /ensure-codex-cloud-git-remote\.mjs/, "Maintenance must preserve the safe origin remote.", ); - requireMatch( - errors, - maintenance, - /configure-codex-cloud-github-shell\.sh/, - "Maintenance must repair authenticated GitHub shell access before scrubbing setup secrets.", - ); requireMatch( errors, maintenance, @@ -914,31 +908,6 @@ export function validateCodexCloudSetup() { if (!commandShims.includes('exec "$node_bin/$command_name" "\\$@"')) { errors.push("Cloud command shims must execute absolute Node commands."); } - for (const [pattern, message] of [ - [/set \+x/, "GitHub shell setup must disable shell tracing before reading the setup credential."], - [/--with-token/, "GitHub shell setup must pass the setup credential through GitHub CLI standard input."], - [ - /GH_CONFIG_DIR="\$HOME\/\.config\/gh"/, - "GitHub shell setup must keep its credential store outside the repository.", - ], - [ - /unset CODEX_CLOUD_GITHUB_PAT GH_TOKEN GITHUB_TOKEN/, - "GitHub shell setup must scrub token variables immediately after staging authentication input.", - ], - [/gh api user --jq \.login/, "GitHub shell setup must verify the authenticated identity."], - [/repos\/\$repository/, "GitHub shell setup must verify repository-specific push permission."], - [/gh auth setup-git/, "GitHub shell setup must configure a token-free Git credential helper."], - [/grep -Eq '[^']*oauth_token:/, "GitHub shell setup must detect GitHub CLI plaintext credential fallback."], - [ - /GitHub CLI fell back to plaintext credential storage/, - "GitHub shell setup must fail closed when OS-backed secure storage is unavailable.", - ], - ]) { - requireMatch(errors, githubShellSetup, pattern, message); - } - if (/printf[^\n]*(?:CODEX_CLOUD_GITHUB_PAT|GH_TOKEN|GITHUB_TOKEN)/.test(githubShellSetup)) { - errors.push("GitHub shell setup must never print a credential variable."); - } requireMatch( errors, checkoutBaseRefresh, diff --git a/scripts/configure-codex-cloud-github-shell.sh b/scripts/configure-codex-cloud-github-shell.sh deleted file mode 100755 index b4f4c7823..000000000 --- a/scripts/configure-codex-cloud-github-shell.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash - -set -Eeuo pipefail -set +x - -log() { - printf '[codex-cloud:github] %s\n' "$*" -} - -fail() { - printf '[codex-cloud:github] ERROR: %s\n' "$*" >&2 - exit 1 -} - -repository="BigSimmo/Database" -expected_identity="BigSimmo" -export GH_CONFIG_DIR="$HOME/.config/gh" - -[[ "${CODEX_CLOUD:-0}" = "1" ]] || fail "This helper is only supported during Codex Cloud setup or maintenance." - -install_github_cli() { - command -v apt-get >/dev/null 2>&1 || fail "GitHub CLI is unavailable and apt-get cannot install it." - log "Installing GitHub CLI from the Cloud image package repository." - if [[ "$(id -u)" -eq 0 ]]; then - apt-get update - apt-get install -y --no-install-recommends gh - elif command -v sudo >/dev/null 2>&1; then - sudo apt-get update - sudo apt-get install -y --no-install-recommends gh - else - fail "GitHub CLI installation requires root or sudo." - fi -} - -validate_github_access() { - local identity repository_permission - identity="$(gh api user --jq .login 2>/dev/null)" || fail "GitHub identity verification failed." - [[ "$identity" = "$expected_identity" ]] || fail "GitHub identity must be ${expected_identity}; detected ${identity:-unavailable}." - - repository_permission="$(gh api "repos/$repository" --jq '.permissions.push // false' 2>/dev/null)" || - fail "GitHub repository permission verification failed for $repository." - [[ "$repository_permission" = "true" ]] || fail "GitHub access to $repository does not permit ordinary feature-branch pushes." - - gh auth setup-git --hostname github.com >/dev/null || fail "GitHub CLI could not configure its token-free Git credential helper." - log "PASS: identity=$identity repository=$repository feature_branch_push=true" -} - -if [[ -z "${CODEX_CLOUD_GITHUB_PAT:-}" ]]; then - if ! command -v gh >/dev/null 2>&1 || ! gh auth status --hostname github.com >/dev/null 2>&1; then - log "SKIP: no setup-only GitHub credential is available; a separately verified native connector may still provide access." - exit 0 - fi - validate_github_access - exit 0 -fi - -[[ "${CODEX_CLOUD_ACCESS_PROFILE:-offline}" = "connected" ]] || - fail "CODEX_CLOUD_GITHUB_PAT may only be consumed by the connected Cloud access profile." - -command -v gh >/dev/null 2>&1 || install_github_cli - -github_auth_status=0 -gh auth login --hostname github.com --git-protocol https --with-token <<< "$CODEX_CLOUD_GITHUB_PAT" >/dev/null || - github_auth_status=$? -unset CODEX_CLOUD_GITHUB_PAT GH_TOKEN GITHUB_TOKEN -[[ "$github_auth_status" -eq 0 ]] || fail "GitHub CLI authentication failed." -unset github_auth_status - -if [[ -d "$GH_CONFIG_DIR" ]]; then - chmod 0700 "$GH_CONFIG_DIR" - [[ ! -f "$GH_CONFIG_DIR/hosts.yml" ]] || chmod 0600 "$GH_CONFIG_DIR/hosts.yml" -fi -if [[ -f "$GH_CONFIG_DIR/hosts.yml" ]] && grep -Eq '^[[:space:]]*oauth_token:' "$GH_CONFIG_DIR/hosts.yml"; then - gh auth logout --hostname github.com --user "$expected_identity" >/dev/null 2>&1 || true - rm -f "$GH_CONFIG_DIR/hosts.yml" - fail "GitHub CLI fell back to plaintext credential storage; configure the native connector or a Cloud image with OS-backed secure storage." -fi - -validate_github_access diff --git a/scripts/maintain-codex-cloud.sh b/scripts/maintain-codex-cloud.sh index 48d5c06ce..44d206c66 100644 --- a/scripts/maintain-codex-cloud.sh +++ b/scripts/maintain-codex-cloud.sh @@ -9,7 +9,6 @@ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || { cd "$repo_root" export CODEX_CLOUD=1 -bash scripts/configure-codex-cloud-github-shell.sh if [[ -f "$HOME/.clinical-kb-codex-cloud.sh" ]]; then # shellcheck source=/dev/null diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index 562e9504a..9f5816c97 100644 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -260,9 +260,6 @@ for shell_profile in "$HOME/.bashrc" "$HOME/.profile" "$HOME/.bash_profile"; do fi done -setup_step="github-shell-auth" -bash scripts/configure-codex-cloud-github-shell.sh - # shellcheck source=/dev/null source "$runtime_profile" diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index f6b0f2bbd..021e1aa20 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -265,7 +265,7 @@ describe("Codex Cloud environment contract", () => { expect(report).toContain("hosted_workspace.class_documented=personal-pro"); expect(report).toContain("OPENAI_API_KEY.present=true"); expect(report).toContain("hosted_app.inventory=external-unverified-until-fresh-task"); - expect(report).toContain("provider_route.github=codex-native-connector-or-authenticated-gh"); + expect(report).toContain("provider_route.github=codex-native-connector"); expect(report).toContain("provider_route.railway=chatgpt-official-app"); expect(report).toContain("provider_route.supabase=chatgpt-project-scoped-read-only-app"); expect(report).toContain( @@ -576,10 +576,6 @@ describe("Codex Cloud environment contract", () => { new URL("../scripts/install-codex-cloud-command-shims.sh", import.meta.url), "utf8", ); - const githubShellSetup = readFileSync( - new URL("../scripts/configure-codex-cloud-github-shell.sh", import.meta.url), - "utf8", - ); const checkoutBaseRefresh = readFileSync( new URL("../scripts/refresh-codex-cloud-base.sh", import.meta.url), "utf8", @@ -628,33 +624,22 @@ describe("Codex Cloud environment contract", () => { expect(setup).toContain('setup_step="python-worker-requirements"'); expect(setup).not.toContain("@railway/cli"); expect(setup).not.toContain('setup_step="railway-cli"'); + expect(setup).not.toContain("gh auth login"); + expect(setup).not.toContain("configure-codex-cloud-github-shell.sh"); + expect(maintenance).not.toContain("gh auth login"); + expect(maintenance).not.toContain("configure-codex-cloud-github-shell.sh"); expect(setup).toContain("--require-hashes -r worker/python/requirements-cloud.txt"); expect(setup).toContain('"$ocr_venv/bin/python" -m pip check'); expect(setup).toContain("CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"); - expect(setup).toContain("bash scripts/configure-codex-cloud-github-shell.sh"); expect(setup).toContain("bash scripts/refresh-codex-cloud-base.sh"); - expect(setup.indexOf("bash scripts/configure-codex-cloud-github-shell.sh")).toBeLessThan( - setup.indexOf('source "$runtime_profile"'), - ); expect(maintenance).toContain("CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"); expect(maintenance).toContain("ensure-codex-cloud-git-remote.mjs"); - expect(maintenance).toContain("bash scripts/configure-codex-cloud-github-shell.sh"); expect(maintenance).toContain("bash scripts/refresh-codex-cloud-base.sh"); expect(commandShims).toContain('nvm version "$expected_node_major"'); expect(commandShims).toContain('node_bin="$NVM_DIR/versions/node/$resolved_node_version/bin"'); expect(commandShims).toContain('clean_path="\\${clean_path//:\\$HOME\\/.local\\/bin:/:}"'); expect(commandShims).toContain('. "$runtime_profile"'); expect(commandShims).toContain('mkdir -p "$HOME/.local/bin"'); - expect(githubShellSetup).toContain("set +x"); - expect(githubShellSetup).toContain("gh auth login --hostname github.com --git-protocol https --with-token"); - expect(githubShellSetup).toContain('export GH_CONFIG_DIR="$HOME/.config/gh"'); - expect(githubShellSetup).toContain("unset CODEX_CLOUD_GITHUB_PAT GH_TOKEN GITHUB_TOKEN"); - expect(githubShellSetup).toContain("gh api user --jq .login"); - expect(githubShellSetup).toContain('gh api "repos/$repository"'); - expect(githubShellSetup).toContain("grep -Eq '^[[:space:]]*oauth_token:'"); - expect(githubShellSetup).toContain("GitHub CLI fell back to plaintext credential storage"); - expect(githubShellSetup).toContain('rm -f "$GH_CONFIG_DIR/hosts.yml"'); - expect(githubShellSetup).not.toMatch(/printf[^\n]*(?:CODEX_CLOUD_GITHUB_PAT|GH_TOKEN|GITHUB_TOKEN)/u); expect(checkoutBaseRefresh).toContain("git merge-base HEAD refs/remotes/origin/main"); expect(checkoutBaseRefresh).toContain("cloud-expected-base-sha"); expect(patDelete).toContain('[[ "${CODEX_CLOUD:-0}" != "1" ]]'); From 9d7f0a75da4e864c8bfb89c981ffeab774d93859 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:03:08 +0800 Subject: [PATCH 5/6] fix(cloud): avoid empty shim PATH entry --- scripts/install-codex-cloud-command-shims.sh | 6 +++++- tests/codex-cloud-setup.test.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/install-codex-cloud-command-shims.sh b/scripts/install-codex-cloud-command-shims.sh index 7eda67545..c7518898b 100644 --- a/scripts/install-codex-cloud-command-shims.sh +++ b/scripts/install-codex-cloud-command-shims.sh @@ -36,7 +36,11 @@ clean_path=":\$PATH:" clean_path="\${clean_path//:\$HOME\/.local\/bin:/:}" clean_path="\${clean_path#:}" clean_path="\${clean_path%:}" -export PATH="$node_bin:\$clean_path" +if [[ -n "\$clean_path" ]]; then + export PATH="$node_bin:\$clean_path" +else + export PATH="$node_bin" +fi unset clean_path exec "$node_bin/$command_name" "\$@" EOF diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index 021e1aa20..4fcbc1a48 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -638,6 +638,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('clean_path="\\${clean_path//:\\$HOME\\/.local\\/bin:/:}"'); + expect(commandShims).toContain('if [[ -n "\\$clean_path" ]]; then'); + expect(commandShims).toContain('export PATH="$node_bin"'); expect(commandShims).toContain('. "$runtime_profile"'); expect(commandShims).toContain('mkdir -p "$HOME/.local/bin"'); expect(checkoutBaseRefresh).toContain("git merge-base HEAD refs/remotes/origin/main"); From 627deb8f4b14c5c432f459de4bef0806dfab504e Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:57:47 +0800 Subject: [PATCH 6/6] fix(cloud): harden command shim path cleanup --- scripts/install-codex-cloud-command-shims.sh | 4 +- tests/codex-cloud-setup.test.ts | 51 ++++++++++++++++++-- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/scripts/install-codex-cloud-command-shims.sh b/scripts/install-codex-cloud-command-shims.sh index c7518898b..7d2230d95 100644 --- a/scripts/install-codex-cloud-command-shims.sh +++ b/scripts/install-codex-cloud-command-shims.sh @@ -33,7 +33,9 @@ set -Eeuo pipefail # or maintenance replaces this wrapper through the same idempotent command. . "$runtime_profile" clean_path=":\$PATH:" -clean_path="\${clean_path//:\$HOME\/.local\/bin:/:}" +while [[ "\$clean_path" == *":\$HOME/.local/bin:"* ]]; do + clean_path="\${clean_path//:\$HOME\/.local\/bin:/:}" +done clean_path="\${clean_path#:}" clean_path="\${clean_path%:}" if [[ -n "\$clean_path" ]]; then diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index 4fcbc1a48..acc632017 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -630,13 +630,20 @@ describe("Codex Cloud environment contract", () => { expect(maintenance).not.toContain("configure-codex-cloud-github-shell.sh"); expect(setup).toContain("--require-hashes -r worker/python/requirements-cloud.txt"); expect(setup).toContain('"$ocr_venv/bin/python" -m pip check'); - expect(setup).toContain("CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"); - expect(setup).toContain("bash scripts/refresh-codex-cloud-base.sh"); - expect(maintenance).toContain("CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"); + const refreshCommand = "bash scripts/refresh-codex-cloud-base.sh"; + const runtimeCheck = "CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime"; + + expect(setup).toContain(runtimeCheck); + expect(setup).toContain(refreshCommand); + expect(maintenance).toContain(runtimeCheck); expect(maintenance).toContain("ensure-codex-cloud-git-remote.mjs"); - expect(maintenance).toContain("bash scripts/refresh-codex-cloud-base.sh"); + expect(maintenance).toContain(refreshCommand); + for (const script of [setup, maintenance]) { + expect(script.indexOf(refreshCommand)).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('if [[ -n "\\$clean_path" ]]; then'); expect(commandShims).toContain('export PATH="$node_bin"'); @@ -652,6 +659,40 @@ 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-"); + const bashHome = bashPathEntry(home); + const nodeVersion = "v24.19.0"; + const nodeBin = path.join(home, ".nvm", "versions", "node", nodeVersion, "bin"); + const bashNodeBin = bashPathEntry(nodeBin); + 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'); + 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", + env: { ...process.env, HOME: bashHome, PATH: bashPathList(process.env.PATH || "") }, + }); + expect(install.status, install.stderr || install.stdout).toBe(0); + + const shimDir = `${bashHome}/.local/bin`; + const result = spawnSync(bashCommand, [`${shimDir}/node`], { + cwd: repoRoot, + encoding: "utf8", + env: { ...process.env, HOME: bashHome, PATH: `${shimDir}:${shimDir}:/usr/bin:/bin` }, + }); + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(result.stdout.trim()).toBe(`${bashNodeBin}:/usr/bin:/bin`); + }); + it("writes managed shell policy behaviorally and preserves unrelated Codex config", () => { const home = temporaryDirectory("codex-cloud-home-"); mkdirSync(path.join(home, ".codex"), { recursive: true });