diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml deleted file mode 100644 index e2bf049d87..0000000000 --- a/.github/workflows/autofix.yml +++ /dev/null @@ -1,38 +0,0 @@ -# autofix.ci — auto-apply Prettier fixes to PRs (and main pushes). -# Workflow name must remain exactly "autofix.ci" so the autofix.ci GitHub App -# can securely identify this workflow. Install the app for this repository at -# https://autofix.ci before expecting commits to land. -name: autofix.ci # needed to securely identify the workflow - -on: - pull_request: - push: - branches: ["main"] - -permissions: - contents: read - -concurrency: - group: autofix-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - autofix: - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Setup Node and dependencies - uses: ./.github/actions/setup-node-cached - - - name: Format with Prettier - run: npm run format - - - name: Apply autofix.ci patch - uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4 - with: - commit-message: "style: apply Prettier format" diff --git a/AGENTS.md b/AGENTS.md index b200dfafe9..da6d9bc091 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -866,8 +866,8 @@ Codex Cloud uses an isolated Linux container and does not inherit desktop files, credentials, OAuth sessions, MCP authentication, local services, or uncommitted work. Use `docs/codex-cloud.md` as the environment contract: -- Setup: `bash scripts/setup-codex-cloud.sh`. -- Maintenance: `bash scripts/maintain-codex-cloud.sh`. +- Configure setup as `bash scripts/setup-codex-cloud.sh && bash scripts/install-codex-cloud-command-shims.sh`. +- 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. - Cloud has no Windows task-start script. Report that exact fact, then perform equivalent @@ -880,16 +880,19 @@ 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 Cloud agent shell, `npm run check:codex-cloud` validates the tracked contract and - effective access-profile modes. Use `npm run check:codex-cloud -- --runtime` for the - complete installed toolchain and browser executables. 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 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. - 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; do not copy them into files to bypass that boundary. + agent phase unless the platform explicitly exposes a secret to the named task phase; do not + copy them into files to bypass that boundary. - Provider-backed checks, hosted CI mutations, deployment, production data access, and Git publishing still require the explicit authorization defined above. - The ordinary offline Cloud profile intentionally cannot perform authenticated production or @@ -900,22 +903,27 @@ Use `docs/codex-cloud.md` as the environment contract: 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; never add a PAT or token-bearing credential helper. -- The Codex GitHub connection used to clone a repository is separate from agent-shell - `git push` or `gh` authentication. Reconnect the repository in Codex settings if a - controlled write test cannot publish; never add a PAT to Cloud variables or secrets. + 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 run/job/log/artifact inspection, and approved branch, file, or PR mutations. Missing `gh`, shell GitHub credentials, or direct shell network access is not a loss of this - capability and must not prompt a PAT workaround. The intended connection is `BigSimmo` - with administrator access to this repository. Use shell `git` or `gh` only for a - genuine connector gap and only when the task permits it. -- Confirm the exact repository and PR/thread/job before a write, and verify the - connector result before treating the write as successful. If the connector lacks a - needed GitHub setting or organisation control, report that limit rather than attempting a - credential, secret, or shell-based bypass. + 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. +- 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 + `node`/`npm`/`npx` commands. Report raw-parent exposure as a Codex Cloud launcher defect + rather than weakening the provider-variable contract. - Cloud browser proof is Playwright/Chromium, Firefox, or WebKit container evidence, not physical iPhone Safari/PWA acceptance. @@ -933,7 +941,7 @@ Durable notes for Cloud Agents. Standard commands live in `README.md` and `packa connector/MCP tools first for PR, issue, comment, review-thread, and Actions tasks they support (including run/job/log/artifact inspection and review-thread replies/resolution). A missing `gh` CLI is not a blocker for connector-supported work; never add a PAT as a - workaround. The intended connection is `BigSimmo` with administrator access to this - repository. Verify the exact target and connector result before any write. Ordinary + workaround. The intended connection is `BigSimmo` with repository write access. + Reserve administrator access for separately approved operations. Verify the exact target and connector result before any write. Ordinary authorised shell `git` branch publication remains allowed; use shell `gh` only for a genuine connector gap and only when the task permits it. diff --git a/README.md b/README.md index 5cf222a8b5..9c911bc667 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,9 @@ the Cloud-only Bash setup script. For Codex Cloud, use the tracked environment setup and acceptance contract in [`docs/codex-cloud.md`](docs/codex-cloud.md). It installs the complete repository -toolchain and distinguishes safe offline tasks from explicitly connected provider tasks. +toolchain, installs the profile-loading Node command shims, and distinguishes safe offline +tasks from explicitly connected provider tasks. Fresh Cloud validation runs the direct +`npm run check:codex-cloud` commands without manually sourcing a profile. 3. Copy the full `.env.example` to `.env.local` and fill in Supabase and OpenAI values. Copy the worker and upload defaults too — they are conservative diff --git a/docs/README.md b/docs/README.md index 3300c2bd50..7959ece23a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,7 +19,7 @@ npm run docs:check-links | [site-map.md](site-map.md) | **Generated** route map — regenerate with `npm run docs:update`, verify with `npm run sitemap:check` | | [agents-guide.md](agents-guide.md) | Human onboarding pointer; Cursor MCP default read path (Supabase, Railway, Context7); rules in `AGENTS.md` | | [scripts-index.md](scripts-index.md) | Curated map of `scripts/` and the `package.json` command surface by purpose | -| [codex-cloud.md](codex-cloud.md) | Codex Cloud setup, access profiles, platform settings, and acceptance checks | +| [codex-cloud.md](codex-cloud.md) | Codex Cloud setup, access profiles, profile-loading command shims, GitHub exception, and acceptance checks | ## Architecture diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index 599184c1a4..8a9b36985f 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -30,18 +30,21 @@ In Codex environment settings, create an environment for `BigSimmo/Database` usi official [Cloud environments](https://learn.chatgpt.com/docs/environments/cloud-environment) contract: -| Setting | Value | -| --------------------- | -------------------------------------- | -| Repository | `BigSimmo/Database` | -| Base image | Default universal image | -| Node version | `24` | -| Setup command | `bash scripts/setup-codex-cloud.sh` | -| Maintenance command | `bash scripts/maintain-codex-cloud.sh` | -| Environment variables | Use the complete profile below | - -Enable agent internet access only when a task needs it. Prefer a domain allowlist and the -minimum HTTP methods for the task. Package installation happens during setup; ordinary -structure-only work, including the RAG decomposition prompt below, should remain offline. +| Setting | Value | +| --------------------- | ------------------------------------------------------------------------------------------- | +| Repository | `BigSimmo/Database` | +| Base image | Default universal image | +| Node version | `24` | +| Setup command | `bash scripts/setup-codex-cloud.sh && bash scripts/install-codex-cloud-command-shims.sh` | +| 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: +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. 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 @@ -69,14 +72,17 @@ PLAYWRIGHT_OFFLINE_MODE=true ``` The generated shell profile removes known OpenAI, Supabase, Railway, GitHub/GitLab, -database, CI-trigger, and test-user credential variables. This prevents an unrelated Cloud -task from silently becoming provider-backed. +database, CI-trigger, test-user, and emergency-PAT variables. This prevents an unrelated +Cloud task from silently becoming provider-backed. Set all five offline values in the environment UI. The setup also writes the generated profile to `.bashrc`, `.profile`, and `.bash_profile`. Covering `.bash_profile` is required: Bash stops login-profile discovery at the first matching file, so an existing `.bash_profile` can otherwise prevent `.profile` from running. Offline mode values are forced after inherited platform values so stale `auto`/`false` settings cannot outrank the repository contract. +Do not manually source the profile as an acceptance workaround: start a fresh task and invoke +the direct `npm run check:codex-cloud` commands below. The tracked command shims make those +ordinary Node commands load the profile themselves. ### Connected (explicit opt-in) @@ -133,14 +139,23 @@ 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. Do -not add a personal access token to Cloud secrets or environment variables to make -`git push` or `gh` work. If a Cloud task cannot publish a branch, reconnect the -repository in Codex settings and run a controlled branch/PR write test. Confirm the -exact repository and PR/thread/job before a write, and verify the connector result -before treating the write as successful. If the connector does not expose a required -repository/organisation setting, report the limit rather than attempting a credential or -secret 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. Setup restores a missing `origin` to the credential-free URL `https://github.com/BigSimmo/Database.git`; it preserves an existing correct remote and fails @@ -166,22 +181,21 @@ approve cleanup. ## Setup and maintenance -Setup: +Configure the following complete commands in the environment UI: ```bash -bash scripts/setup-codex-cloud.sh +bash scripts/setup-codex-cloud.sh && bash scripts/install-codex-cloud-command-shims.sh ``` -Maintenance: - ```bash -bash scripts/maintain-codex-cloud.sh +bash scripts/maintain-codex-cloud.sh && bash scripts/install-codex-cloud-command-shims.sh ``` The maintenance command reasserts the safe `origin`, runs static/effective environment -acceptance, then runtime acceptance. Any runtime, dependency, CLI, Deno, Python/OCR, or browser -drift reruns the full setup instead of repairing only `node_modules`. All profile insertions, -CLI installs, and remote repair are idempotent. +acceptance, then runtime acceptance. The shim installer runs after either lifecycle command and +repairs the normal Node command boundary. Any runtime, dependency, CLI, Deno, Python/OCR, or +browser drift reruns the full setup instead of repairing only `node_modules`. All profile +insertions, command shims, CLI installs, and remote repair are idempotent. ## Acceptance @@ -212,6 +226,12 @@ identity, offline credential absence when applicable, and obsolete npm proxy var without reading or printing their values. 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. 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. + `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 not make the provider-free cache unhealthy. `ALLOW_PROVIDER_TESTS=true` expresses authorization, diff --git a/docs/scripts-index.md b/docs/scripts-index.md index 73095ea48c..371a20c7b2 100644 --- a/docs/scripts-index.md +++ b/docs/scripts-index.md @@ -1,6 +1,6 @@ # Scripts index -Curated map of `scripts/` (202 files) and the `package.json` script surface (215 entries), +Curated map of `scripts/` (204 files) and the `package.json` script surface (215 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`, `check-codex-cloud-setup.mjs`, `ensure-codex-cloud-git-remote.mjs` | Reproducible Codex Cloud toolchain/profile setup, 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`, `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 | ## Verification gates [live] @@ -58,8 +58,9 @@ contract), `check-lighthouse-budget.mjs` + `run-lighthouse-budget.mjs`, `measure `layout-shift-elements` audit did not; CLS reproduces offline, LCP does not), and the workflow-contract guards `check-pr-policy-workflow.mjs` and `check-codex-autofix-workflow.mjs`. `audit-formatting-fixtures.ts` checks the formatting fixtures themselves. -`setup-codex-cloud.sh` and `maintain-codex-cloud.sh` reproduce the provider-free Cloud -runtime documented in `docs/codex-cloud.md`; `npm run check:codex-cloud` verifies that +`setup-codex-cloud.sh`, `maintain-codex-cloud.sh`, and +`install-codex-cloud-command-shims.sh` reproduce the provider-free Cloud runtime documented in +`docs/codex-cloud.md`; `npm run check:codex-cloud` verifies that the runtime pins, install commands, offline defaults, and documentation stay aligned. For executable phone-chrome changes, use `verify:phone-chrome` before the broad UI gate. It checks installed-lock parity, then selects focused contracts and Playwright owners from the changed paths; shared foundations add `verify:ui` last. Documentation-only scopes run only documentation guards. `audit:final-merge` is local-only unless both `--providers` and `ALLOW_PROVIDER_READS=true` are supplied. diff --git a/docs/testing.md b/docs/testing.md index 4b390088d1..6801fb9272 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -27,7 +27,10 @@ explicit `run-authenticated-live-tests` dispatch confirmation, records the run a guard and live-test steps. The secret-bearing job runs only from `refs/heads/main` and checks out that trusted ref; it never runs on a push, pull request, or schedule. This suite is not read-only: the confirmation explicitly authorizes bounded E2E-user sign-in/sign-out, -test requests, and production rate-limit row updates. +test requests, and production rate-limit row updates. A connected-only GitHub PAT exception +does not authorize provider tests, provider credentials, deployment, or production data access; +it is limited to the documented, exact GitHub connector-gap operation in +`docs/codex-cloud.md`. ## Commands diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index fd0ed76a34..e13c345226 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -32,6 +32,7 @@ export const providerCredentialVariables = Object.freeze([ "RAILWAY_TOKEN", "GH_TOKEN", "GITHUB_TOKEN", + "CODEX_CLOUD_GITHUB_PAT", "GITLAB_TOKEN", "GLAB_TOKEN", "CODEX_TRIGGER_TOKEN", @@ -311,6 +312,8 @@ export function validateCodexCloudSetup() { const nvmVersion = read(".nvmrc").trim(); 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 patDelete = read("scripts/delete-codex-cloud-branch-with-pat.sh"); const guide = read("docs/codex-cloud.md"); const agents = read("AGENTS.md"); const envExample = read(".env.example"); @@ -393,8 +396,68 @@ export function validateCodexCloudSetup() { /ensure-codex-cloud-git-remote\.mjs/, "Maintenance must preserve the safe origin remote.", ); + requireMatch( + errors, + commandShims, + /nvm which/, + "Cloud command shims must resolve the selected Node version through nvm.", + ); + requireMatch( + errors, + commandShims, + /mkdir -p "\$HOME\/\.local\/bin"/, + "Cloud command shims must create their destination directory.", + ); + requireMatch( + errors, + commandShims, + /\.clinical-kb-codex-cloud\.sh/, + "Cloud command shims must load the generated profile.", + ); + if (!commandShims.includes('exec "$node_bin/$command_name" "\\$@"')) { + errors.push("Cloud command shims must execute absolute Node commands."); + } + requireMatch( + errors, + patDelete, + /CODEX_CLOUD_ACCESS_PROFILE.*connected/, + "PAT deletion helper must require the connected profile.", + ); + requireMatch( + errors, + patDelete, + /\[\[ "\$branch" != -\* \]\]/, + "PAT deletion helper must reject option-like branch names.", + ); + requireMatch(errors, patDelete, /git check-ref-format --branch/, "PAT deletion helper must validate branch names."); + requireMatch( + errors, + patDelete, + /git remote get-url --push --all origin/, + "PAT deletion helper must validate effective push URLs.", + ); + requireMatch(errors, patDelete, /GIT_ASKPASS/, "PAT deletion helper must use a temporary askpass program."); + requireMatch( + errors, + patDelete, + /core\.hooksPath=\/dev\/null/, + "PAT deletion helper must disable Git hooks before the token-bearing push.", + ); + requireMatch( + errors, + patDelete, + /https:\/\/github\.com\/BigSimmo\/Database\.git/, + "PAT deletion helper must require the credential-free origin.", + ); requireMatch(errors, guide, /bash scripts\/setup-codex-cloud\.sh/, "The guide must provide the setup command."); + requireMatch( + errors, + guide, + /install-codex-cloud-command-shims\.sh/, + "The guide must document the command-shim workaround.", + ); requireMatch(errors, guide, /CODEX_CLOUD_ACCESS_PROFILE=connected/, "The guide must document connected access."); + requireMatch(errors, guide, /CODEX_CLOUD_GITHUB_PAT/, "The guide must document the narrowly scoped PAT exception."); requireMatch(errors, guide, /GitHub connector/, "The guide must document GitHub connector access."); try { parseMcpServerMetadata(mcp); @@ -444,7 +507,11 @@ function commandVersion(command, args, expectedPattern) { } function repositoryCommand(command, args) { - const result = spawnSync(command, args, { cwd: repoRoot, encoding: "utf8", shell: false }); + const result = spawnSync(command, args, { + cwd: repoRoot, + encoding: "utf8", + shell: false, + }); if (result.status === 0) return null; const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim().split(/\r?\n/).at(-1); return `${command} ${args.join(" ")} failed: ${output || `exit ${result.status}`}`; @@ -474,7 +541,11 @@ 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 })) { + for (const [name, browserType] of Object.entries({ + chromium, + firefox, + webkit, + })) { if (!executableFile(browserType.executablePath())) { errors.push(`${name} browser executable is unavailable.`); } diff --git a/scripts/delete-codex-cloud-branch-with-pat.sh b/scripts/delete-codex-cloud-branch-with-pat.sh new file mode 100644 index 0000000000..13125bb69d --- /dev/null +++ b/scripts/delete-codex-cloud-branch-with-pat.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +usage() { + printf 'Usage: %s \n' "${0##*/}" >&2 + exit 64 +} + +fail() { + printf '[codex-cloud: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." + +case "$branch" in + main|master|develop|release|release/*|HEAD|HEAD/*|refs/*|*..*|*~*|*@\{*|*\\*|*\ *|"") + fail "Refusing to delete a protected or invalid branch name." + ;; +esac +git check-ref-format --branch "$branch" >/dev/null || fail "Invalid branch name." +expected_origin="https://github.com/BigSimmo/Database.git" +[[ "$(git config --get remote.origin.url 2>/dev/null || true)" = "$expected_origin" ]] || + fail "origin must be the credential-free BigSimmo/Database URL." +push_urls="$(git remote get-url --push --all origin)" || fail "Could not read origin push URLs." +[[ -n "$push_urls" ]] || fail "origin has no push URL." +while IFS= read -r push_url; do + [[ "$push_url" = "$expected_origin" ]] || fail "origin push URL must be the credential-free BigSimmo/Database URL." +done <<< "$push_urls" + +askpass="$(mktemp)" +trap 'rm -f "$askpass"; unset CODEX_CLOUD_GITHUB_PAT' EXIT +cat > "$askpass" <<'EOF' +#!/bin/sh +case "$1" in + *Username*) printf '%s\n' x-access-token ;; + *Password*) printf '%s\n' "$CODEX_CLOUD_GITHUB_PAT" ;; + *) exit 1 ;; +esac +EOF +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" diff --git a/scripts/install-codex-cloud-command-shims.sh b/scripts/install-codex-cloud-command-shims.sh new file mode 100644 index 0000000000..6a473bb4bd --- /dev/null +++ b/scripts/install-codex-cloud-command-shims.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +fail() { + printf '[codex-cloud:shims] 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" + +expected_node_major="$(tr -cd '0-9' < .node-version)" +[[ -n "$expected_node_major" ]] || fail "Could not read the Node major from .node-version." + +runtime_profile="$HOME/.clinical-kb-codex-cloud.sh" +[[ -f "$runtime_profile" ]] || fail "The generated Cloud runtime profile is unavailable; run setup first." + +# shellcheck source=/dev/null +source "$runtime_profile" +command -v nvm >/dev/null 2>&1 || fail "nvm is unavailable after loading the Cloud runtime profile." + +node_bin="$(dirname "$(nvm which "$expected_node_major")")" +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}." + cat > "$HOME/.local/bin/$command_name" < { }; expect(configuredProviderCredentialNames(env)).toEqual(["OPENAI_API_KEY", "CROSS_TENANT_SERVICE_ROLE_KEY"]); + expect( + configuredProviderCredentialNames({ + CODEX_CLOUD_GITHUB_PAT: "never-print-this", + }), + ).toEqual(["CODEX_CLOUD_GITHUB_PAT"]); expect(obsoleteNpmProxyVariables(env)).toEqual(["npm_config_https_proxy"]); }); @@ -156,9 +161,12 @@ describe("Codex Cloud environment contract", () => { PLAYWRIGHT_OFFLINE_MODE: "true", }; expect(validateCodexCloudEnvironment(offline)).toEqual([]); - expect(validateCodexCloudEnvironment({ ...offline, NEXT_PUBLIC_DEMO_MODE: "false" })).toContain( - "NEXT_PUBLIC_DEMO_MODE must be true in offline mode.", - ); + expect( + validateCodexCloudEnvironment({ + ...offline, + NEXT_PUBLIC_DEMO_MODE: "false", + }), + ).toContain("NEXT_PUBLIC_DEMO_MODE must be true in offline mode."); expect( validateCodexCloudEnvironment({ ...offline, @@ -191,7 +199,11 @@ describe("Codex Cloud environment contract", () => { OPENAI_API_KEY: secret, }, { - origin: { configured: true, repositoryMatch: true, credentialsEmbedded: false }, + origin: { + configured: true, + repositoryMatch: true, + credentialsEmbedded: false, + }, railwayCliAvailable: true, codexCliAvailable: true, safeGitHelper: true, @@ -253,7 +265,10 @@ describe("Codex Cloud environment contract", () => { it("requires hosted Railway OAuth and project-scoped read-only Supabase MCP", () => { const valid = JSON.stringify({ mcpServers: { - railway: { type: "http", url: expectedMcpConfiguration.railwayUrl.replace(/\/$/, "") }, + railway: { + type: "http", + url: expectedMcpConfiguration.railwayUrl.replace(/\/$/, ""), + }, supabase: { type: "http", url: `${expectedMcpConfiguration.supabaseUrl}?project_ref=${expectedMcpConfiguration.supabaseProjectRef}&read_only=true&features=${expectedMcpConfiguration.supabaseFeatures.join(",")}`, @@ -281,6 +296,14 @@ describe("Codex Cloud environment contract", () => { 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"); + const commandShims = readFileSync( + new URL("../scripts/install-codex-cloud-command-shims.sh", import.meta.url), + "utf8", + ); + const patDelete = readFileSync( + new URL("../scripts/delete-codex-cloud-branch-with-pat.sh", import.meta.url), + "utf8", + ); expect(setup).toContain("if ! grep -Fq '.clinical-kb-codex-cloud.sh'"); expect(setup).toContain('if [[ "$actual_version" != "$expected_version" ]]'); expect(setup).toContain('"$HOME/.bash_profile"'); @@ -303,6 +326,15 @@ describe("Codex Cloud environment contract", () => { expect(setup).toContain("DATABASE_URL"); expect(setup).toContain("CODEX_CLOUD_SETUP_STOP_AFTER_POLICY"); 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('[[ "$branch" != -* ]]'); + expect(patDelete).toContain("git check-ref-format --branch"); + expect(patDelete).toContain("git remote get-url --push --all origin"); + expect(patDelete).toContain("GIT_ASKPASS"); + expect(patDelete).toContain("core.hooksPath=/dev/null"); }); it("writes managed shell policy behaviorally and preserves unrelated Codex config", () => { @@ -415,7 +447,9 @@ describe("Codex Cloud environment contract", () => { const atomicPath = path.join(atomicHome, ".codex/config.toml"); writeFileSync(atomicPath, atomicConfig); const failingMktemp = path.join(atomicTools, "mktemp"); - writeFileSync(failingMktemp, "#!/usr/bin/env sh\nexit 1\n", { mode: 0o755 }); + writeFileSync(failingMktemp, "#!/usr/bin/env sh\nexit 1\n", { + mode: 0o755, + }); const atomic = runSetupPolicyOnly(atomicHome, { CODEX_CLOUD_ACCESS_PROFILE: "offline", PATH: [atomicTools, path.dirname(process.execPath), process.env.PATH].filter(Boolean).join(path.delimiter), @@ -464,12 +498,20 @@ describe("Codex Cloud environment contract", () => { }); it("does not describe a source-only runtime as fully browser-ready", () => { - expect(codexCloudValidationScope({ runtime: true, environment: true, browserInstallSkipped: true })).toBe( - "static, environment, and source-only runtime (browser validation skipped)", - ); - expect(codexCloudValidationScope({ runtime: true, environment: true, browserInstallSkipped: false })).toBe( - "static, environment, and runtime", - ); + expect( + codexCloudValidationScope({ + runtime: true, + environment: true, + browserInstallSkipped: true, + }), + ).toBe("static, environment, and source-only runtime (browser validation skipped)"); + expect( + codexCloudValidationScope({ + runtime: true, + environment: true, + browserInstallSkipped: false, + }), + ).toBe("static, environment, and runtime"); }); it("distinguishes executable files from missing paths", () => { @@ -486,9 +528,11 @@ describe("Codex Cloud environment contract", () => { expect(pythonWorkerImportError(process.execPath, run as typeof spawnSync)).toBeNull(); expect(invocation).toEqual([process.execPath, "-c", `import ${pythonWorkerImports.join(", ")}`]); - expect(pythonWorkerImportError(process.execPath, (() => ({ status: 1 })) as unknown as typeof spawnSync)).toContain( - "Python worker imports failed", - ); + expect( + pythonWorkerImportError(process.execPath, (() => ({ + status: 1, + })) as unknown as typeof spawnSync), + ).toContain("Python worker imports failed"); }); });