diff --git a/.codex/config.toml b/.codex/config.toml index 90047c779..a1959415b 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -1,13 +1,15 @@ -# Secret-free, project-scoped MCP registrations for trusted Codex hosts. +# Secret-free MCP registrations for trusted Codex Desktop/CLI hosts. # OAuth credentials remain in each host credential store — never commit them here. # # checked-in policy: # - enabled = false so ordinary/offline Codex sessions do not initialize providers +# (check:codex-cloud fails if any tracked entry is enabled; opt in via +# $CODEX_HOME/config.toml or a never-committed local edit) # - 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 -# Hosted Cloud tools come from installed OAuth plugins/connectors; `.mcp.json` is a cross-client template, not runtime proof. +# Hosted ChatGPT/Codex tools come only from installed, authenticated apps; this file is not runtime proof. [mcp_servers.figma_cloud] url = "https://mcp.figma.com/mcp" @@ -21,7 +23,9 @@ url = "https://mcp.supabase.com/mcp?project_ref=sjrfecxgysukkwxsowpy&read_only=t enabled = false default_tools_approval_mode = "prompt" -[mcp_servers.railway_cloud] +# Named `railway` (not `railway_cloud`) so `codex mcp login railway` matches the +# entry name. Other providers keep the `_cloud` suffix; this asymmetry is intentional. +[mcp_servers.railway] url = "https://mcp.railway.com" enabled = false default_tools_approval_mode = "writes" diff --git a/AGENTS.md b/AGENTS.md index 52f8393de..a05beab18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -366,7 +366,7 @@ surface, read `docs/rag-behaviour/` (README → behaviour-map → refuted-approa - Production services `Database` (Next.js app tier, serves `https://psychiatry.tools`) and `worker` (ingestion) auto-deploy from `BigSimmo/Database` pushes to `main`; the `staging` environment runs the `app` service. - The older Railway project `clinical-kb` (`4361c04f-dd3c-4ee9-9e97-49e4e5707b70`) is superseded with zero active deployments; treat it as stale — never `railway link` to it or deploy there. - The similarly named Supabase project `Clinical KB Database` is the database/auth tier, not a Railway project; see "Supabase project safety" above. -- Railway CLI/MCP auth uses `RAILWAY_API_TOKEN` (personal account token; see `.env.example`). The project-scoped `RAILWAY_TOKEN` is for CI deploys only and cannot list or link projects. The project-scoped Railway MCP server is registered in `.mcp.json`. +- Railway CLI token auth uses `RAILWAY_API_TOKEN` (personal account token; see `.env.example`). The project-scoped `RAILWAY_TOKEN` is for CI deploys only and cannot list or link projects; Cloud runtime acceptance no longer installs or probes the CLI, so that substitution rule is documentation-enforced until an operator workflow reintroduces CLI checks. Desktop/CLI MCP uses the secret-free `railway` entry (enable in `$CODEX_HOME/config.toml` or via a never-committed local edit — never commit `enabled = true`) plus `codex mcp login railway`; neither repository MCP file activates a hosted ChatGPT/Codex app. - Railway deploys and mutations fall under the "API and provider confirmation boundary" below; verify target project/environment IDs before any mutation. @@ -876,11 +876,13 @@ Use `docs/codex-cloud.md` as the environment contract: 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. + `.codex/config.toml` keeps Desktop/CLI MCP entries `enabled = false` in git (`check:codex-cloud` + fails if any tracked entry is enabled). Opt in locally via `$CODEX_HOME/config.toml` (preferred) + or a never-committed project-file edit, then `codex mcp login railway`. Cloud setup never writes + Railway or Supabase MCP registrations to `$CODEX_HOME`. Hosted ChatGPT/Codex requires an + installed, workspace-authorized, OAuth-authenticated app, and a fresh task must prove the callable + inventory with read-only identity calls. Root `.mcp.json` is a static cross-client template, not + hosted 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. @@ -893,16 +895,22 @@ Use `docs/codex-cloud.md` as the environment contract: separately without printing secret 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. + never values. Treat exit `1` / `FAIL`+`STOP` as a hard stop for any unexpected inherited name. + Only exit `2` / `FAIL-KNOWN` for `OPENAI_BASE_URL` alone may use the restricted + profile-and-shim continue path; do not generalize that allowance. Exit `2` is still a failed raw + boundary — future automation must not treat non-1 as success or as a blind retry. That name can + redirect OpenAI-bound traffic, so never invoke OpenAI clients from the raw parent or any binary + that bypasses the profile/`node`/`npm`/`npx` scrub. 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 @@ -915,9 +923,17 @@ 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. -- 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 +- The active hosted workspace is **Personal Pro**. Use Railway's installed official ChatGPT app + with browser OAuth and **Allow read actions**; Personal Pro does not provide the dedicated-group + RBAC or per-tool action disabling assumed by Enterprise/Edu instructions. Prove Railway with the + callable tool inventory and a read-only identity/project-list call. Repository setup and local + MCP config cannot activate it, and the Codex Cloud connector page currently offers no Railway + connector. Use the documented split control plane: Codex Cloud for code and its native GitHub + connector, ChatGPT web for Railway and project-scoped read-only Supabase. Every provider change + still requires explicit approval. Enterprise/Edu custom-app controls are a future governance + option, not the current workspace classification. + CLI token auth is a separate operator capability: it requires a separately installed Railway 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. diff --git a/docs/README.md b/docs/README.md index f0fd281ec..63bb45c5d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -125,6 +125,7 @@ as it was on that date; supersede with a new dated document rather than editing. - [audit/audit-handover-2026-07-14.md](audit/audit-handover-2026-07-14.md) — multi-skill repository audit findings inventory - [audit/audit-remediation-plan-2026-07-14.md](audit/audit-remediation-plan-2026-07-14.md) — sequenced remediation plan for the 2026-07-14 audit, with the 2026-07-17 reconciliation - [audit/design-audit-2026-07-17.md](audit/design-audit-2026-07-17.md) — repository-wide design, accessibility, and interaction audit +- [audit/cloud-connection-acceptance-2026-08-05.md](audit/cloud-connection-acceptance-2026-08-05.md) — hosted versus local MCP boundary acceptance, Personal Pro split control plane, and remaining Cloud launcher blockers - [current-clinical-work-brief.md](current-clinical-work-brief.md) — ledger #063 product/privacy/persistence brief (decision only, no implementation) - [factsheets-reading-model-brief.md](factsheets-reading-model-brief.md) — ledger #041 reading-model decision (no second Factsheets mode) - [source-governance-refresh-worklist-2026-07-22.md](source-governance-refresh-worklist-2026-07-22.md) — ledger #022 worklist and BMJ attestation policy status diff --git a/docs/agents-guide.md b/docs/agents-guide.md index ed5d43ba9..a862286cf 100644 --- a/docs/agents-guide.md +++ b/docs/agents-guide.md @@ -44,7 +44,7 @@ below defers to it, so rules live in one place and cannot drift. | **Codex** (OpenAI) | Primary PR code-review + automatic resolve | AGENTS.md "Codex review" sections, `docs/codex-review-protocol.md`, `docs/codex-prompt-playbook.md`, `.github/workflows/codex-autofix-review-comments.yml` | | **Claude Code** | Interactive dev; scoped review subagents + workflow skills | `.claude/` (agents, skills, hooks), `.github/workflows/claude.yml` | | **Cursor** | Editor skills + project MCP (Supabase, Context7, …) | `.cursor/` (skills, `mcp.json`) | -| **Railway MCP** | Deploy/logs/env **names** (project-scoped) | Root `.mcp.json` (`@railway/cli` mcp); needs `RAILWAY_API_TOKEN` — not `RAILWAY_TOKEN` | +| **Railway MCP** | Desktop/CLI template; hosted app is separate | Root `.mcp.json` / `.codex/config.toml` use `https://mcp.railway.com` with OAuth; hosted ChatGPT/Codex requires a workspace-installed app | | **CodeRabbit** | Advisory PR review (never blocking) | `.coderabbit.yaml` (`commit_status: false`) | | **`.agents/`** | Home-grown single-word skill catalogue | `.agents/skills/catalog.json`; list with `npm run skills` | @@ -58,13 +58,13 @@ servers** per session (tool-schema token bloat degrades agents). Use registered MCPs before opening dashboards when the task is read-only inspection. Writes, secret rotations, and hosted mutations stay confirmation-gated per `AGENTS.md`. -| Server | Config | Use for | Do not | -| --------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| **Supabase** (read-only) | `.cursor/mcp.json` — pinned `project_ref=sjrfecxgysukkwxsowpy`, `read_only=true` | `search_docs`, advisors, read SQL, schema inspection | Print secret values; raw-edit retrieval RPCs via `execute_sql`; Auth DB connection-cap (`#011`) — **dashboard only** | -| **Railway** | Root `.mcp.json` | Deploy status, service logs, env **names**/presence | Confuse `RAILWAY_API_TOKEN` (personal) with CI `RAILWAY_TOKEN`; mutate without approval | -| **Context7** | `.cursor/mcp.json` → `https://mcp.context7.com/mcp` (+ Cursor `context7-plugin`) | Versioned docs for **Tailwind 4, Zod 4, Playwright, Vitest, React 19, `@supabase/supabase-js`** (peers; not exhaustive). Optional higher limits: set `CONTEXT7_API_KEY` (see below) | Next.js 16 — always use `node_modules/next/dist/docs/` (AGENTS.md). Do not invent App Router APIs from training data; never commit the API key | -| **Chrome DevTools** | `.cursor/mcp.json` → `npx -y chrome-devtools-mcp@1.6.0` | CLS/LCP/console/network while implementing redesigns (`#147`, `#162`–`#164`, Therapy Compass) | Don't leave it always-on with Browse + Playwright MCP (token bloat). Use for perf/debug passes | -| **GitHub Checks / Actions** | Operator approval pending | PR check visibility when `gh pr checks` returns empty totals | Bot `update-branch`; broaden scopes beyond Checks/Actions read | +| Server | Config | Use for | Do not | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| **Supabase** (read-only) | `.cursor/mcp.json` — pinned `project_ref=sjrfecxgysukkwxsowpy`, `read_only=true` | `search_docs`, advisors, read SQL, schema inspection | Print secret values; raw-edit retrieval RPCs via `execute_sql`; Auth DB connection-cap (`#011`) — **dashboard only** | +| **Railway** | Desktop/CLI: root `.mcp.json` / `.codex/config.toml` (`railway` + OAuth). Hosted ChatGPT/Codex: workspace-installed app only — repository MCP files are not a read path there | Deploy status, service logs, env **names**/presence (Desktop/CLI MCP or hosted app tools) | Treat root `.mcp.json` as hosted proof; confuse `RAILWAY_API_TOKEN` (personal) with CI `RAILWAY_TOKEN`; mutate without approval | +| **Context7** | `.cursor/mcp.json` → `https://mcp.context7.com/mcp` (+ Cursor `context7-plugin`) | Versioned docs for **Tailwind 4, Zod 4, Playwright, Vitest, React 19, `@supabase/supabase-js`** (peers; not exhaustive). Optional higher limits: set `CONTEXT7_API_KEY` (see below) | Next.js 16 — always use `node_modules/next/dist/docs/` (AGENTS.md). Do not invent App Router APIs from training data; never commit the API key | +| **Chrome DevTools** | `.cursor/mcp.json` → `npx -y chrome-devtools-mcp@1.6.0` | CLS/LCP/console/network while implementing redesigns (`#147`, `#162`–`#164`, Therapy Compass) | Don't leave it always-on with Browse + Playwright MCP (token bloat). Use for perf/debug passes | +| **GitHub Checks / Actions** | Operator approval pending | PR check visibility when `gh pr checks` returns empty totals | Bot `update-branch`; broaden scopes beyond Checks/Actions read | ### Context7 API key (optional) diff --git a/docs/audit/cloud-connection-acceptance-2026-08-05.md b/docs/audit/cloud-connection-acceptance-2026-08-05.md new file mode 100644 index 000000000..569d2adb4 --- /dev/null +++ b/docs/audit/cloud-connection-acceptance-2026-08-05.md @@ -0,0 +1,157 @@ +# Cloud connection acceptance — 2026-08-05 + +## Root cause and durable boundary + +Hosted ChatGPT/Codex does not register tools from repository `.mcp.json`, `.codex/config.toml`, +or container setup. Those files remain secret-free Desktop/CLI templates. Hosted tools require an +installed, workspace-authorized, OAuth-authenticated app and must be verified in a fresh task. +Cloud setup also omits Railway CLI: it is unnecessary for the hosted-app path and its binary +postinstall can fail when GitHub release downloads are unavailable. + +## Hosted Railway setup and observed controls + +- Railway's official hosted app was already installed and OAuth-connected in the available ChatGPT + Pro workspace. Its permission was tightened from **Allow low-risk actions** to **Allow read + actions**, so reads can run automatically and every change must ask. No static token, header, or + repository secret was supplied. +- The available workspace is personal Pro, not the Enterprise/Edu workspace assumed by the plan. + It offers the global read-versus-change control but not per-tool disable or dedicated-group RBAC. + If Enterprise/Edu governance is required, an admin must enable Developer Mode and either govern + the official app or create `Railway — Database` at `https://mcp.railway.com`, run Scan Tools, + restrict it to a dedicated group, and disable write tools individually. +- A current Codex settings review independently confirmed the **PRO** classification. Its connector + page shows GitHub connected to `BigSimmo` and offers GitHub, Slack, and Linear; it provides no + Railway or Supabase connector control. This is a product-surface limit, not a repository setup + failure. +- The installed app exposed these read actions: `Fetch-docs`, `Get-feature-flag`, `Get-logs`, + `Get-service-config`, `Get-service-metrics`, `Get-status`, `List-deployments`, `List-domains`, + `List-feature-flags`, `List-projects`, `List-services`, `List-variables`, `List-workspaces`, + `Search-docs`, and `Whoami`. `List-variables` reports names only. +- It exposed these change actions, all approval-gated by **Allow read actions**: `Accept-deploy`, + `Create-deployment`, `Create-project`, `Create-service`, `Delete-feature-flag`, `Generate-domain`, + `Railway-agent`, `Redeploy`, `Set-feature-flag`, `Set-variables`, and `Update-service`. None was + invoked. +- Railway OAuth metadata advertises `openid`, `profile`, `email`, `offline_access`, and + `workspace:member`. An elapsed-time follow-up from a new ChatGPT Work task displayed **Your + Railway connection has expired** before `whoami` could run and offered the normal **Reconnect** + flow. The connection error identified the cause as `oauth_refresh_token_missing`. Reauthentication + through the normal Railway GitHub login completed successfully, after which `whoami`, + `list-projects`, and `get-status` passed immediately. Treat periodic reauthentication as required + for this Personal Pro app path; never introduce a static-token workaround. + +## Acceptance status + +| Surface | Required proof | Status | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Repository boundary | Setup never writes Railway/Supabase MCP servers; local templates stay secret-free | PASS: focused tests and static checker pass; Cloud setup also no longer installs Railway CLI | +| GitHub | Connector reads `BigSimmo/Database`; exact commit is published and verified | ChatGPT PASS via `mcp__codex_apps__github_get_repo`; draft PR #1617 was published through the GitHub connector. Fresh Codex Cloud FAIL: no GitHub tool exposed at tested head `7d485f88db391cc7e8e73c57ddbde61f532375fc` | +| Railway | Exact tools callable in a fresh ChatGPT chat and fresh Codex Cloud task; read-only identity/project/service checks pass | ChatGPT PASS via `railway_whoami`, `railway_get_status`, `railway_list_projects`, and `railway_list_services`; project `Database` and services `Database` and `worker` were visible with `SUCCESS` status. Fresh Codex Cloud FAIL: no Railway tool exposed | +| Supabase | Existing app exposes only project-scoped read-only metadata without row queries | ChatGPT PASS via `mcp__codex_apps__supabase_list_projects`: `sjrfecxgysukkwxsowpy`, `Clinical KB Database`, `ACTIVE_HEALTHY`; no schema/table/row/log call. Codex Cloud FAIL: no Supabase tool exposed | +| Raw Cloud shell | Only five documented non-secret values; `OPENAI_BASE_URL` absent before profiles/shims | Environment UI has exactly the five documented values and no `OPENAI_BASE_URL`, but fresh raw-shell FAIL still reports the inherited name. This is a launcher/workspace defect, not repository state | +| OAuth durability | Second read-only Railway call succeeds after one hour, or reauthentication is documented | REAUTH VERIFIED: the elapsed-time task reported `oauth_refresh_token_missing`; normal Railway GitHub OAuth restored access and immediate read-only validation passed. Periodic reconnect remains required; no token workaround was added | + +## Personal Pro operating workarounds + +| Blocker | Safe workaround applied | Functional boundary | +| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| No Personal Pro group RBAC or per-tool disabling | Railway app policy is **Allow read actions**; every change remains approval-gated | Maximum available Pro control; not equivalent to Enterprise/Edu RBAC | +| Railway and Supabase absent from Codex Cloud connectors | Use a split control plane: Codex Cloud for repository/GitHub work; ChatGPT web for official Railway OAuth and project-scoped read-only Supabase | Providers remain usable without copying tokens; a single Codex Cloud task still cannot call them | +| Raw `OPENAI_BASE_URL` injected although absent in environment UI | Keep the name-only raw probe fail-closed, then use the generated profile, Codex shell policy, and `node`/`npm`/`npx` shims that remove provider variables before ordinary work | Normal repository commands are sanitized and functional; the raw parent-process defect remains visible for OpenAI support | +| Railway OAuth expires instead of refreshing in this app path | Use the app's normal **Reconnect** flow when prompted; the existing GitHub sign-in restored access without static or shared tokens | Safe continuity workaround; Railway login/consent remains user-controlled | + +This operating mode favors maximum safe functionality on Personal Pro. It does not relabel the +Codex Cloud provider-tool acceptance failure as success. + +The first fresh Codex Cloud acceptance attempt did not reach the agent: setup failed while +`@railway/cli@5.30.4` tried to download its binary from GitHub Releases and received +`ENETUNREACH`. The corrected fresh task at +`https://chatgpt.com/codex/cloud/tasks/task_e_6a7303bcdd988322bc979d5d2c0f946f` passed setup and ran at +exact head `76dfe85fa93787b3845d0bd460aa18ff753ca2ca`. It reported: + +- `[Codex Cloud Check] PASS: static and environment Cloud contracts match.` +- `[Runtime Check] PASS: Node runtime 24.19.0 matches required Node 24.x.` +- `[Runtime Check] PASS: npm runtime 11.17.0 matches required npm 11.x.` +- `[Codex Cloud Check] PASS: static, environment, and runtime Cloud contracts match.` +- `git.expected_base_ancestor=true` and `git.checkout_freshness=verified` for base + `9d4a28c16e189256d2e2b1fc6edfb351138837cc`. +- `[Codex Cloud Raw Env] FAIL: inherited provider variable names: OPENAI_BASE_URL` before profiles, + npm, Node, or shims. +- No callable Railway, GitHub, or Supabase tools: MCP resources/templates were empty and focused + tool discovery returned zero tools. No provider call was attempted. + +A second, independently provisioned environment-attached task at +`https://chatgpt.com/codex/cloud/tasks/task_e_6a7313d1364483228b7642cb942e674c` repeated acceptance at +exact head `7d485f88db391cc7e8e73c57ddbde61f532375fc`. It passed locked installation, static and runtime +Cloud contracts, Node `24.19.0`, npm `11.17.0`, installed-lock parity, and expected-base ancestry and +freshness for `9d4a28c16e189256d2e2b1fc6edfb351138837cc`. The raw probe again failed only on the inherited +name `OPENAI_BASE_URL`, and the task again exposed no callable Railway, GitHub, or Supabase tools. + +A separate fresh ChatGPT Work task did expose the hosted apps and completed only safe metadata +reads: GitHub authenticated as `BigSimmo`; Supabase project `Clinical KB Database` +(`sjrfecxgysukkwxsowpy`) was `ACTIVE_HEALTHY`; Railway authenticated as `bigsimmo`, project +`Database` was visible, and services `Database` and `worker` were `SUCCESS`. No logs, variables, +SQL, row contents, or provider writes were requested. This validates the Personal Pro split-control +plane workaround, not single-task Codex Cloud provider acceptance. + +After the Railway token expired, a direct connector call returned `UNAUTHORIZED` with reason +`oauth_refresh_token_missing`. The normal **Reconnect** flow completed through the existing GitHub +sign-in, ChatGPT reported **Railway is now connected**, and new read-only calls again authenticated +as `bigsimmo`. Project `Database` remained visible and both production services remained `SUCCESS`. +GitHub repository access and project-scoped Supabase health also passed again in the same task. + +This proves the repository setup fix and also proves that repository code cannot close either +remaining hosted blocker. The environment UI and launcher disagree about `OPENAI_BASE_URL`, and +the Codex Cloud product did not project the installed ChatGPT apps into the task tool inventory. + +## Prior branch and blocked-result disposition + +- GitHub connector inspection found PR #1613 still open at current head + `68c1f17909802c1d0e7b8e999de2259de4072dc7`; its description still cites stale acceptance head + `8a4ad8ff53072bc796c81c0039006222bca6c068` and retains the old claim that repository setup enables + connected Railway/Supabase MCPs. This repair is isolated from that unrelated PR and does not + merge, close, or rewrite it. +- Connector commit search did not find the Cloud-only `fe31128` GitHub preflight commit. The bounded + behavior was recreated as `check:github-shell-access`: the plain npm entry is always offline + `--self-test` (ambient opt-in cannot override it); the live `gh` shell fallback is + `check:github-shell-access:live` (or a direct script invocation with `--allow-provider` / + `ALLOW_GITHUB_SHELL_ACCESS=true` and without `--self-test`). `GH_AUTH_MISSING` therefore says + nothing about the hosted GitHub connector, which passed independently. +- The original blocked report's `OPENAI_BASE_URL` condition was removed in Codex environment + settings. `Database - connected` now contains exactly the five documented non-secret variables, + but a new task still inherited the variable name. Escalate this mismatch to Codex environment + support; repository scripts correctly fail closed and must not hide it. + +## Tool-only gateway fallback design + +Do not deploy this fallback unless OpenAI confirms that Codex Cloud can install the resulting app +and the user separately approves a hosting target and Railway OAuth registration. + +- **Hosting boundary:** a new isolated service outside Railway project `Database`, with its own + environment, network policy, secret store, persistence, monitoring, and public HTTPS callback. + Do not host it in either production service. The exact provider, region, URL, and data-retention + policy remain approval-required choices. +- **Identity:** one Railway authorization-code grant per ChatGPT user, using PKCE and refresh only + if Railway's registered-client requirements allow it. Store client credentials and refresh tokens + encrypted server-side; never return them to ChatGPT or place them in repository/Cloud variables. + Railway client registration, redirect URIs, supported scopes, and revocation behavior must be + confirmed from Railway before implementation. Do not invent endpoints or broader scopes. +- **Authorization:** hard-code the only allowed project ID as + `5deaad0b-675a-4c13-978e-5ca2b5b877f9`; reject every other project/workspace after resolving the + authenticated Railway identity. A workspace match alone is insufficient. +- **v1 tool surface:** expose only `whoami`, `list-projects`, `list-services`, + `list-feature-flags`, and `get-feature-flag`. Return bounded non-secret metadata. Do not expose + logs, variables, service configuration, deployments, agent tools, mutations, or generic HTTP/API + forwarding in v1. +- **Operational controls:** authenticated per-user sessions, encrypted token storage, append-only + audit events, correlation IDs, redaction, per-user/project rate limits, response-size limits, + connection and request timeouts, strict schemas, and denial-by-default for new tools. Any later + write tool requires a separate review, explicit per-call approval, and a non-production canary. + +Do not mark Railway accepted because it appears in workspace settings or repository files. Success +requires a callable tool in both fresh hosted contexts. If direct Railway exposure or safe project +isolation fails, stop before deployment and design a separate tool-only gateway with per-user OAuth +and PKCE/refresh, exact project allowlisting, read-only v1 tools, encrypted server-side tokens, +audit logs, redaction, rate limits, bounded log metadata, request timeouts, and explicit write +approvals. Its hosting target and Railway OAuth registration require separate approval before code. + +References: [OpenAI Developer Mode](https://help.openai.com/en/articles/12584461-developer-mode-and-full-mcp-connectors-in-chatgpt-beta), [Apps in ChatGPT](https://help.openai.com/en/articles/11487775-connectors-in-chatgpt/), [Railway MCP server](https://docs.railway.com/ai/mcp-server), and [Railway OAuth token lifecycle](https://docs.railway.com/integrations/oauth/login-and-tokens). diff --git a/docs/codebase-index.md b/docs/codebase-index.md index adc6096dd..3331ee809 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -34,19 +34,19 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map Smaller top-level directories that are easy to miss: -| Path | Purpose | -| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `data/` | Committed clinical **snapshot exports** loaded at runtime by `src/lib/` (differentials, forms, medications, services, specifiers). Regenerate via the matching `scripts/import-*-export.ts` / `build-*-index.mjs`; do not hand-edit. Distinct from `src/data/`, which holds hand-authored static content. | -| `eslint-rules/` | Repo-specific lint rules enforced by `npm run lint` (button wiring, hardcoded hex, type/icon scale, z-index ladder) | -| `mockups/` | Notes for the design-scratch routes under `src/app/mockups/` (the routes themselves 404 in production) | -| `plugins/` | `plugins/clinical-kb/` Codex plugin manifest and workflow skill | -| `.agents/` | Single-word skill catalogue (`npm run skills`, validated by `npm run check:skills`) | -| `.claude/` | Claude Code agents, skills, hooks, settings — plus the `.claude/worktrees/` working copies | -| `.codex/` | Project-scoped Codex host config; tracked `config.toml` has disabled, secret-free Figma, Supabase, Railway, and Sentry MCP registrations. Figma/Railway/Sentry use `"writes"`; read-only Supabase uses `"auto"`. Runtime Cloud MCP stays `.mcp.json`; all `.codex/*` stay ignored; OAuth stays in the host credential store. | -| `.cursor/` | Cursor project rules and local-agent configuration | -| `.design-sync/` | Generated design-system package metadata, validation notes, and project-sync artifacts | -| `.githooks/` | Installed by `npm install`; `pre-push` runs `scripts/guard-push.mjs` (format, auto-merge race, drift staleness) | -| `.vscode/` | Shared VS Code workspace recommendations and settings | +| Path | Purpose | +| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `data/` | Committed clinical **snapshot exports** loaded at runtime by `src/lib/` (differentials, forms, medications, services, specifiers). Regenerate via the matching `scripts/import-*-export.ts` / `build-*-index.mjs`; do not hand-edit. Distinct from `src/data/`, which holds hand-authored static content. | +| `eslint-rules/` | Repo-specific lint rules enforced by `npm run lint` (button wiring, hardcoded hex, type/icon scale, z-index ladder) | +| `mockups/` | Notes for the design-scratch routes under `src/app/mockups/` (the routes themselves 404 in production) | +| `plugins/` | `plugins/clinical-kb/` Codex plugin manifest and workflow skill | +| `.agents/` | Single-word skill catalogue (`npm run skills`, validated by `npm run check:skills`) | +| `.claude/` | Claude Code agents, skills, hooks, settings — plus the `.claude/worktrees/` working copies | +| `.codex/` | Trusted Desktop/CLI config; tracked `config.toml` has disabled, secret-free Figma, Supabase, Railway, and Sentry MCP templates. Hosted ChatGPT/Codex apps are installed and authenticated separately; OAuth stays in the host credential store. | +| `.cursor/` | Cursor project rules and local-agent configuration | +| `.design-sync/` | Generated design-system package metadata, validation notes, and project-sync artifacts | +| `.githooks/` | Installed by `npm install`; `pre-push` runs `scripts/guard-push.mjs` (format, auto-merge race, drift staleness) | +| `.vscode/` | Shared VS Code workspace recommendations and settings | **Do not commit:** `.next/`, `node_modules/`, `coverage/`, `.env*`, `sample-documents/`, logs. diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index 8722df8f0..aae49924a 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -46,10 +46,10 @@ it makes every normal `node`, `npm`, and `npx` invocation load the generated san 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.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 +The setup command fails if the required Cloud toolchain cannot be installed. It intentionally does +not install Railway CLI: hosted Railway access comes from the authenticated workspace app, and the +CLI postinstall downloads a separate binary that may be blocked in the Cloud setup network. It pins +Codex CLI `0.146.0`, reviewed on 2026-07-30. OpenAI's [official Codex CLI guide](https://learn.chatgpt.com/docs/codex/cli) supports Linux installation; the npm package is used here so maintenance can verify an exact version without running an unversioned installer. Set @@ -106,8 +106,8 @@ PLAYWRIGHT_OFFLINE_MODE=true Keep OpenAI disabled unless a later task explicitly authorizes it. Do not add provider keys, tokens, database URLs, service-role values, E2E credentials, or `ALLOW_PROVIDER_TESTS` to this environment. The generated agent profile removes the complete provider-variable inventory in -both access profiles. Connected access configures the repository profile for scoped OAuth MCP -servers and GitHub integration, but it does not expose raw credentials to the shell or guarantee +both access profiles. Connected access records authorization intent and keeps the GitHub boundary +explicit, but it does not register hosted MCP apps, expose raw credentials to the shell, or guarantee that every GitHub capability appears as a direct agent tool. For ordinary Cloud task publishing, use the native Cloud diff/PR controls and verify the resulting GitHub branch and PR link. A metadata-only `make_pr` response is not publication evidence. If a requested GitHub API is not @@ -234,7 +234,7 @@ Expected decisive lines include: The effective-environment check runs automatically when `CODEX_CLOUD=1`, including without `--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, +policy and installed-lock parity, the pinned Codex CLI, Deno 2, Python 3 and worker imports, 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, @@ -253,10 +253,24 @@ sourcing any profile or invoking node/npm in a fresh task, run: bash --noprofile --norc scripts/check-codex-cloud-raw-env.sh ``` -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. +The probe checks the complete provider-variable inventory and prints names only. Outcomes are +name-scoped in the probe itself: + +- exit `0` / `PASS` — raw boundary clean +- exit `1` / `FAIL` + `STOP` — any unexpected provider name (for example + `SUPABASE_SERVICE_ROLE_KEY`); start another fresh task and do not continue +- exit `2` / `FAIL-KNOWN` + `CONTINUE-RESTRICTED` — only the documented Personal Pro launcher + defect name `OPENAI_BASE_URL` + +Exit `2` is still a failed raw boundary: humans/agents may continue only under this contract. +Do not treat “non-1” or “retry on any failure” as success in future automation — wire the three +states explicitly. When the probe reports `FAIL-KNOWN` for `OPENAI_BASE_URL` alone, preserve that +name-only output for OpenAI support and continue provider-free work only through the generated +profile and command shims, followed by a passing `npm run check:codex-cloud`. Do not generalize +that restricted path to any other inherited name. An inherited `OPENAI_BASE_URL` can redirect +OpenAI-bound traffic, so never call OpenAI clients from the raw parent process or from binaries +that bypass the profile/`node`/`npm`/`npx` shim scrub. A sanitized child shell is never proof +that the raw-parent boundary passed. `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 @@ -267,15 +281,19 @@ in the offline profile. ## Provider acceptance Provider access is verified separately because a generic bootstrap must not make paid or -production-like calls. For a connected environment, name each provider, use a read-only or -minimal no-op endpoint, confirm the intended account/project by non-secret metadata, and -report cost or mutation risk before any write. The checked-in MCP configuration uses Railway's -hosted `https://mcp.railway.com` endpoint so fresh Cloud tasks authenticate through browser OAuth -instead of depending on machine-local CLI state. Authorize only workspace `bigsimmo's Projects` -and project `Database` (`5deaad0b-675a-4c13-978e-5ca2b5b877f9`), restart the MCP client after -consent, and reduce identity/status results to non-secret account, project, workspace, environment, -and service metadata. Railway's remote MCP does not accept project tokens; retain the pinned CLI -only for explicitly approved local/operator workflows. +production-like calls. The active hosted workspace is **Personal Pro**. It does not have the +dedicated-group RBAC or per-tool action disabling assumed by Enterprise/Edu instructions. Use +Railway's installed official ChatGPT app, complete browser OAuth without static tokens or headers, +set the global app policy to **Allow read actions**, and leave changes approval-gated. Authorize +only workspace `bigsimmo's Projects` and project `Database` +(`5deaad0b-675a-4c13-978e-5ca2b5b877f9`) where Railway offers that choice. Reduce read results to +non-secret account, project, workspace, environment, and service metadata. Railway's remote MCP +does not accept project tokens; install Railway CLI separately only for explicitly approved +local/operator workflows (for example `npm run check:env-parity -- --railway`). That CLI path is +not available in ordinary Cloud tasks and is not part of Cloud runtime acceptance. Prefer +`RAILWAY_API_TOKEN` for personal CLI auth; never substitute the project-scoped CI +`RAILWAY_TOKEN`. Enterprise/Edu custom-app controls are an optional future governance upgrade, +not the current operating target. The Supabase MCP entry is scoped to production project `sjrfecxgysukkwxsowpy`, forces `read_only=true`, and exposes only documentation/development metadata tools. The database and @@ -286,17 +304,36 @@ and storage mutations require a separately configured non-production project or 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 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. Context7 / library-docs +Project `.codex/config.toml` is the checked-in Codex Desktop/CLI MCP template. Its URL-only entries +must stay `enabled = false` in git — `npm run check:codex-cloud` hard-fails on any tracked +`enabled = true`. A trusted local operator opts in outside the committed tree: prefer enabling +`railway` in `$CODEX_HOME/config.toml`, or make a never-committed local edit to the project file for +the session, then run `codex mcp login railway`. Setup does not copy any MCP server into +`$CODEX_HOME`. Hosted ChatGPT and Codex Cloud require the separately installed/authenticated +workspace app. Start a fresh task after consent and verify the actual callable inventory. +The root `.mcp.json` is a cross-client Desktop/CLI template and static allowlist only. It does not +prove hosted Cloud availability. Context7 / library-docs MCP is Cursor-side (`.cursor/mcp.json` or a host-injected connector), not part of this Codex Cloud Railway + Supabase allowlist. +### Personal Pro split control plane + +Personal Pro currently exposes GitHub, Slack, and Linear on the Codex connector settings page; it +does not expose Railway or Supabase there. Use the smallest functional split instead of copying +credentials into Cloud: + +- **Codex Cloud:** repository work, offline checks, and GitHub reads/publication through the native + GitHub connector or Cloud PR controls. +- **ChatGPT web:** Railway through the official OAuth app and Supabase through the pinned + project-scoped read-only app. Keep Railway on **Allow read actions** and ask before every change. +- **Desktop/CLI:** opt-in local MCP via `$CODEX_HOME/config.toml` (preferred) or a + never-committed local enable of the project `.codex/config.toml` `railway` entry, followed by + `codex mcp login railway`; this is a local operator fallback, never hosted proof. + +The repository checker prints these routes as sanitized `provider_route.*` lines. They describe +where a capability is allowed, not proof that a host installed or authenticated it. A fresh task +must still establish the callable inventory. + 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 @@ -322,15 +359,21 @@ 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`. 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 + `ALLOW_PROVIDER_TESTS`. Connected setup writes only the managed shell-environment policy; it + never registers hosted MCP apps or writes OAuth tokens. +2. **Grant the host integrations.** In the Personal Pro workspace, install Railway's official + ChatGPT app, complete Railway OAuth, select **Allow read actions**, and keep all changes subject + to explicit approval. Personal Pro has no dedicated-group RBAC or per-tool disabling, so do not + claim those controls. 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 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. + provider; their write-capable tools remain approval-gated. Railway's OAuth metadata advertises + `offline_access`; verify the scanned consent includes it and prove refresh behavior with a second + read-only call after the one-hour access-token lifetime. If no refresh token is issued, require + reauthentication instead of adding a token workaround. 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 @@ -345,7 +388,12 @@ copying credentials into the checkout. 5. **Prove each provider read-only.** Use the tools exposed by the fresh host session, not shell tokens. For GitHub, read repository metadata and confirm `BigSimmo/Database` plus the intended identity. For Railway, read workspace/project/service metadata and confirm the IDs above without - triggering a deployment. For Supabase, read project/schema metadata and confirm the pinned ref + triggering a deployment. Record the exact Railway inventory. Set the app to allow reads and ask + before changes. The Personal Pro global read-versus-change control does not provide tool-level + RBAC. If the account later moves to Enterprise/Edu, an admin may additionally allow only + `whoami`, `list-projects`, `list-services`, `list-feature-flags`, and `get-feature-flag`, while + disabling write/agent tools and newly discovered actions by default. For Supabase, read + project/schema metadata and confirm the pinned ref 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. @@ -371,15 +419,15 @@ copying credentials into the checkout. Acceptance is complete only when evidence distinguishes these independent capabilities: -| Capability | Required evidence | Not sufficient | -| ----------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | -| Connected boundary | Cloud environment and runtime PASS lines with provider variables absent | Editing the generated profile in one running task | -| GitHub read/write | Connector repository read plus verified publication of the exact task commit | Repository discovery, `make_pr` metadata, or a local commit | -| Railway read | OAuth-backed metadata for the expected workspace and project | Installed Railway CLI alone | -| Supabase read | OAuth-backed metadata for the pinned read-only project | A configured MCP URL without completed OAuth | -| OpenAI/live application | Explicitly approved paid canary or protected authenticated workflow | Setting `RAG_PROVIDER_MODE=auto` without credentials | -| Local application | Project identity plus the reported health status from the `ensure` URL | Assuming a localhost port or treating demo 503 as production-ready | -| Capacity | Full intended checks complete without OOM, or a host-level capacity change is proven | Swap size by itself | +| Capability | Required evidence | Not sufficient | +| ----------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| Connected boundary | Cloud environment and runtime PASS lines with provider variables absent | Editing the generated profile in one running task | +| GitHub read/write | Connector repository read plus verified publication of the exact task commit | Repository discovery, `make_pr` metadata, or a local commit | +| Railway read | Exact callable hosted-app tools plus OAuth metadata for the expected workspace/project | Installed Railway CLI or repository MCP config alone | +| Supabase read | OAuth-backed metadata for the pinned read-only project | A configured MCP URL without completed OAuth | +| OpenAI/live application | Explicitly approved paid canary or protected authenticated workflow | Setting `RAG_PROVIDER_MODE=auto` without credentials | +| Local application | Project identity plus the reported health status from the `ensure` URL | Assuming a localhost port or treating demo 503 as production-ready | +| Capacity | Full intended checks complete without OOM, or a host-level capacity change is proven | Swap size by itself | If a capability still fails, record the exact sanitized failure and its owner: repository setup, Codex environment/OAuth, provider RBAC, GitHub installation, protected workflow, or host capacity. diff --git a/docs/codex-review-protocol.md b/docs/codex-review-protocol.md index a97741c65..f3a656e3c 100644 --- a/docs/codex-review-protocol.md +++ b/docs/codex-review-protocol.md @@ -22,6 +22,19 @@ Use this protocol for every Codex review, audit, bug hunt, PR review, release-re - Include checks run, checks not run, and whether any check was skipped because it could touch an API/provider. - Keep summaries secondary to findings. +## GitHub access routing + +For an explicitly authorized hosted task, use the GitHub connector or native Cloud controls first. +Their permissions are independent of `gh` in the agent shell. Run +`npm run check:github-shell-access:live` (which passes `--allow-provider` and does not pass +`--self-test`) only before an intentional GitHub CLI fallback. +`ALLOW_GITHUB_SHELL_ACCESS=true` or `--allow-provider` on a direct +`node scripts/check-github-shell-access.mjs` invocation (without `--self-test`) authorize the +same live path. The plain `npm run check:github-shell-access` entry is always offline +`--self-test`; ambient opt-in cannot convert it into provider traffic. +`GH_AUTH_MISSING` means shell authentication is absent, not that the hosted connector is +disconnected. Never add a PAT to an ordinary Cloud task. + ## Mutation Rules - For a pure review request, do not edit files, stage, commit, push, post PR comments, rerun hosted CI, or call provider-backed services. diff --git a/docs/scripts-index.md b/docs/scripts-index.md index a6429f3b0..8f636f269 100644 --- a/docs/scripts-index.md +++ b/docs/scripts-index.md @@ -1,6 +1,6 @@ # Scripts index -Curated map of `scripts/` (216 files) and the `package.json` script surface (226 entries), +Curated map of `scripts/` (217 files) and the `package.json` script surface (228 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`, `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 | +| 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`, `check-github-shell-access.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 (`FAIL-KNOWN`/`exit 2` only for documented `OPENAI_BASE_URL`; hard `STOP` otherwise), an operator-only non-Cloud PAT branch-deletion helper, sanitized Personal Pro capability/route acceptance, GitHub shell preflight (`check:github-shell-access` = offline self-test; `check:github-shell-access:live` = provider-backed opt-in), 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/package.json b/package.json index c3ce5aa37..2933731c4 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,8 @@ "check:installed-lock-parity": "node scripts/check-installed-lock-parity.mjs", "check:upload-limit-parity": "node scripts/check-upload-limit-parity.mjs --self-test && node scripts/check-upload-limit-parity.mjs", "check:codex-cloud": "node scripts/check-codex-cloud-setup.mjs", + "check:github-shell-access": "node scripts/check-github-shell-access.mjs --self-test", + "check:github-shell-access:live": "node scripts/check-github-shell-access.mjs --allow-provider", "cloud:setup": "bash scripts/setup-codex-cloud.sh", "cloud:maintain": "bash scripts/maintain-codex-cloud.sh", "check:codex-autofix-workflow": "node scripts/check-codex-autofix-workflow.mjs", diff --git a/scripts/check-codex-cloud-raw-env.sh b/scripts/check-codex-cloud-raw-env.sh old mode 100644 new mode 100755 index cd37bbe43..ee83d157b --- a/scripts/check-codex-cloud-raw-env.sh +++ b/scripts/check-codex-cloud-raw-env.sh @@ -4,6 +4,13 @@ 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. +# +# Exit codes: +# 0 — no provider variables inherited (raw boundary passed) +# 1 — unexpected provider variable names present; stop and start a fresh task +# 2 — only the documented Personal Pro launcher defect (`OPENAI_BASE_URL`) is +# present. The raw boundary still failed; continue only through the +# generated profile and command shims, never by treating this as a pass. 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 @@ -17,6 +24,10 @@ provider_variables=( E2E_AUTH_ENABLED E2E_USER_EMAIL E2E_USER_PASSWORD ALLOW_PROVIDER_TESTS ) +# Name-scoped allowance: only this inherited name may use the restricted continue +# path. Any other provider name remains a hard stop. +known_launcher_defect_variables=(OPENAI_BASE_URL) + present=() for name in "${provider_variables[@]}"; do if [[ -n "${!name:-}" ]]; then @@ -24,9 +35,35 @@ for name in "${provider_variables[@]}"; do fi done -if (( ${#present[@]} > 0 )); then +if (( ${#present[@]} == 0 )); then + printf '[Codex Cloud Raw Env] PASS: no provider variables are inherited by the raw task shell.\n' + exit 0 +fi + +known=() +unexpected=() +for name in "${present[@]}"; do + is_known=0 + for known_name in "${known_launcher_defect_variables[@]}"; do + if [[ "$name" == "$known_name" ]]; then + is_known=1 + break + fi + done + if (( is_known )); then + known+=("$name") + else + unexpected+=("$name") + fi +done + +if (( ${#unexpected[@]} > 0 )); then printf '[Codex Cloud Raw Env] FAIL: inherited provider variable names: %s\n' "${present[*]}" >&2 + printf '[Codex Cloud Raw Env] STOP: unexpected credential-bearing names require a fresh task; do not continue.\n' >&2 exit 1 fi -printf '[Codex Cloud Raw Env] PASS: no provider variables are inherited by the raw task shell.\n' +printf '[Codex Cloud Raw Env] FAIL-KNOWN: inherited documented launcher defect names: %s\n' "${known[*]}" >&2 +printf '[Codex Cloud Raw Env] CONTINUE-RESTRICTED: use only the generated profile and command shims; raw boundary remains failed.\n' >&2 +printf '[Codex Cloud Raw Env] CONTINUE-RESTRICTED: OPENAI_BASE_URL can redirect OpenAI-bound traffic; never invoke OpenAI clients from the raw parent or any binary that bypasses the profile/shim scrub.\n' >&2 +exit 2 diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index 432f192e8..3bc92d926 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -16,10 +16,11 @@ import { providerEnvironmentKeys } from "./test-environment.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); export const expectedCloudCliVersions = Object.freeze({ - railway: "5.30.4", codex: "0.146.0", }); +export const expectedHostedWorkspaceClass = "personal-pro"; + export const expectedMcpConfiguration = Object.freeze({ // Canonical form matches `.mcp.json` (no trailing slash). railwayUrl: "https://mcp.railway.com", @@ -34,7 +35,7 @@ export const expectedCodexProjectMcpServers = Object.freeze({ url: "https://mcp.figma.com/mcp", approvalMode: "writes", }), - railway_cloud: Object.freeze({ + railway: Object.freeze({ url: expectedMcpConfiguration.railwayUrl, approvalMode: "writes", }), @@ -151,8 +152,8 @@ function validateSupabaseMcpUrl(urlString, label, errors) { } /** - * Project `.codex/config.toml` must register the approved MCP surface as disabled - * URL-only templates so offline/ordinary Codex hosts do not initialize providers. + * Project `.codex/config.toml` must register the approved Desktop/CLI MCP surface + * as disabled URL-only templates. Hosted tools require separately installed apps. * @param {string} text * @returns {string[]} */ @@ -172,7 +173,9 @@ export function validateCodexProjectMcpConfiguration(text) { const expected = expectedCodexProjectMcpServers[name]; if (server.enabled !== false) { - errors.push(`${label} must set enabled = false (host/connected layers opt in).`); + errors.push( + `${label} must set enabled = false (opt in via $CODEX_HOME/config.toml or a never-committed local edit; do not commit enabled = true).`, + ); } if (server.default_tools_approval_mode !== expected.approvalMode) { const reason = @@ -294,16 +297,6 @@ export function validateCodexCloudEnvironment(env = process.env) { return errors; } -/** @param {NodeJS.ProcessEnv | Record} [env] */ -export function railwayReadCapability(env = process.env, cliAvailable = false) { - return { - cliAvailable, - dedicatedCredentialPresent: Boolean(env.RAILWAY_API_TOKEN), - projectCredentialPresent: Boolean(env.RAILWAY_TOKEN), - cliTokenAuthReady: cliAvailable && Boolean(env.RAILWAY_API_TOKEN), - }; -} - export function parseMcpServerMetadata(text) { const parsed = JSON.parse(text); const servers = parsed?.mcpServers; @@ -347,17 +340,17 @@ export function validateMcpConfiguration(text) { const serverNames = Object.keys(servers).sort(); if (JSON.stringify(serverNames) !== JSON.stringify(["railway", "supabase"])) { - errors.push("Cloud MCP configuration must contain only Railway and Supabase."); + errors.push("Desktop/CLI .mcp.json must contain only Railway and Supabase."); } for (const name of ["railway", "supabase"]) { if (servers[name]?.env !== undefined || servers[name]?.headers !== undefined) { - errors.push(`${name} MCP must use hosted OAuth without embedded environment variables or headers.`); + errors.push(`${name} MCP must use OAuth without embedded environment variables or headers.`); } } const railway = servers.railway; if (railway?.type !== "http" || railway?.url !== expectedMcpConfiguration.railwayUrl.replace(/\/$/, "")) { - errors.push("Railway MCP must use the hosted OAuth endpoint."); + errors.push("Railway MCP must use Railway's official remote OAuth endpoint."); } const supabase = servers.supabase; @@ -410,12 +403,12 @@ function commandAvailable(command) { /** @param {NodeJS.ProcessEnv | Record} [env] */ export function sanitizedCloudCapabilityLines(env = process.env, options = {}) { const origin = options.origin ?? inspectOriginRemote(repoRoot); - const railway = railwayReadCapability(env, options.railwayCliAvailable ?? commandAvailable("railway")); 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 = [ + `hosted_workspace.class_documented=${expectedHostedWorkspaceClass}`, `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, ["offline"])}`, @@ -423,11 +416,10 @@ export function sanitizedCloudCapabilityLines(env = process.env, options = {}) { `PLAYWRIGHT_OFFLINE_MODE=${approvedModeValue(env.PLAYWRIGHT_OFFLINE_MODE, ["true", "false"])}`, ]; for (const name of providerCredentialVariables) lines.push(`${name}.present=${Boolean(env[name])}`); - 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.cli_token_auth_ready=${railway.cliTokenAuthReady}`); - lines.push("mcp.runtime_tool_inventory=host-provided-unverified-by-repository"); + lines.push("hosted_app.inventory=external-unverified-until-fresh-task"); + lines.push("provider_route.github=codex-native-connector"); + lines.push("provider_route.railway=chatgpt-official-app"); + lines.push("provider_route.supabase=chatgpt-project-scoped-read-only-app"); lines.push(`codex.cli_available=${codexCliAvailable}`); lines.push(pythonWorkerVersionLine(env.CODEX_CLOUD_OCR_PYTHON)); lines.push(`git.origin_configured=${origin.configured}`); @@ -442,7 +434,7 @@ export function sanitizedCloudCapabilityLines(env = process.env, options = {}) { 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"}`, + `desktop_cli_mcp.template=${server.name} type=${server.type} command=${server.command} endpoint=${server.endpoint} query_names=${server.queryNames.join(",") || "none"} environment_names=${server.environmentNames.join(",") || "none"}`, ); } return lines; @@ -648,18 +640,9 @@ 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."], - [/@railway\/cli/, "Cloud setup must install the Railway CLI."], [/@openai\/codex/, "Cloud setup must install the Codex CLI."], [/ensure-codex-cloud-git-remote\.mjs/, "Cloud setup must restore a safe origin remote."], [/check:codex-cloud -- --runtime/, "Cloud setup must run runtime acceptance."], @@ -681,15 +664,31 @@ export function validateCodexCloudSetup() { ]) { requireMatch(errors, setup, pattern, message); } - if (!setup.includes(`railway_cli_version="${expectedCloudCliVersions.railway}"`)) { - errors.push("Cloud setup Railway CLI version must match the checked runtime contract."); - } if (!setup.includes(`codex_cli_version="${expectedCloudCliVersions.codex}"`)) { errors.push("Cloud setup Codex CLI version must match the checked runtime contract."); } for (const name of providerCredentialVariables) { if (!setup.includes(name)) errors.push(`Cloud setup must handle provider environment variable ${name}.`); } + // Reject any executable (non-comment) reference that would write MCP tables. + // Strip full-line comments and whitespace-prefixed trailing comments so an + // inline note cannot false-trip the guard. Text-level only: a `#` inside a + // quoted shell string is also stripped, and split/concatenated table names + // would not match. The generated-config behavioural test (fresh temp $HOME + // asserting no `[mcp_servers.` after connected setup) is the stronger proof. + const setupWithoutComments = setup + .split("\n") + .map((line) => { + if (/^\s*#/.test(line)) return ""; + return line.replace(/\s+#.*$/, ""); + }) + .join("\n"); + if (/\bmcp_servers\b/.test(setupWithoutComments)) { + errors.push("Cloud setup must not generate MCP registrations; hosted apps are external to the repository."); + } + if (setup.includes("@railway/cli") || setup.includes('setup_step="railway-cli"')) { + errors.push("Cloud setup must not install or invoke Railway CLI; hosted access comes from the authenticated app."); + } const providerScrubIndex = setup.indexOf("unset OPENAI_API_KEY"); const accessProfileBranchIndex = setup.indexOf('if [ "\\$CODEX_CLOUD_ACCESS_PROFILE" = "connected" ]'); if (providerScrubIndex < 0 || accessProfileBranchIndex < 0 || providerScrubIndex > accessProfileBranchIndex) { @@ -743,6 +742,30 @@ export function validateCodexCloudSetup() { "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."); + requireMatch( + errors, + rawEnvironmentProbe, + /known_launcher_defect_variables=\(OPENAI_BASE_URL\)/, + "Raw Cloud environment probe must name-scope the OPENAI_BASE_URL launcher-defect allowance.", + ); + requireMatch( + errors, + rawEnvironmentProbe, + /FAIL-KNOWN: inherited documented launcher defect names/, + "Raw Cloud environment probe must emit FAIL-KNOWN for the documented launcher defect.", + ); + requireMatch( + errors, + rawEnvironmentProbe, + /CONTINUE-RESTRICTED: OPENAI_BASE_URL can redirect OpenAI-bound traffic/, + "Raw Cloud environment probe must warn that OPENAI_BASE_URL can redirect provider traffic.", + ); + requireMatch( + errors, + rawEnvironmentProbe, + /STOP: unexpected credential-bearing names require a fresh task/, + "Raw Cloud environment probe must hard-stop on unexpected provider names.", + ); for (const name of providerCredentialVariables) { if (!rawEnvironmentProbe.includes(name)) { errors.push(`Raw Cloud environment probe must cover provider environment variable ${name}.`); @@ -784,6 +807,8 @@ export function validateCodexCloudSetup() { 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."); + requireMatch(errors, guide, /Personal Pro/, "The guide must identify the active Personal Pro workspace."); + requireMatch(errors, guide, /split control plane/, "The guide must document the Personal Pro provider workaround."); try { parseMcpServerMetadata(mcp); } catch (error) { @@ -846,10 +871,12 @@ function repositoryCommand(command, args) { export async function validateCodexCloudRuntime(env = process.env) { const errors = validateCodexCloudEnvironment(env); + // Hosted Railway access is the OAuth ChatGPT/Codex app, not CLI token auth. + // Do not reintroduce `railway --version` or RAILWAY_API_TOKEN-vs-RAILWAY_TOKEN + // substitution checks here unless a future workflow restores CLI token auth. for (const error of [ commandVersion("deno", ["--version"], /^deno 2\./m), commandVersion("tesseract", ["--version"], /^tesseract \d+\./m), - commandVersion("railway", ["--version"], exactVersionPattern(expectedCloudCliVersions.railway)), commandVersion("codex", ["--version"], exactVersionPattern(expectedCloudCliVersions.codex)), ]) { if (error) errors.push(error); diff --git a/scripts/check-github-shell-access.mjs b/scripts/check-github-shell-access.mjs new file mode 100644 index 000000000..19747f485 --- /dev/null +++ b/scripts/check-github-shell-access.mjs @@ -0,0 +1,123 @@ +#!/usr/bin/env node + +import { realpathSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const repository = "BigSimmo/Database"; +const allowProviderFlag = "--allow-provider"; +const allowProviderEnv = "ALLOW_GITHUB_SHELL_ACCESS"; + +function shellGh(command, args) { + return spawnSync(command, args, { encoding: "utf8", shell: false }); +} + +/** + * Check only the optional GitHub CLI fallback available to the current shell. + * Hosted GitHub connector/native Cloud access is a separate capability. + * + * Live `gh` API calls require an explicit opt-in (`--allow-provider` or + * `ALLOW_GITHUB_SHELL_ACCESS=true`) so the provider confirmation boundary is + * self-enforcing rather than documentation-only. The plain npm script passes + * `--self-test` and always stays offline: an ambient opt-in must not convert a + * documentation-safe sweep into provider traffic. Use + * `npm run check:github-shell-access:live` (no `--self-test`) for the + * dedicated provider-backed entry. + */ +export function githubShellAccess(run = shellGh) { + if (run("gh", ["--version"]).status !== 0) { + return { ok: false, outcome: "GH_CLI_MISSING" }; + } + if (run("gh", ["auth", "status", "--hostname", "github.com"]).status !== 0) { + return { ok: false, outcome: "GH_AUTH_MISSING" }; + } + if (run("gh", ["repo", "view", repository, "--json", "nameWithOwner,viewerPermission"]).status !== 0) { + return { ok: false, outcome: "GH_REPO_ACCESS_MISSING" }; + } + if ( + run("gh", ["pr", "list", "--repo", repository, "--state", "open", "--limit", "1", "--json", "number"]).status !== 0 + ) { + return { ok: false, outcome: "GH_PR_LIST_ACCESS_MISSING" }; + } + return { ok: true, outcome: "GH_SHELL_ACCESS_READY" }; +} + +export function providerAccessAuthorized(argv = process.argv, env = process.env) { + return argv.includes(allowProviderFlag) || env[allowProviderEnv] === "true"; +} + +function fakeRun(statuses) { + let index = 0; + return () => ({ status: statuses[index++] ?? 0, stdout: "", stderr: "" }); +} + +function selfTest() { + const cases = [ + [[1], "GH_CLI_MISSING"], + [[0, 1], "GH_AUTH_MISSING"], + [[0, 0, 1], "GH_REPO_ACCESS_MISSING"], + [[0, 0, 0, 1], "GH_PR_LIST_ACCESS_MISSING"], + [[0, 0, 0, 0], "GH_SHELL_ACCESS_READY"], + ]; + for (const [statuses, expected] of cases) { + const actual = githubShellAccess(fakeRun(statuses)); + if (actual.outcome !== expected || actual.ok !== (expected === "GH_SHELL_ACCESS_READY")) { + throw new Error(`expected ${expected}, received ${actual.outcome}`); + } + } + if (providerAccessAuthorized(["node", "script.mjs"], {})) { + throw new Error("provider access must fail closed without an opt-in"); + } + if (!providerAccessAuthorized(["node", "script.mjs", allowProviderFlag], {})) { + throw new Error("provider access must accept --allow-provider"); + } + if (!providerAccessAuthorized(["node", "script.mjs"], { [allowProviderEnv]: "true" })) { + throw new Error(`provider access must accept ${allowProviderEnv}=true`); + } + // --self-test always stays offline, even when an ambient opt-in is exported. + if (!shouldRunSelfTest(["node", "script.mjs", "--self-test", allowProviderFlag])) { + throw new Error("--self-test must stay offline when --allow-provider is also present"); + } + if (!shouldRunSelfTest(["node", "script.mjs", "--self-test"])) { + throw new Error("plain --self-test must stay offline"); + } + if (shouldRunSelfTest(["node", "script.mjs", allowProviderFlag])) { + throw new Error("live entry without --self-test must not run the offline stub"); + } + console.log("GITHUB_SHELL_ACCESS_SELF_TEST=PASS"); +} + +function shouldRunSelfTest(argv = process.argv) { + return argv.includes("--self-test"); +} + +function main() { + if (shouldRunSelfTest()) { + selfTest(); + return; + } + if (!providerAccessAuthorized()) { + console.error( + [ + "Refusing live GitHub API calls without confirmation.", + `Use npm run check:github-shell-access:live, or invoke node scripts/check-github-shell-access.mjs with ${allowProviderFlag} / ${allowProviderEnv}=true (and without --self-test), only after explicit provider approval.`, + "The plain npm run check:github-shell-access entry always stays offline (--self-test); ambient opt-in cannot override it.", + ].join("\n"), + ); + process.exitCode = 1; + return; + } + const result = githubShellAccess(); + console.log(`GITHUB_SHELL_ACCESS=${result.outcome}`); + console.log("GITHUB_HOSTED_CONNECTOR=SEPARATE_UNVERIFIED_CAPABILITY"); + if (!result.ok) process.exitCode = 1; +} + +const invokedDirectly = (() => { + try { + return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); + } catch { + return false; + } +})(); +if (invokedDirectly) main(); diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index c34d81d70..660c60ed9 100644 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -35,7 +35,6 @@ 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.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." @@ -98,13 +97,6 @@ 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 @@ -247,8 +229,6 @@ 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" diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index de7742737..c0f14cc54 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -19,7 +19,6 @@ import { pythonWorkerImportError, pythonWorkerVersionLine, pythonWorkerImports, - railwayReadCapability, sanitizedCloudCapabilityLines, validateCodexCloudEnvironment, validateCodexProjectMcpConfiguration, @@ -224,7 +223,6 @@ describe("Codex Cloud environment contract", () => { repositoryMatch: true, credentialsEmbedded: false, }, - railwayCliAvailable: true, codexCliAvailable: true, safeGitHelper: true, checkout: { @@ -248,8 +246,15 @@ describe("Codex Cloud environment contract", () => { }, ); const report = lines.join("\n"); + expect(report).toContain("hosted_workspace.class_documented=personal-pro"); 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("hosted_app.inventory=external-unverified-until-fresh-task"); + expect(report).toContain("provider_route.github=codex-native-connector"); + expect(report).toContain("provider_route.railway=chatgpt-official-app"); + expect(report).toContain("provider_route.supabase=chatgpt-project-scoped-read-only-app"); + expect(report).toContain( + "desktop_cli_mcp.template=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"); @@ -264,7 +269,6 @@ describe("Codex Cloud environment contract", () => { }, { origin: { configured: true, repositoryMatch: true, credentialsEmbedded: false }, - railwayCliAvailable: false, codexCliAvailable: false, safeGitHelper: false, checkout: { @@ -281,16 +285,6 @@ describe("Codex Cloud environment contract", () => { ).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).cliTokenAuthReady).toBe(true); - expect(railwayReadCapability({ RAILWAY_TOKEN: "configured" }, true)).toMatchObject({ - dedicatedCredentialPresent: false, - projectCredentialPresent: true, - cliTokenAuthReady: false, - }); - expect(railwayReadCapability({ RAILWAY_API_TOKEN: "configured" }, false).cliTokenAuthReady).toBe(false); - }); - it("parses MCP transport metadata without query or environment values", () => { const secret = "never-print-mcp-value"; const metadata = parseMcpServerMetadata( @@ -335,10 +329,10 @@ describe("Codex Cloud environment contract", () => { "Supabase MCP must keep the production project read-only.", ); expect(validateMcpConfiguration(valid.replace('"supabase":', '"unexpected":{},"supabase":'))).toContain( - "Cloud MCP configuration must contain only Railway and Supabase.", + "Desktop/CLI .mcp.json must contain only Railway and Supabase.", ); expect(validateMcpConfiguration(valid.replace('"railway":{"type"', '"railway":{"headers":{},"type"'))).toContain( - "railway MCP must use hosted OAuth without embedded environment variables or headers.", + "railway MCP must use OAuth without embedded environment variables or headers.", ); expect(validateMcpConfiguration(valid.replace("&read_only=true", "&read_only=true&token=forbidden"))).toContain( "Supabase MCP must not include additional query parameters.", @@ -352,7 +346,7 @@ describe("Codex Cloud environment contract", () => { const tracked = readFileSync(new URL("../.codex/config.toml", import.meta.url), "utf8"); expect(validateCodexProjectMcpConfiguration(tracked)).toEqual([]); expect(validateCodexProjectMcpConfiguration(tracked.replaceAll("enabled = false", "enabled = true"))).toContain( - `.codex/config.toml figma_cloud must set enabled = false (host/connected layers opt in).`, + `.codex/config.toml figma_cloud must set enabled = false (opt in via $CODEX_HOME/config.toml or a never-committed local edit; do not commit enabled = true).`, ); expect( validateCodexProjectMcpConfiguration( @@ -443,9 +437,42 @@ describe("Codex Cloud environment contract", () => { }); expect(result.status).toBe(1); expect(result.stderr).toContain("OPENAI_API_KEY"); + expect(result.stderr).toContain("STOP:"); expect(result.stderr).not.toContain(secret); }); + it("name-scopes the documented OPENAI_BASE_URL launcher defect separately from unexpected leaks", () => { + const known = spawnSync(bashCommand, ["scripts/check-codex-cloud-raw-env.sh"], { + cwd: repoRoot, + encoding: "utf8", + env: { PATH: process.env.PATH, NODE_ENV: "test", OPENAI_BASE_URL: "https://example.invalid" }, + }); + expect(known.status).toBe(2); + expect(known.stderr).toContain("FAIL-KNOWN:"); + expect(known.stderr).toContain("OPENAI_BASE_URL"); + expect(known.stderr).toContain("CONTINUE-RESTRICTED:"); + expect(known.stderr).toContain("redirect OpenAI-bound traffic"); + expect(known.stderr).toContain("bypasses the profile/shim scrub"); + expect(known.stderr).not.toContain("https://example.invalid"); + + const mixed = spawnSync(bashCommand, ["scripts/check-codex-cloud-raw-env.sh"], { + cwd: repoRoot, + encoding: "utf8", + env: { + PATH: process.env.PATH, + NODE_ENV: "test", + OPENAI_BASE_URL: "https://example.invalid", + SUPABASE_SERVICE_ROLE_KEY: "never-print-service-role", + }, + }); + expect(mixed.status).toBe(1); + expect(mixed.stderr).toContain("FAIL:"); + expect(mixed.stderr).toContain("STOP:"); + expect(mixed.stderr).toContain("SUPABASE_SERVICE_ROLE_KEY"); + expect(mixed.stderr).not.toContain("never-print-service-role"); + expect(mixed.stderr).not.toContain("CONTINUE-RESTRICTED:"); + }); + 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"); @@ -483,6 +510,8 @@ describe("Codex Cloud environment contract", () => { expect(setup).toContain("diagnose-codex-cloud.mjs"); expect(setup).toContain("trap diagnose_setup_failure ERR"); expect(setup).toContain('setup_step="python-worker-requirements"'); + expect(setup).not.toContain("@railway/cli"); + expect(setup).not.toContain('setup_step="railway-cli"'); 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"); @@ -540,6 +569,29 @@ describe("Codex Cloud environment contract", () => { it("pins connected retrieval mode and rejects unsafe shell-policy configs", () => { const connectedHome = temporaryDirectory("codex-cloud-connected-"); + mkdirSync(path.join(connectedHome, ".codex"), { recursive: true }); + // Seed the previous hosted registration shape between managed markers so re-run + // cleanup is proven (fresh empty $HOME alone would not exercise that path). + writeFileSync( + path.join(connectedHome, ".codex/config.toml"), + [ + "[mcp_servers.keep_outside]", + 'command = "echo"', + "", + "# BEGIN clinical-kb-codex-cloud shell policy (managed by setup-codex-cloud.sh)", + "[mcp_servers.railway_connected]", + 'url = "https://mcp.railway.com"', + "enabled = true", + "[mcp_servers.supabase_connected]", + 'url = "https://mcp.supabase.com/mcp"', + "enabled = true", + "[shell_environment_policy]", + 'inherit = "all"', + "exclude = []", + "# END clinical-kb-codex-cloud shell policy (managed by setup-codex-cloud.sh)", + "", + ].join("\n"), + ); const connected = runSetupPolicyOnly(connectedHome, { CODEX_CLOUD_ACCESS_PROFILE: "connected", RAG_PROVIDER_MODE: "offline", @@ -547,11 +599,11 @@ 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(connectedConfig).toContain("[mcp_servers.keep_outside]"); + expect(connectedConfig).not.toContain("[mcp_servers.railway_connected]"); + expect(connectedConfig).not.toContain("[mcp_servers.supabase_connected]"); + expect(connectedConfig).not.toContain("mcp_servers.railway"); + expect(connectedConfig).not.toContain("mcp_servers.supabase"); 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}"); @@ -642,7 +694,7 @@ describe("Codex Cloud environment contract", () => { expect(incomplete.status).not.toBe(0); expect(incomplete.stderr).toContain("Incomplete managed shell policy block"); expect(readFileSync(incompletePath, "utf8")).toBe(incompleteConfig); - }); + }, 120_000); it("accepts a task-only HEAD only inside Codex Cloud", () => { const directory = temporaryGitRepository();