diff --git a/.codex/config.toml b/.codex/config.toml index 1c16b5cb04..90047c7796 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -3,11 +3,11 @@ # # checked-in policy: # - enabled = false so ordinary/offline Codex sessions do not initialize providers -# - read-only Supabase tools use "auto"; write-capable Figma/Railway/Sentry tools use -# "writes" so reads stay frictionless while writes still require explicit approval +# - production Supabase uses "prompt" so every metadata/read operation requires +# explicit approval; write-capable Figma/Railway/Sentry tools use "writes" # - Paid API canaries (eval:rag, eval:retrieval:quality, eval:quality, verify:release, # test:live, check:supabase-project) still need explicit confirmation per AGENTS.md -# Runtime Cloud MCP remains `.mcp.json` (Railway + read-only Supabase only). +# Hosted Cloud tools come from installed OAuth plugins/connectors; `.mcp.json` is a cross-client template, not runtime proof. [mcp_servers.figma_cloud] url = "https://mcp.figma.com/mcp" @@ -15,11 +15,11 @@ enabled = false default_tools_approval_mode = "writes" [mcp_servers.supabase_cloud] -# Production project, read_only=true, approved feature groups only. +# Production project, read_only=true, docs/development metadata only; database and debugging groups are excluded. # OAuth scopes come from the hosted Supabase consent screen; do not broaden this URL. -url = "https://mcp.supabase.com/mcp?project_ref=sjrfecxgysukkwxsowpy&read_only=true&features=docs%2Cdatabase%2Cdebugging%2Cdevelopment" +url = "https://mcp.supabase.com/mcp?project_ref=sjrfecxgysukkwxsowpy&read_only=true&features=docs%2Cdevelopment" enabled = false -default_tools_approval_mode = "auto" +default_tools_approval_mode = "prompt" [mcp_servers.railway_cloud] url = "https://mcp.railway.com" diff --git a/.mcp.json b/.mcp.json index 8031e3a019..22e70a4073 100644 --- a/.mcp.json +++ b/.mcp.json @@ -6,7 +6,7 @@ }, "supabase": { "type": "http", - "url": "https://mcp.supabase.com/mcp?project_ref=sjrfecxgysukkwxsowpy&read_only=true&features=docs%2Cdatabase%2Cdebugging%2Cdevelopment" + "url": "https://mcp.supabase.com/mcp?project_ref=sjrfecxgysukkwxsowpy&read_only=true&features=docs%2Cdevelopment" } } } diff --git a/AGENTS.md b/AGENTS.md index e092df524e..e8d53509df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -870,13 +870,17 @@ Use `docs/codex-cloud.md` as the environment contract: - Configure maintenance as `bash scripts/maintain-codex-cloud.sh && bash scripts/install-codex-cloud-command-shims.sh`. - Default to `CODEX_CLOUD_ACCESS_PROFILE=offline` for ordinary and protected RAG work. Use `connected` only when the user explicitly authorizes the required provider access. -- When MCP tools are already available in a Cloud session and the task needs them, read-only tools - can run without per-tool approval. Write-capable Figma, Railway, and Sentry tools still require - explicit confirmation. Paid API canaries (`eval:rag`, - `eval:retrieval:quality`, `eval:quality`, `verify:release`, `test:live`, - `check:supabase-project`) still need explicit confirmation. Project `.codex/config.toml` - keeps MCP entries `enabled = false` so ordinary/offline Codex hosts do not initialize them; - runtime Cloud MCP remains `.mcp.json` plus the host environment. +- When MCP tools are already callable in a Cloud session and the task needs them, use the host + plugin/connector inventory. The production Supabase target is limited to prompted, read-only + `docs` and `development` metadata tools; do not enable database, SQL, row, or log tools. + Write-capable Figma, Railway, and Sentry tools still require explicit confirmation. Paid API + canaries (`eval:rag`, `eval:retrieval:quality`, `eval:quality`, `verify:release`, + `test:live`, `check:supabase-project`) still need explicit confirmation. Project + `.codex/config.toml` keeps MCP entries `enabled = false` so ordinary/offline hosts do not + initialize them. Connected setup writes enabled Railway and constrained Supabase entries to the + host `$CODEX_HOME/config.toml`; actual availability still requires the installed host + plugin/connector to complete OAuth and a fresh task to prove the callable inventory with + read-only identity calls. Root `.mcp.json` is a static cross-client template, not runtime proof. - Cloud has no Windows task-start script. Report that exact fact, then perform equivalent read-only identity, branch, status, worktree, and Git-operation checks. Proceed only in a clean disposable checkout on a task-specific non-protected branch. @@ -887,15 +891,18 @@ Use `docs/codex-cloud.md` as the environment contract: - Repository setup cannot grant GitHub installation permissions, workspace RBAC, network policy, or provider credentials. Treat those as product/account settings and verify them separately without printing secret values. -- In a fresh Cloud agent shell, run `npm run check:codex-cloud` directly, without manually - sourcing a profile or entering a login shell; it must report the static-and-environment PASS - line. Then run `npm run check:codex-cloud -- --runtime`; it must report the static, - environment, and runtime PASS line. The command shims load the generated profile for normal - `node`, `npm`, and `npx` work. Also run `npm run check:runtime` and - `npm run check:installed-lock-parity` before trusting a new or reset environment. A skipped - browser install is not full browser readiness. Output is limited to approved mode values, - presence booleans, repository identity, and MCP server/command/environment-variable names; - never print credential values. +- In a fresh Cloud task, run `bash scripts/check-codex-cloud-raw-env.sh` before sourcing a + profile or entering a login shell. It must report only provider variable names and presence, + never values. Then run `npm run check:codex-cloud` directly; it must report the + static-and-environment PASS line. Run `npm run check:codex-cloud -- --runtime` with + `CODEX_CLOUD_EXPECTED_BASE_SHA` set to the intended merge/base commit when the checkout has + only a task HEAD. Setup and maintenance may report freshness as unverified so provisioning + remains repairable, but explicit acceptance must not pass an arbitrary HEAD. The command shims + load the generated profile for normal `node`, `npm`, and `npx` work. Also run + `npm run check:runtime` and `npm run check:installed-lock-parity` before trusting a new or + reset environment. A skipped browser install is not full browser readiness. Output is limited + to approved mode values, presence booleans, full Git commit identities, and MCP + server/command/environment-variable names; never print credential values. - Do not add OpenAI, Supabase, Railway, GitHub, database, or user credentials as ordinary Cloud environment variables. Codex Cloud secrets are setup-only and unavailable to the agent phase unless the platform explicitly exposes a secret to the named task phase; do not @@ -908,9 +915,12 @@ Use `docs/codex-cloud.md` as the environment contract: `.github/workflows/authenticated-live-tests.yml` GitHub Actions workflow, its explicit dispatch confirmation, and the `Database / production` environment, never by exposing credentials to the Codex Cloud agent shell. -- Railway reads require both the pinned CLI and a dedicated `RAILWAY_API_TOKEN`. Never substitute - `RAILWAY_TOKEN`. GitHub CLI authentication, the credential-free `origin` URL, and shell Git - authentication are separate capabilities. +- Connected Cloud Railway access uses the hosted Railway MCP connector and browser OAuth; prove it + with the callable tool inventory and a read-only identity/project-list call. CLI token auth is a + separate operator capability: it requires both the pinned CLI and a dedicated + `RAILWAY_API_TOKEN`, and must never substitute `RAILWAY_TOKEN` or expose either token to an + ordinary agent shell. GitHub connector access, GitHub CLI authentication, the credential-free + `origin` URL, and shell Git authentication are separate capabilities. - For an explicitly authorised GitHub task, use the authenticated GitHub connector/MCP tools as the default remote control plane. Use them for repository, PR, issue, review thread, and Actions work, including inline-thread replies/resolution, Actions @@ -918,14 +928,13 @@ Use `docs/codex-cloud.md` as the environment contract: `gh`, shell GitHub credentials, or direct shell network access is not a loss of this capability. The intended connection is `BigSimmo` with repository write access. Reserve administrator access for separately approved operations. -- A PAT is a connected-only, user-authorised exception for a genuine connector gap. Store a - short-lived, fine-grained `CODEX_CLOUD_GITHUB_PAT` only as a connected Cloud secret and - scope it to this repository and the named operation. Never make it an ordinary variable, - profile value, remote URL, or cached file. Use only - `bash scripts/delete-codex-cloud-branch-with-pat.sh ` for an exact - branch-deletion instruction; it verifies the profile, ref, and credential-free origin and - never prints the token. Remove or rotate the secret immediately afterwards. If secrets are - unavailable in that task phase, report the platform limit rather than bypassing it. +- In Codex Cloud, use native Push, the authenticated GitHub connector, or GitHub's UI for branch + publication and cleanup. `CODEX_CLOUD_GITHUB_PAT` is excluded from every Cloud agent shell. + The helper `bash scripts/delete-codex-cloud-branch-with-pat.sh ` is + operator-only outside Codex Cloud; it must reject `CODEX_CLOUD=1`, validate the exact + non-protected ref and credential-free origin, and never print the token. If the native or + connector path is unavailable, report the platform limit rather than copying a PAT into a + profile, remote URL, cached file, or agent environment. - Confirm the exact repository and PR/thread/job before a write, and verify the connector result before treating the write as successful. A repository cannot sanitize a variable already inherited by the top-level task process; the tracked shims protect normal diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index 3ba9c278c1..cc91207436 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -47,7 +47,7 @@ profile before starting Node. It is idempotent and uses `nvm which` rather than `command -v node`, so maintenance cannot accidentally wrap an earlier wrapper. The setup command fails if the complete toolchain cannot be installed. It pins Railway CLI -`5.30.1` and Codex CLI `0.146.0`, both stable npm releases as reviewed on 2026-07-30. Railway's +`5.30.4` and Codex CLI `0.146.0`, both stable npm releases as reviewed on 2026-07-30. Railway's [official CLI guide](https://docs.railway.com/cli) supports global npm installation on Node 16+ (this repository uses Node 24). OpenAI's [official Codex CLI guide](https://learn.chatgpt.com/docs/codex/cli) supports Linux installation; @@ -139,23 +139,17 @@ approved operations. Some GitHub APIs, including review-thread or Actions manage 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 -remains the default for repository, PR, review, and Actions work. For an explicitly authorised -connector gap, a fine-grained GitHub PAT may be stored only as the connected environment secret -`CODEX_CLOUD_GITHUB_PAT`. Scope it to the `BigSimmo/Database` repository, give it only the -least privilege needed for the named operation (for stale-branch deletion, **Contents: write**), -and set a short expiry. Never add it as an ordinary environment variable, print it, put it in a -remote URL, cache, profile, or repository file, or use it for provider access. The default and -ordinary connected profiles both scrub the name before Node work begins. - -The only tracked PAT helper is -`bash scripts/delete-codex-cloud-branch-with-pat.sh `. It refuses offline -mode, protected/invalid refs, and any origin other than the credential-free -`https://github.com/BigSimmo/Database.git`; it uses a temporary askpass program and deletes -only the specified branch and disables Git hooks for its PAT-bearing push. Use it only for the exact -user-authorised cleanup, then remove or -rotate the secret. If Cloud does not expose secrets to the requested task phase, the PAT is not a -usable workaround—report that platform limit rather than copying the token anywhere. +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`, +protected/invalid refs, and any origin other than the credential-free +`https://github.com/BigSimmo/Database.git`. Never copy a PAT into a Cloud task, profile, checkout, +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 @@ -224,9 +218,10 @@ 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 npm run check:codex-cloud -- --runtime. 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. +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 +provider unless this task explicitly names and authorizes that provider. Report the decisive +line from every command and any unrun check. ``` Expected decisive lines include: @@ -240,16 +235,28 @@ The effective-environment check runs automatically when `CODEX_CLOUD=1`, includi `--runtime`, so a newly started agent shell cannot pass with stale modes. Its report prints only approved mode values and presence booleans. The runtime check additionally verifies Node/npm policy and installed-lock parity, pinned Railway/Codex CLIs, Deno 2, Python 3 and worker imports, -Tesseract, browser executables, local `main`/`origin/main`, the `BigSimmo/Database` origin -identity, offline credential absence when applicable, and obsolete npm proxy variable names -without reading or printing their values. MCP inspection emits server names, commands, and -environment variable names only. +Tesseract, actual headless launch-and-close for Chromium/Firefox/WebKit, the Python requirements +fingerprint plus `pip check` and medspaCy/spaCy versions, the expected base commit as an ancestor +of HEAD, the `BigSimmo/Database` origin identity, offline credential absence when applicable, +and obsolete npm proxy variable names without reading or printing their values. It reports +the full current HEAD, local main and origin/main when present, expected base, ancestry result, +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. +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 +sourcing any profile or invoking node/npm in a fresh task, run: + +```bash +bash --noprofile --norc scripts/check-codex-cloud-raw-env.sh +``` -A repository cannot remove a variable already inherited by the top-level task process. The -command shims protect normal Node work, which is what the acceptance commands exercise. If a -fresh task still exposes a provider variable to a direct raw `/bin/bash`, Python, or another -native child before the generated profile is loaded, treat that as a Codex Cloud launcher defect -and report the variable name only; do not weaken the profile or reintroduce provider variables. +The probe checks the complete provider-variable inventory and prints names only. A failure is a +launcher/environment defect; remove the variable in host environment settings and start another +fresh task. Passing only after sourcing the profile or using a command shim does not close the +raw-environment boundary. `npm run check:production-readiness` remains useful in the offline profile for local safeguards. Missing Supabase/OpenAI agent-phase credentials are reported as a provider capability gap and do @@ -271,32 +278,33 @@ and service metadata. Railway's remote MCP does not accept project tokens; retai only for explicitly approved local/operator workflows. The Supabase MCP entry is scoped to production project `sjrfecxgysukkwxsowpy`, forces -`read_only=true`, and exposes only documentation, database, debugging, and development feature -groups. Complete its browser OAuth flow for the organization containing `Clinical KB Database` +`read_only=true`, and exposes only documentation/development metadata tools. The database and +debugging groups are excluded so ordinary Cloud cannot execute SQL, read clinical rows, or inspect +production logs. Complete its browser OAuth flow for the organization containing `Clinical KB Database` and restart the client if tools do not appear. Schema writes, Edge Function deployment, branching, and storage mutations require a separately configured non-production project or branch; do not broaden the production entry. OpenAI generation, Supabase live data, Railway changes, hosted CI reruns, ingestion, deployment, and release workflows remain separate explicit actions. -Project `.codex/config.toml` is a second, project-scoped MCP template that trusted Codex -hosts load in addition to `$CODEX_HOME/config.toml` (where `setup-codex-cloud.sh` writes the -shell-environment policy). It is not inert documentation: Codex applies project-local -`.codex/config.toml` when the project is trusted. The tracked template lists Figma -(`https://mcp.figma.com/mcp`), Railway, read-only Supabase, and Sentry -(`https://mcp.sentry.dev/mcp`) as URL-only registrations with `enabled = false`. Ordinary/offline -sessions therefore do not initialize those providers. Production read-only Supabase uses -`default_tools_approval_mode = "auto"`; write-capable Figma, Railway, and Sentry use `"writes"` -so reads avoid per-tool prompts while writes still require explicit confirmation per AGENTS.md. -Paid API canaries also require explicit confirmation. Figma and Sentry OAuth credentials stay in -the host credential store — never in the tracked file. Runtime Cloud MCP allowlist remains -`.mcp.json` (Railway + read-only Supabase only). `npm run check:codex-cloud` validates both files. - -In a fresh connected Cloud session, run `npm run check:codex-cloud -- --environment` before any -provider call. The sanitized report must show `CODEX_CLOUD_ACCESS_PROFILE=connected`, every -provider environment variable as `present=false`, the credential-free `BigSimmo/Database` origin, -and only the hosted Railway and project-scoped read-only Supabase MCP metadata. This proves the -shell boundary and configured capabilities, not OAuth authorization. Then verify each explicitly -authorized provider with a read-only identity/status call and report only non-secret metadata. +Project `.codex/config.toml` is the checked-in Codex MCP template. Its URL-only entries +remain `enabled = false` so offline tasks do not initialize providers. In the connected profile, +setup copies the audited Railway and constrained Supabase URLs into its managed +`$CODEX_HOME/config.toml` block with `enabled = true`; the first use completes browser OAuth. +Hosted ChatGPT still requires the matching installed plugin/connector. In either host, start a fresh +task after consent and verify the actual callable inventory. +The root `.mcp.json` is a cross-client template and static allowlist only. It does not prove hosted +Cloud availability unless a plugin manifest or host explicitly imports it. + +Production Supabase stays project-scoped and `read_only=true`, with +`default_tools_approval_mode = "prompt"` so every production metadata/read call requires +confirmation. Do not use unrestricted SQL or query clinical rows. Railway, Figma, and Sentry +write-capable tools remain approval-gated. OAuth credentials stay in the host store—never the +tracked files or agent environment. + +In a fresh connected task, first run the raw-shell probe and repository acceptance, then inspect +the actual callable tool inventory. A configured URL or `enabled = false` template is not runtime +proof. Verify Railway and Supabase with read-only identity/project metadata calls and report only +non-secret status; if either tool is absent, the host integration is not activated. ### Connected-environment remediation checklist @@ -312,24 +320,26 @@ copying credentials into the checkout. maintenance as `bash scripts/maintain-codex-cloud.sh && bash scripts/install-codex-cloud-command-shims.sh`. Do not add provider keys, database URLs, service-role credentials, test-user credentials, or - `ALLOW_PROVIDER_TESTS`. + `ALLOW_PROVIDER_TESTS`. Connected setup writes only the audited Railway/Supabase endpoints + into the managed host MCP block; it never writes OAuth tokens. 2. **Grant the host integrations.** Authorize the Codex GitHub connector for `BigSimmo/Database` with repository write access. Complete Railway OAuth only for workspace `bigsimmo's Projects` and project `Database` (`5deaad0b-675a-4c13-978e-5ca2b5b877f9`). Complete Supabase OAuth only for the organization containing `Clinical KB Database`; retain project ref - `sjrfecxgysukkwxsowpy`, `read_only=true`, and the existing feature allowlist. Do not broaden the + `sjrfecxgysukkwxsowpy`, `read_only=true`, and the docs/development-only feature allowlist. Do not broaden the production Supabase MCP to write access. Enable Figma or Sentry only for a task that names that provider; their write-capable tools remain approval-gated. 3. **Start a fresh task.** OAuth tools and environment values are fixed when the task starts. A setup rerun inside an already-running offline task can validate a generated connected profile, but it cannot inject host MCP tools or retroactively grant OAuth. Restart the MCP client or open a new task after consent. -4. **Prove the shell boundary before providers.** Run `npm run check:codex-cloud`, +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`. Require the two Cloud PASS lines, correct runtime and - lock parity, `CODEX_CLOUD_ACCESS_PROFILE=connected`, no provider variable reported present, a - credential-free matching origin, and the expected MCP metadata. A connected label alone is not - provider proof. + `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 + 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 @@ -337,8 +347,8 @@ 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. Commit and format - the intended repository change, publish that exact existing commit through the native GitHub +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 diff --git a/docs/scripts-index.md b/docs/scripts-index.md index e4d83fe500..2aa21a4d2e 100644 --- a/docs/scripts-index.md +++ b/docs/scripts-index.md @@ -1,6 +1,6 @@ # Scripts index -Curated map of `scripts/` (212 files) and the `package.json` script surface (222 entries), +Curated map of `scripts/` (213 files) and the `package.json` script surface (222 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. @@ -16,25 +16,25 @@ migration has shipped (see `docs/maturity-backlog-workorders.md` L1). ## Runner & guard infrastructure [infra] -| Script | Role | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `run-heavy.mjs` | Acquires shared/exclusive cross-worktree leases (`test-run-lock.mjs`) so focused checks can overlap safely | -| `run-tsx.mjs`, `run-vitest.mjs`, `run-playwright.mjs`, `run-eval-safe.mjs` | Typed/test/e2e/eval entrypoint wrappers | -| `dev-free-port.mjs`, `ensure-local-server.mjs` | Project-stable localhost port selection + background server ensure | -| `design-sync.mjs`, `capture-mockup-screenshots.mjs` | Local design-sync CSS prep (`node scripts/design-sync.mjs`) + redesign "current" PNG pack after ensure (`node scripts/capture-mockup-screenshots.mjs`) | -| `check-node-engine.cjs`, `install-git-hooks.mjs`, `guard-push.mjs`, `guard-next-build.mjs` | Install/preflight guards | -| `setup-codex-cloud.sh`, `maintain-codex-cloud.sh`, `install-codex-cloud-command-shims.sh`, `delete-codex-cloud-branch-with-pat.sh`, `check-codex-cloud-setup.mjs`, `ensure-codex-cloud-git-remote.mjs` | Reproducible Codex Cloud toolchain/profile setup, profile-loading Node command shims, a connected-only guarded PAT branch-deletion exception, sanitized acceptance, and safe credential-free `origin` repair | -| `ci-change-scope.mjs`, `ci-triage.mjs`, `pr-policy.mjs`, `pr-mergeability.mjs` | CI change classification + PR policy + conflict signal (self-tested via `check:ci-scope`/`check:ci-triage`/`check:pr-policy`/`check:pr-mergeability`) | -| `check-outstanding-issues.mjs`, `check-pr-mergeability-workflow.mjs` | Outstanding-issues ID/marker/no-driver guard + PR mergeability workflow contract | -| `outstanding-issues.mjs` | Writer for `docs/outstanding-issues.md` (`issues:add` / `issues:done` / `issues:update`) — allocates the id, picks the right table, escapes `\|`, and re-runs the guard on its own output. Never hand-edit that file, as with `ledger:append` | -| `check-installed-lock-parity.mjs`, `phone-chrome-plan.mjs`, `verify-phone-chrome.mjs`, `playwright-browser-preflight.mjs` | Lock-trust preflight, change-scoped phone contracts, and Playwright browser-binary preflight before build | -| `final-merge-audit.mjs` | Fail-closed local merge-tree audit; explicit provider mode adds PR/check/thread/tree/deployment proof | -| `child-process-result.mjs`, `cli-utils.ts`, `productivity-core.mjs` | Shared helpers | -| `test-focused.mjs`, `test-run-selection.mjs`, `test-cache-path.mjs`, `test-environment.mjs` | Backs `npm run test:focused` — change-scoped selection, cache pathing, env setup; fails closed for deleted files and test infrastructure | -| `primary-checkout-lease.mjs`, `test-run-lock.mjs`, `clean-worktree.mjs` | Cross-worktree lease arbitration for the primary checkout, plus worktree cleanup | -| `resolve-tsx-cli.mjs`, `register-server-only.mjs`, `enable-server-only-stub.mjs` | tsx CLI resolution and `server-only` import shims | -| `check-format-changed.mjs`, `check-base-freshness.mjs`, `check-local-presence.mjs` | Push-time helpers behind `guard-push.mjs`: changed-file formatting, stale-base and local-presence checks | -| `yaml-contract.mjs`, `sensitive-text.mjs`, `design-system-contract-utils.mjs` | Shared parsing/redaction/contract helpers used by the gates | +| Script | Role | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `run-heavy.mjs` | Acquires shared/exclusive cross-worktree leases (`test-run-lock.mjs`) so focused checks can overlap safely | +| `run-tsx.mjs`, `run-vitest.mjs`, `run-playwright.mjs`, `run-eval-safe.mjs` | Typed/test/e2e/eval entrypoint wrappers | +| `dev-free-port.mjs`, `ensure-local-server.mjs` | Project-stable localhost port selection + background server ensure | +| `design-sync.mjs`, `capture-mockup-screenshots.mjs` | Local design-sync CSS prep (`node scripts/design-sync.mjs`) + redesign "current" PNG pack after ensure (`node scripts/capture-mockup-screenshots.mjs`) | +| `check-node-engine.cjs`, `install-git-hooks.mjs`, `guard-push.mjs`, `guard-next-build.mjs` | Install/preflight guards | +| `setup-codex-cloud.sh`, `maintain-codex-cloud.sh`, `install-codex-cloud-command-shims.sh`, `check-codex-cloud-raw-env.sh`, `delete-codex-cloud-branch-with-pat.sh`, `check-codex-cloud-setup.mjs`, `ensure-codex-cloud-git-remote.mjs` | Reproducible Codex Cloud toolchain/profile setup, profile-loading Node command shims, a pre-profile name-only credential probe, an operator-only non-Cloud PAT branch-deletion helper, sanitized acceptance, and safe credential-free `origin` repair | +| `ci-change-scope.mjs`, `ci-triage.mjs`, `pr-policy.mjs`, `pr-mergeability.mjs` | CI change classification + PR policy + conflict signal (self-tested via `check:ci-scope`/`check:ci-triage`/`check:pr-policy`/`check:pr-mergeability`) | +| `check-outstanding-issues.mjs`, `check-pr-mergeability-workflow.mjs` | Outstanding-issues ID/marker/no-driver guard + PR mergeability workflow contract | +| `outstanding-issues.mjs` | Writer for `docs/outstanding-issues.md` (`issues:add` / `issues:done` / `issues:update`) — allocates the id, picks the right table, escapes `\|`, and re-runs the guard on its own output. Never hand-edit that file, as with `ledger:append` | +| `check-installed-lock-parity.mjs`, `phone-chrome-plan.mjs`, `verify-phone-chrome.mjs`, `playwright-browser-preflight.mjs` | Lock-trust preflight, change-scoped phone contracts, and Playwright browser-binary preflight before build | +| `final-merge-audit.mjs` | Fail-closed local merge-tree audit; explicit provider mode adds PR/check/thread/tree/deployment proof | +| `child-process-result.mjs`, `cli-utils.ts`, `productivity-core.mjs` | Shared helpers | +| `test-focused.mjs`, `test-run-selection.mjs`, `test-cache-path.mjs`, `test-environment.mjs` | Backs `npm run test:focused` — change-scoped selection, cache pathing, env setup; fails closed for deleted files and test infrastructure | +| `primary-checkout-lease.mjs`, `test-run-lock.mjs`, `clean-worktree.mjs` | Cross-worktree lease arbitration for the primary checkout, plus worktree cleanup | +| `resolve-tsx-cli.mjs`, `register-server-only.mjs`, `enable-server-only-stub.mjs` | tsx CLI resolution and `server-only` import shims | +| `check-format-changed.mjs`, `check-base-freshness.mjs`, `check-local-presence.mjs` | Push-time helpers behind `guard-push.mjs`: changed-file formatting, stale-base and local-presence checks | +| `yaml-contract.mjs`, `sensitive-text.mjs`, `design-system-contract-utils.mjs` | Shared parsing/redaction/contract helpers used by the gates | ## Verification gates [live] diff --git a/scripts/check-codex-cloud-raw-env.sh b/scripts/check-codex-cloud-raw-env.sh new file mode 100644 index 0000000000..cd37bbe435 --- /dev/null +++ b/scripts/check-codex-cloud-raw-env.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Run this directly with a pristine shell before sourcing profiles or invoking +# node/npm shims. It reports names and presence only, never values. +provider_variables=( + OPENAI_API_KEY OPENAI_ORG_ID OPENAI_PROJECT_ID OPENAI_BASE_URL + NEXT_PUBLIC_SUPABASE_URL NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY NEXT_PUBLIC_SUPABASE_ANON_KEY + SUPABASE_URL SUPABASE_ANON_KEY SUPABASE_PUBLISHABLE_KEY SUPABASE_SECRET_KEY + SUPABASE_SERVICE_ROLE_KEY SUPABASE_ACCESS_TOKEN SUPABASE_DB_URL + SUPABASE_PROJECT_REF SUPABASE_PROJECT_NAME SUPABASE_STAGING_PROJECT_REF SUPABASE_STAGING_PROJECT_NAME + DATABASE_URL POSTGRES_PASSWORD CROSS_TENANT_SERVICE_ROLE_KEY + RAILWAY_API_TOKEN RAILWAY_TOKEN + GH_TOKEN GITHUB_TOKEN CODEX_CLOUD_GITHUB_PAT GITLAB_TOKEN GLAB_TOKEN CODEX_TRIGGER_TOKEN + HEALTH_DEEP_PROBE_SECRET INDEXING_V3_AGENT_SECRET + E2E_AUTH_ENABLED E2E_USER_EMAIL E2E_USER_PASSWORD ALLOW_PROVIDER_TESTS +) + +present=() +for name in "${provider_variables[@]}"; do + if [[ -n "${!name:-}" ]]; then + present+=("$name") + fi +done + +if (( ${#present[@]} > 0 )); then + printf '[Codex Cloud Raw Env] FAIL: inherited provider variable names: %s\n' "${present[*]}" >&2 + exit 1 +fi + +printf '[Codex Cloud Raw Env] PASS: no provider variables are inherited by the raw task shell.\n' diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index feb0c93a41..432f192e89 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node import { accessSync, constants, readFileSync, statSync } from "node:fs"; +import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import path from "node:path"; @@ -15,7 +16,7 @@ import { providerEnvironmentKeys } from "./test-environment.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); export const expectedCloudCliVersions = Object.freeze({ - railway: "5.30.1", + railway: "5.30.4", codex: "0.146.0", }); @@ -24,18 +25,27 @@ export const expectedMcpConfiguration = Object.freeze({ railwayUrl: "https://mcp.railway.com", supabaseUrl: "https://mcp.supabase.com/mcp", supabaseProjectRef: "sjrfecxgysukkwxsowpy", - supabaseFeatures: Object.freeze(["database", "debugging", "development", "docs"]), + supabaseFeatures: Object.freeze(["development", "docs"]), }); /** Project `.codex/config.toml` registrations — disabled by default, secret-free URLs only. */ export const expectedCodexProjectMcpServers = Object.freeze({ - figma_cloud: Object.freeze({ url: "https://mcp.figma.com/mcp", approvalMode: "writes" }), - railway_cloud: Object.freeze({ url: expectedMcpConfiguration.railwayUrl, approvalMode: "writes" }), - sentry_cloud: Object.freeze({ url: "https://mcp.sentry.dev/mcp", approvalMode: "writes" }), + figma_cloud: Object.freeze({ + url: "https://mcp.figma.com/mcp", + approvalMode: "writes", + }), + railway_cloud: Object.freeze({ + url: expectedMcpConfiguration.railwayUrl, + approvalMode: "writes", + }), + sentry_cloud: Object.freeze({ + url: "https://mcp.sentry.dev/mcp", + approvalMode: "writes", + }), supabase_cloud: Object.freeze({ // URL validated with the same project/read-only/feature rules as `.mcp.json`. kind: "supabase", - approvalMode: "auto", + approvalMode: "prompt", }), }); @@ -275,7 +285,7 @@ export function validateCodexCloudEnvironment(env = process.env) { } for (const [name, allowed] of Object.entries({ - RAG_PROVIDER_MODE: ["auto", "openai", "offline"], + RAG_PROVIDER_MODE: ["offline"], NEXT_PUBLIC_DEMO_MODE: ["true", "false"], PLAYWRIGHT_OFFLINE_MODE: ["true", "false"], })) { @@ -290,7 +300,7 @@ export function railwayReadCapability(env = process.env, cliAvailable = false) { cliAvailable, dedicatedCredentialPresent: Boolean(env.RAILWAY_API_TOKEN), projectCredentialPresent: Boolean(env.RAILWAY_TOKEN), - ready: cliAvailable && Boolean(env.RAILWAY_API_TOKEN), + cliTokenAuthReady: cliAvailable && Boolean(env.RAILWAY_API_TOKEN), }; } @@ -404,10 +414,11 @@ export function sanitizedCloudCapabilityLines(env = process.env, options = {}) { const codexCliAvailable = options.codexCliAvailable ?? commandAvailable("codex"); const safeGitHelper = options.safeGitHelper ?? hasSafeGitHubCredentialHelper(repoRoot); const mcpServers = options.mcpServers ?? parseMcpServerMetadata(read(".mcp.json")); + const checkout = options.checkout ?? gitCheckoutFreshness(repoRoot, env); const lines = [ `CODEX_CLOUD=${approvedModeValue(env.CODEX_CLOUD, ["1"])}`, `CODEX_CLOUD_ACCESS_PROFILE=${approvedModeValue(env.CODEX_CLOUD_ACCESS_PROFILE ?? "offline", ["offline", "connected"])}`, - `RAG_PROVIDER_MODE=${approvedModeValue(env.RAG_PROVIDER_MODE, ["auto", "openai", "offline"])}`, + `RAG_PROVIDER_MODE=${approvedModeValue(env.RAG_PROVIDER_MODE, ["offline"])}`, `NEXT_PUBLIC_DEMO_MODE=${approvedModeValue(env.NEXT_PUBLIC_DEMO_MODE, ["true", "false"])}`, `PLAYWRIGHT_OFFLINE_MODE=${approvedModeValue(env.PLAYWRIGHT_OFFLINE_MODE, ["true", "false"])}`, ]; @@ -415,12 +426,20 @@ export function sanitizedCloudCapabilityLines(env = process.env, options = {}) { lines.push(`railway.cli_available=${railway.cliAvailable}`); lines.push(`railway.dedicated_credential_present=${railway.dedicatedCredentialPresent}`); lines.push(`railway.project_credential_present=${railway.projectCredentialPresent}`); - lines.push(`railway.read_commands_ready=${railway.ready}`); + lines.push(`railway.cli_token_auth_ready=${railway.cliTokenAuthReady}`); + lines.push("mcp.runtime_tool_inventory=host-provided-unverified-by-repository"); lines.push(`codex.cli_available=${codexCliAvailable}`); + lines.push(pythonWorkerVersionLine(env.CODEX_CLOUD_OCR_PYTHON)); lines.push(`git.origin_configured=${origin.configured}`); lines.push(`git.origin_repository_match=${origin.repositoryMatch}`); lines.push(`git.origin_credential_embedded=${origin.credentialsEmbedded}`); lines.push(`git.github_cli_helper_configured=${safeGitHelper}`); + lines.push(`git.head=${checkout.head}`); + lines.push(`git.local_main=${checkout.localMain}`); + lines.push(`git.origin_main=${checkout.originMain}`); + lines.push(`git.expected_base=${checkout.expectedBase}`); + lines.push(`git.expected_base_ancestor=${checkout.expectedBaseAncestor}`); + lines.push(`git.checkout_freshness=${checkout.freshness}`); for (const server of mcpServers) { lines.push( `mcp.server=${server.name} type=${server.type} command=${server.command} endpoint=${server.endpoint} query_names=${server.queryNames.join(",") || "none"} environment_names=${server.environmentNames.join(",") || "none"}`, @@ -448,6 +467,50 @@ export function localGitBaseline(root = process.cwd(), env = process.env) { return null; } +function fullGitRevision(root, ref) { + const result = spawnSync("git", ["rev-parse", "--verify", ref], { + cwd: root, + encoding: "utf8", + shell: false, + }); + return result.status === 0 ? String(result.stdout ?? "").trim() : "unavailable"; +} + +function normalizedExpectedBase(root, value) { + if (!value) return "unset"; + const candidate = String(value); + if (!/^[0-9a-f]{40}$/i.test(candidate)) return "invalid"; + const revision = fullGitRevision(root, `${candidate}^{commit}`); + return /^[0-9a-f]{40}$/i.test(revision) ? revision.toLowerCase() : "invalid"; +} + +/** @param {NodeJS.ProcessEnv | Record} [env] */ +export function gitCheckoutFreshness(root = process.cwd(), env = process.env) { + const head = fullGitRevision(root, "HEAD"); + const localMain = fullGitRevision(root, "refs/heads/main"); + const originMain = fullGitRevision(root, "refs/remotes/origin/main"); + const expectedBase = normalizedExpectedBase(root, env.CODEX_CLOUD_EXPECTED_BASE_SHA); + let expectedBaseAncestor = "unverified"; + if (expectedBase !== "unset" && expectedBase !== "invalid" && head !== "unavailable") { + const result = spawnSync("git", ["merge-base", "--is-ancestor", expectedBase, "HEAD"], { + cwd: root, + stdio: "ignore", + shell: false, + }); + expectedBaseAncestor = result.status === 0 ? "true" : "false"; + } + const taskOnly = localMain === "unavailable" && originMain === "unavailable"; + const freshness = + expectedBaseAncestor === "true" + ? "verified" + : expectedBaseAncestor === "false" + ? "invalid" + : taskOnly + ? "unverified" + : "branch-reference-available"; + return { head, localMain, originMain, expectedBase, expectedBaseAncestor, freshness }; +} + export function executableFile(filePath) { try { return statSync(filePath).isFile() && (accessSync(filePath, constants.X_OK), true); @@ -456,6 +519,25 @@ export function executableFile(filePath) { } } +export async function playwrightBrowserErrors(browserTypes, timeout = 15_000) { + const errors = []; + for (const [name, browserType] of Object.entries(browserTypes)) { + if (!executableFile(browserType.executablePath())) { + errors.push(`${name} browser executable is unavailable.`); + continue; + } + let browser; + try { + browser = await browserType.launch({ headless: true, timeout }); + } catch (error) { + errors.push(`${name} browser launch failed: ${error instanceof Error ? error.message : String(error)}`); + } finally { + await browser?.close(); + } + } + return errors; +} + export const pythonWorkerImports = ["fitz", "PIL", "pytesseract", "medspacy"]; /** @@ -479,6 +561,59 @@ export function pythonWorkerImportError(pythonCommand, run = spawnSync) { return `Python worker imports failed: ${pythonWorkerImports.join(", ")}.`; } +export function pythonWorkerDependencyErrors(pythonCommand, run = spawnSync) { + if (!pythonCommand || !executableFile(pythonCommand)) { + return ["The configured Codex Cloud OCR Python executable is unavailable."]; + } + const errors = []; + const pipCheck = run(pythonCommand, ["-m", "pip", "check"], { + encoding: "utf8", + shell: false, + }); + if (pipCheck.status !== 0) errors.push("Python worker dependency conflicts were reported by pip check."); + + const requirements = readFileSync(path.join(repoRoot, "worker/python/requirements-cloud.txt")); + const expectedHash = createHash("sha256").update(requirements).digest("hex"); + const markerPath = path.resolve(path.dirname(pythonCommand), "..", ".requirements-cloud.sha256"); + let installedHash = ""; + try { + installedHash = readFileSync(markerPath, "utf8").trim(); + } catch { + errors.push("Python worker requirements fingerprint is missing; rerun Cloud setup."); + } + if (installedHash && installedHash !== expectedHash) { + errors.push("Python worker requirements fingerprint is stale; rerun Cloud setup."); + } + + const versions = run( + pythonCommand, + [ + "-c", + "from importlib.metadata import version; print('medspacy=%s spacy=%s' % (version('medspacy'), version('spacy')))", + ], + { encoding: "utf8", shell: false }, + ); + if (versions.status !== 0) errors.push("Python worker medspacy/spacy version reporting failed."); + return errors; +} + +export function pythonWorkerVersionLine(pythonCommand, run = spawnSync) { + if (!pythonCommand || !executableFile(pythonCommand)) return "python.worker_versions=unavailable"; + const result = run( + pythonCommand, + [ + "-c", + "from importlib.metadata import version; print('medspacy=%s spacy=%s' % (version('medspacy'), version('spacy')))", + ], + { encoding: "utf8", shell: false }, + ); + return result.status === 0 + ? `python.worker_versions=${String(result.stdout ?? "") + .trim() + .replaceAll(/\s+/g, ",")}` + : "python.worker_versions=unavailable"; +} + export function validateCodexCloudSetup() { const errors = []; const packageJson = JSON.parse(read("package.json")); @@ -487,6 +622,7 @@ 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 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"); const agents = read("AGENTS.md"); @@ -512,6 +648,14 @@ export function validateCodexCloudSetup() { [/CODEX_CLOUD_OCR_PYTHON/, "Cloud setup must expose the Python worker environment."], [/playwright install --with-deps chromium firefox webkit/, "Cloud setup must install every browser."], [/CODEX_CLOUD_ACCESS_PROFILE/, "Cloud setup must support explicit access profiles."], + [ + /mcp_servers\.railway_connected/, + "Connected Cloud setup must enable the hosted Railway MCP server in the managed host config.", + ], + [ + /mcp_servers\.supabase_connected/, + "Connected Cloud setup must enable the constrained Supabase MCP server in the managed host config.", + ], [/RAG_PROVIDER_MODE=offline/, "Cloud setup must default RAG to offline mode."], [/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."], @@ -595,9 +739,15 @@ export function validateCodexCloudSetup() { requireMatch( errors, patDelete, - /CODEX_CLOUD_ACCESS_PROFILE.*connected/, - "PAT deletion helper must require the connected profile.", + /CODEX_CLOUD.*use native Push/, + "PAT deletion helper must reject the Codex Cloud agent phase and direct operators to native publication.", ); + requireMatch(errors, rawEnvironmentProbe, /never values/, "Raw Cloud environment probe must report names only."); + for (const name of providerCredentialVariables) { + if (!rawEnvironmentProbe.includes(name)) { + errors.push(`Raw Cloud environment probe must cover provider environment variable ${name}.`); + } + } requireMatch( errors, patDelete, @@ -706,6 +856,7 @@ export async function validateCodexCloudRuntime(env = process.env) { } const pythonError = pythonWorkerImportError(env.CODEX_CLOUD_OCR_PYTHON); if (pythonError) errors.push(pythonError); + else errors.push(...pythonWorkerDependencyErrors(env.CODEX_CLOUD_OCR_PYTHON)); for (const error of [ repositoryCommand(process.execPath, ["scripts/run-tsx.mjs", "scripts/check-runtime.ts"]), @@ -717,15 +868,7 @@ export async function validateCodexCloudRuntime(env = process.env) { if (env.CODEX_CLOUD_SKIP_BROWSER_INSTALL !== "1") { try { const { chromium, firefox, webkit } = await import("playwright"); - for (const [name, browserType] of Object.entries({ - chromium, - firefox, - webkit, - })) { - if (!executableFile(browserType.executablePath())) { - errors.push(`${name} browser executable is unavailable.`); - } - } + errors.push(...(await playwrightBrowserErrors({ chromium, firefox, webkit }))); } catch (error) { errors.push(`Playwright browser validation failed: ${error.message}`); } @@ -735,8 +878,17 @@ export async function validateCodexCloudRuntime(env = process.env) { if (obsoleteProxyNames.length > 0) { errors.push(`Obsolete npm proxy variable names are set: ${obsoleteProxyNames.join(", ")}.`); } - if (!localGitBaseline(repoRoot, env)) { + const baseline = localGitBaseline(repoRoot, env); + const checkout = gitCheckoutFreshness(repoRoot, env); + if (!baseline) { errors.push("Neither local main, origin/main, nor a Cloud task HEAD is available."); + } else if (baseline === "HEAD" && checkout.freshness === "unverified" && env.CODEX_CLOUD_PROVISIONING !== "1") { + errors.push( + "Checkout freshness is unverified: set CODEX_CLOUD_EXPECTED_BASE_SHA to the intended merge/base commit.", + ); + } + if (checkout.expectedBaseAncestor === "false") { + errors.push("CODEX_CLOUD_EXPECTED_BASE_SHA is not an ancestor of the current HEAD."); } const origin = inspectOriginRemote(repoRoot); if (!origin.configured) errors.push("origin is unavailable in the Cloud checkout."); @@ -751,6 +903,15 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me const environment = runtime || process.env.CODEX_CLOUD === "1" || process.argv.includes("--environment"); if (runtime) errors.push(...(await validateCodexCloudRuntime())); else if (environment) errors.push(...validateCodexCloudEnvironment()); + if ( + runtime && + process.env.CODEX_CLOUD_PROVISIONING === "1" && + gitCheckoutFreshness(repoRoot).freshness === "unverified" + ) { + console.warn( + "[Codex Cloud Check] WARN: checkout freshness is unverified during provisioning; run explicit acceptance with CODEX_CLOUD_EXPECTED_BASE_SHA.", + ); + } if (environment) { console.log("[Codex Cloud Environment] sanitized effective modes and capabilities:"); for (const line of sanitizedCloudCapabilityLines()) console.log(` ${line}`); diff --git a/scripts/delete-codex-cloud-branch-with-pat.sh b/scripts/delete-codex-cloud-branch-with-pat.sh index 13125bb69d..4eeeb9bd2a 100644 --- a/scripts/delete-codex-cloud-branch-with-pat.sh +++ b/scripts/delete-codex-cloud-branch-with-pat.sh @@ -8,15 +8,15 @@ usage() { } fail() { - printf '[codex-cloud:github-pat] ERROR: %s\n' "$*" >&2 + printf '[operator:github-pat] ERROR: %s\n' "$*" >&2 exit 1 } [[ "$#" -eq 1 ]] || usage branch="$1" [[ "$branch" != -* ]] || fail "Refusing an option-like branch name." -[[ "${CODEX_CLOUD_ACCESS_PROFILE:-offline}" = "connected" ]] || fail "This emergency helper is connected-profile only." -[[ -n "${CODEX_CLOUD_GITHUB_PAT:-}" ]] || fail "CODEX_CLOUD_GITHUB_PAT is unavailable; add it only as a connected Cloud secret for this one operation." +[[ "${CODEX_CLOUD:-0}" != "1" ]] || fail "Cloud secrets are unavailable during the agent phase; use native Push, the GitHub connector, or GitHub UI." +[[ -n "${CODEX_CLOUD_GITHUB_PAT:-}" ]] || fail "CODEX_CLOUD_GITHUB_PAT is unavailable; this operator-only helper must run outside Codex Cloud." case "$branch" in main|master|develop|release|release/*|HEAD|HEAD/*|refs/*|*..*|*~*|*@\{*|*\\*|*\ *|"") @@ -47,4 +47,4 @@ chmod 0700 "$askpass" GIT_ASKPASS="$askpass" GIT_TERMINAL_PROMPT=0 \ git -c credential.helper= -c core.hooksPath=/dev/null push origin --delete "$branch" -printf '[codex-cloud:github-pat] PASS: deleted %s.\n' "$branch" +printf '[operator:github-pat] PASS: deleted %s.\n' "$branch" diff --git a/scripts/maintain-codex-cloud.sh b/scripts/maintain-codex-cloud.sh index 2e003fa027..c85fdb1528 100644 --- a/scripts/maintain-codex-cloud.sh +++ b/scripts/maintain-codex-cloud.sh @@ -20,7 +20,7 @@ fi node scripts/ensure-codex-cloud-git-remote.mjs --configure-gh-helper npm run check:codex-cloud -if ! npm run check:codex-cloud -- --runtime; then +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' exec bash scripts/setup-codex-cloud.sh fi diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index b4d6a90627..c34d81d709 100644 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -35,7 +35,7 @@ cd "$repo_root" expected_node_major="$(tr -cd '0-9' < .node-version)" expected_npm_version="$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"npm@\([^"]*\)".*/\1/p' package.json | head -n 1)" -railway_cli_version="5.30.1" +railway_cli_version="5.30.4" codex_cli_version="0.146.0" expected_cloud_python="3.12" [[ -n "$expected_node_major" ]] || fail "Could not read the Node major from .node-version." @@ -91,15 +91,20 @@ esac # these variables, so a runtime `${RAG_PROVIDER_MODE:-auto}` fallback would # override a connected environment configured for offline retrieval. if [[ "$access_profile" = "connected" ]]; then - rag_provider_mode="${RAG_PROVIDER_MODE:-auto}" - case "$rag_provider_mode" in - auto|openai|offline) ;; - *) fail "Unsupported RAG_PROVIDER_MODE: $rag_provider_mode" ;; - esac + rag_provider_mode="${RAG_PROVIDER_MODE:-offline}" + [[ "$rag_provider_mode" = "offline" ]] || + fail "Ordinary Codex Cloud must keep RAG_PROVIDER_MODE=offline; run live OpenAI checks only in the protected provider workflow." else rag_provider_mode="offline" fi +connected_supabase_mcp_url="" +if [[ "$access_profile" = "connected" ]]; then + connected_supabase_mcp_url="$(sed -n '/^\[mcp_servers\.supabase_cloud\]$/,/^\[mcp_servers\./ s/^url = "\(.*\)"$/\1/p' .codex/config.toml | head -n 1)" + [[ "$connected_supabase_mcp_url" = https://mcp.supabase.com/mcp\?* ]] || + fail "Could not resolve the audited Supabase MCP URL from .codex/config.toml." +fi + runtime_profile="$HOME/.clinical-kb-codex-cloud.sh" cat > "$runtime_profile" < "$codex_config_candidate" if [[ "${CODEX_CLOUD_SETUP_TEST_FAIL_ATOMIC_WRITE:-0}" = "1" ]]; then @@ -232,11 +247,15 @@ log "Installing locked Node dependencies." setup_step="node-dependencies" npm ci --include=dev +setup_step="railway-cli" install_npm_cli "@railway/cli" "$railway_cli_version" "railway" +setup_step="codex-cli" 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="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." npm install --global 'deno@2' @@ -283,6 +302,11 @@ log "Installing Python worker requirements." setup_step="python-worker-requirements" "$ocr_venv/bin/python" -m pip install --disable-pip-version-check --require-hashes -r worker/python/requirements-cloud.txt "$ocr_venv/bin/python" -m pip check +requirements_marker="$ocr_venv/.requirements-cloud.sha256" +requirements_marker_candidate="${requirements_marker}.tmp" +sha256sum worker/python/requirements-cloud.txt | awk '{print $1}' > "$requirements_marker_candidate" +mv -f "$requirements_marker_candidate" "$requirements_marker" +"$ocr_venv/bin/python" -c 'from importlib.metadata import version; print("medspacy=%s spacy=%s" % (version("medspacy"), version("spacy")))' export CODEX_CLOUD_OCR_PYTHON="$ocr_venv/bin/python" if [[ "${CODEX_CLOUD_SKIP_BROWSER_INSTALL:-0}" = "1" ]]; then @@ -298,7 +322,7 @@ npm run check:runtime npm run check:installed-lock-parity npm run check:worker-python-locks:static npm run check:codex-cloud -npm run check:codex-cloud -- --runtime +CODEX_CLOUD_PROVISIONING=1 npm run check:codex-cloud -- --runtime npm run diagnose:codex-cloud trap - ERR log "Setup complete with ${CODEX_CLOUD_ACCESS_PROFILE} access profile." diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index e0369cf212..de77427370 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -10,11 +10,14 @@ import { configuredProviderCredentialNames, expectedMcpConfiguration, executableFile, + gitCheckoutFreshness, localGitBaseline, obsoleteNpmProxyVariables, parseMcpServerMetadata, + playwrightBrowserErrors, providerCredentialVariables, pythonWorkerImportError, + pythonWorkerVersionLine, pythonWorkerImports, railwayReadCapability, sanitizedCloudCapabilityLines, @@ -62,6 +65,7 @@ const requiredPolicyExcludes = [ "RAILWAY_TOKEN", "GH_TOKEN", "GITHUB_TOKEN", + "CODEX_CLOUD_GITHUB_PAT", "GITLAB_TOKEN", "GLAB_TOKEN", "CODEX_TRIGGER_TOKEN", @@ -191,7 +195,7 @@ describe("Codex Cloud environment contract", () => { NEXT_PUBLIC_DEMO_MODE: "false", PLAYWRIGHT_OFFLINE_MODE: "false", }), - ).toEqual([]); + ).toContain("RAG_PROVIDER_MODE must be an approved value in connected mode."); expect( validateCodexCloudEnvironment({ @@ -223,6 +227,14 @@ describe("Codex Cloud environment contract", () => { railwayCliAvailable: true, codexCliAvailable: true, safeGitHelper: true, + checkout: { + head: "a".repeat(40), + localMain: "b".repeat(40), + originMain: "c".repeat(40), + expectedBase: "a".repeat(40), + expectedBaseAncestor: "true", + freshness: "verified", + }, mcpServers: [ { name: "railway", @@ -238,18 +250,45 @@ describe("Codex Cloud environment contract", () => { const report = lines.join("\n"); expect(report).toContain("OPENAI_API_KEY.present=true"); expect(report).toContain("mcp.server=railway type=http command=none endpoint=https://mcp.railway.com/"); + expect(report).toContain(`git.head=${"a".repeat(40)}`); + expect(report).toContain("git.expected_base_ancestor=true"); + expect(report).toContain("git.checkout_freshness=verified"); expect(report).not.toContain(secret); expect(report).not.toContain("sensitive-test"); + expect( + sanitizedCloudCapabilityLines( + { + CODEX_CLOUD: "1", + CODEX_CLOUD_ACCESS_PROFILE: "connected", + RAG_PROVIDER_MODE: "auto", + }, + { + origin: { configured: true, repositoryMatch: true, credentialsEmbedded: false }, + railwayCliAvailable: false, + codexCliAvailable: false, + safeGitHelper: false, + checkout: { + head: "unavailable", + localMain: "unavailable", + originMain: "unavailable", + expectedBase: "unset", + expectedBaseAncestor: "unverified", + freshness: "unverified", + }, + mcpServers: [], + }, + ), + ).toContain("RAG_PROVIDER_MODE=invalid"); }); it("requires the Railway CLI and dedicated account token without substituting a project token", () => { - expect(railwayReadCapability({ RAILWAY_API_TOKEN: "configured" }, true).ready).toBe(true); + expect(railwayReadCapability({ RAILWAY_API_TOKEN: "configured" }, true).cliTokenAuthReady).toBe(true); expect(railwayReadCapability({ RAILWAY_TOKEN: "configured" }, true)).toMatchObject({ dedicatedCredentialPresent: false, projectCredentialPresent: true, - ready: false, + cliTokenAuthReady: false, }); - expect(railwayReadCapability({ RAILWAY_API_TOKEN: "configured" }, false).ready).toBe(false); + expect(railwayReadCapability({ RAILWAY_API_TOKEN: "configured" }, false).cliTokenAuthReady).toBe(false); }); it("parses MCP transport metadata without query or environment values", () => { @@ -324,10 +363,10 @@ describe("Codex Cloud environment contract", () => { ); expect( validateCodexProjectMcpConfiguration( - tracked.replace('default_tools_approval_mode = "auto"', 'default_tools_approval_mode = "writes"'), + tracked.replace('default_tools_approval_mode = "prompt"', 'default_tools_approval_mode = "auto"'), ), ).toContain( - `.codex/config.toml supabase_cloud must set default_tools_approval_mode = "auto" because the production server is constrained read-only.`, + `.codex/config.toml supabase_cloud must set default_tools_approval_mode = "prompt" because the production server is constrained read-only.`, ); expect( validateCodexProjectMcpConfiguration( @@ -395,6 +434,18 @@ describe("Codex Cloud environment contract", () => { ).toContain(`.codex/config.toml supabase_cloud must keep the production project read-only.`); }); + it("probes the raw task environment without printing credential values", () => { + const secret = "never-print-raw-provider-value"; + const result = spawnSync(bashCommand, ["scripts/check-codex-cloud-raw-env.sh"], { + cwd: repoRoot, + encoding: "utf8", + env: { PATH: process.env.PATH, NODE_ENV: "test", OPENAI_API_KEY: secret }, + }); + expect(result.status).toBe(1); + expect(result.stderr).toContain("OPENAI_API_KEY"); + expect(result.stderr).not.toContain(secret); + }); + it("keeps setup and maintenance repairs guarded for repeat execution", () => { const setup = readFileSync(new URL("../scripts/setup-codex-cloud.sh", import.meta.url), "utf8"); const maintenance = readFileSync(new URL("../scripts/maintain-codex-cloud.sh", import.meta.url), "utf8"); @@ -420,8 +471,8 @@ describe("Codex Cloud environment contract", () => { expect(setup).toContain("Unmanaged [shell_environment_policy] table found"); expect(setup).toContain("Incomplete managed shell policy block"); expect(setup).toContain('export RAG_PROVIDER_MODE="${rag_provider_mode}"'); - expect(setup).toContain('rag_provider_mode="${RAG_PROVIDER_MODE:-auto}"'); - expect(setup).not.toContain('RAG_PROVIDER_MODE="\\${RAG_PROVIDER_MODE:-auto}"'); + expect(setup).toContain('rag_provider_mode="${RAG_PROVIDER_MODE:-offline}"'); + expect(setup).not.toContain('rag_provider_mode="${RAG_PROVIDER_MODE:-auto}"'); expect(setup).toContain("SUPABASE_URL"); expect(setup).toContain("SUPABASE_PROJECT_REF"); expect(setup).toContain("NEXT_PUBLIC_SUPABASE_URL"); @@ -434,11 +485,13 @@ describe("Codex Cloud environment contract", () => { expect(setup).toContain('setup_step="python-worker-requirements"'); 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(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(commandShims).toContain('. "$runtime_profile"'); expect(commandShims).toContain('mkdir -p "$HOME/.local/bin"'); - expect(patDelete).toContain("CODEX_CLOUD_ACCESS_PROFILE:-offline"); + expect(patDelete).toContain('[[ "${CODEX_CLOUD:-0}" != "1" ]]'); expect(patDelete).toContain('[[ "$branch" != -* ]]'); expect(patDelete).toContain("git check-ref-format --branch"); expect(patDelete).toContain("git remote get-url --push --all origin"); @@ -464,6 +517,8 @@ describe("Codex Cloud environment contract", () => { expect(config).toContain("BEGIN clinical-kb-codex-cloud shell policy"); expect(config).toContain("[shell_environment_policy]"); expect(config).toContain('inherit = "all"'); + expect(config).not.toContain("[mcp_servers.railway_connected]"); + expect(config).not.toContain("[mcp_servers.supabase_connected]"); for (const name of requiredPolicyExcludes) { expect(config).toContain(`"${name}"`); } @@ -491,6 +546,12 @@ describe("Codex Cloud environment contract", () => { }); expect(connected.status, connected.stderr || connected.stdout).toBe(0); const connectedProfile = readRuntimeProfile(connectedHome); + const connectedConfig = readCodexConfig(connectedHome); + expect(connectedConfig).toContain("[mcp_servers.railway_connected]"); + expect(connectedConfig).toContain("[mcp_servers.supabase_connected]"); + expect(connectedConfig).toContain("features=docs%2Cdevelopment"); + expect(connectedConfig.match(/^enabled = true$/gm)).toHaveLength(2); + expect(connectedConfig).toContain('default_tools_approval_mode = "prompt"'); expect(connectedProfile).toContain('export CODEX_CLOUD_ACCESS_PROFILE="connected"'); expect(connectedProfile).toContain('export RAG_PROVIDER_MODE="offline"'); expect(connectedProfile).not.toContain("${RAG_PROVIDER_MODE:-auto}"); @@ -600,6 +661,67 @@ describe("Codex Cloud environment contract", () => { expect(localGitBaseline(directory, {})).toBeNull(); expect(localGitBaseline(directory, { CODEX_CLOUD: "1" })).toBe("HEAD"); + + const head = git(directory, "rev-parse", "HEAD").stdout.trim(); + expect(gitCheckoutFreshness(directory, { CODEX_CLOUD: "1" })).toEqual({ + head, + localMain: "unavailable", + originMain: "unavailable", + expectedBase: "unset", + expectedBaseAncestor: "unverified", + freshness: "unverified", + }); + expect( + gitCheckoutFreshness(directory, { + CODEX_CLOUD: "1", + CODEX_CLOUD_EXPECTED_BASE_SHA: head, + }), + ).toMatchObject({ + expectedBase: head, + expectedBaseAncestor: "true", + freshness: "verified", + }); + expect( + gitCheckoutFreshness(directory, { + CODEX_CLOUD: "1", + CODEX_CLOUD_EXPECTED_BASE_SHA: "arbitrary-sensitive-value", + }), + ).toMatchObject({ + expectedBase: "invalid", + expectedBaseAncestor: "unverified", + }); + expect( + gitCheckoutFreshness(directory, { + CODEX_CLOUD: "1", + CODEX_CLOUD_EXPECTED_BASE_SHA: `${head}\nmalformed`, + }), + ).toMatchObject({ + expectedBase: "invalid", + expectedBaseAncestor: "unverified", + }); + }); + + it("normalizes Python package version output for capability reports", () => { + const run = (() => ({ + status: 0, + stdout: "medspacy=1.3.1 spacy=3.8.2\n", + })) as unknown as typeof spawnSync; + expect(pythonWorkerVersionLine(process.execPath, run)).toBe("python.worker_versions=medspacy=1.3.1,spacy=3.8.2"); + }); + + it("launches and closes every installed Playwright browser", async () => { + let closeCount = 0; + const browserType = { + executablePath: () => process.execPath, + launch: async (options: { headless: boolean; timeout: number }) => { + expect(options).toEqual({ headless: true, timeout: 1234 }); + return { close: async () => void (closeCount += 1) }; + }, + }; + expect( + await playwrightBrowserErrors({ chromium: browserType, firefox: browserType, webkit: browserType }, 1234), + ).toEqual([]); + expect(closeCount).toBe(3); }); it("does not describe a source-only runtime as fully browser-ready", () => {