diff --git a/.codex/config.toml b/.codex/config.toml index 90047c7796..8b4f9bb811 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -3,8 +3,9 @@ # # checked-in policy: # - enabled = false so ordinary/offline Codex sessions do not initialize providers -# - production Supabase uses "prompt" so every metadata/read operation requires -# explicit approval; write-capable Figma/Railway/Sentry tools use "writes" +# - Figma, Frontend Checklist, Supabase, and Sentry use "prompt" so metadata/file/ +# issue reads require explicit approval (matches connected opt-in posture) +# - Railway keeps "writes" so only mutating tools need confirmation # - 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. @@ -12,7 +13,12 @@ [mcp_servers.figma_cloud] url = "https://mcp.figma.com/mcp" enabled = false -default_tools_approval_mode = "writes" +default_tools_approval_mode = "prompt" + +[mcp_servers.frontendchecklist_cloud] +url = "https://mcp.frontendchecklist.io" +enabled = false +default_tools_approval_mode = "prompt" [mcp_servers.supabase_cloud] # Production project, read_only=true, docs/development metadata only; database and debugging groups are excluded. @@ -29,4 +35,4 @@ default_tools_approval_mode = "writes" [mcp_servers.sentry_cloud] url = "https://mcp.sentry.dev/mcp" enabled = false -default_tools_approval_mode = "writes" +default_tools_approval_mode = "prompt" diff --git a/AGENTS.md b/AGENTS.md index 52f8393de5..11d4ca5b27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -873,14 +873,21 @@ Use `docs/codex-cloud.md` as the environment contract: - When MCP tools are already callable in a Cloud session and the task needs them, use the host plugin/connector inventory. The production Supabase target is limited to prompted, read-only `docs` and `development` metadata tools; do not enable database, SQL, row, or log tools. - Write-capable Figma, Railway, and Sentry tools still require explicit confirmation. Paid API + Figma, Frontend Checklist, and Sentry use `prompt` (reads confirmation-gated); Railway write + tools still require explicit confirmation via `writes`. 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. + initialize them. Connected setup writes enabled Railway (`writes` approval) and constrained + Supabase (`prompt` approval) entries to the host `$CODEX_HOME/config.toml` by default. + Optional Figma, Frontend Checklist, and Sentry hosted entries stay off unless the Codex + environment configures `CODEX_CLOUD_ENABLE_FIGMA=1`, `CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST=1`, + or `CODEX_CLOUD_ENABLE_SENTRY=1` before setup/maintenance runs (re-provision or open a fresh + task after changing them — exporting the flag inside a running agent shell does not rewrite + `$CODEX_HOME/config.toml`); when enabled they use `prompt` approval so reads stay + confirmation-gated. Actual availability still requires the installed host plugin/connector to + complete OAuth and a fresh task to prove the callable inventory with read-only identity calls. + Root `.mcp.json` is a static cross-client template, not runtime proof. - Cloud has no Windows task-start script. Report that exact fact, then perform equivalent read-only identity, branch, status, worktree, and Git-operation checks. Proceed only in a clean disposable checkout on a task-specific non-protected branch. diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index c4c89484cc..a77daed34c 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -624,3 +624,5 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-04 | claude/search-bar-decisions-doc | a7dea7f777255ade72878820a636413aaf9588af | search-bar handoff doc replacement + review fixes | Docs-only review fixes: mode/shelf accounting, Sort consumers, #230/#170 precision; removed unquoted-output claim from prior row | prettier --check . ; check:outstanding-issues ; docs:check-links ; docs:check-index | | 2026-08-04 | claude/search-bar-decisions-doc | 3b4cd6e6bf1f36fb8aff098ce7d333641e0859d3 | search-bar handoff doc replacement + review fixes | Fixed CodeRabbit/Codex findings; Bugbot hosted stuck queued, local Bugbot-equivalent confirmed two P2 doc errors and rejected sheets-are-target finding. verify:pr-local PASS (docs scope). Decisive: prettier All matched files use Prettier code style!; outstanding-issues 228 rows next-id=231; docs link check passed: 1615; docs/codebase-index coverage OK | verify:pr-local (docs); prettier --check; check:outstanding-issues; docs:check-links; docs:check-index; check:branch-review-ledger | | 2026-08-04 | codex/fix-mode-switching-and-loading-issues | 3e3b224a2ec13928d1e28173b1fc4c75d202d7d2 | PR #1607 unblock/fix | clean — behind 0, merge-tree clean, 0 unresolved threads, required CI in progress (no code fix) | merge-tree clean; behind_by 0; Unit/Build/Static/ProdUI in progress; no failing required | +| 2026-08-05 | codex/cloud-mcp-parity-20260804 | 79c4672e31788779793a9317a10f511fa3597e66 | PR #1613 review-fix babysit | addressed open review threads + remaining pasted findings; verify:pr-local PASS; unresolved threads 0; CI watch limited by PAT | verify:pr-local PASS (5098 tests); vitest focused 63; threads resolved | +| 2026-08-05 | codex/cloud-mcp-parity-20260804 | c35f61400078ded68bf34e96a882e8a3711061c8 | PR #1613 review-fix babysit | env-parity Sentry name contract restored; all review threads resolved; awaiting CI | vitest env-sentry+repo-hygiene PASS; parseEnvSchemaNames includes both Sentry DSNs | diff --git a/docs/codebase-index.md b/docs/codebase-index.md index adc6096dd8..88f4d2dfe8 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/` | Project-scoped Codex host config; tracked `config.toml` has disabled, secret-free Figma, Frontend Checklist, Supabase, Railway, and Sentry MCP registrations. Figma/Frontend Checklist/Supabase/Sentry use `"prompt"`; Railway uses `"writes"`. 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 | **Do not commit:** `.next/`, `node_modules/`, `coverage/`, `.env*`, `sample-documents/`, logs. diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index 8722df8f0f..764b6a04fc 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -106,7 +106,7 @@ 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 +both access profiles. Connected access configures the repository profile for audited hosted MCP servers and GitHub integration, but it does not 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 @@ -288,10 +288,14 @@ reruns, ingestion, deployment, and release workflows remain separate explicit ac 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. +setup activates Railway and constrained Supabase by default. Optional Figma, Frontend Checklist, +and Sentry hosted endpoints stay off unless the Codex environment sets +`CODEX_CLOUD_ENABLE_FIGMA=1`, `CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST=1`, or +`CODEX_CLOUD_ENABLE_SENTRY=1` as environment variables consumed at setup/maintenance time. Those +flags are not agent-shell toggles: export them in the environment settings, re-run setup or open a +fresh task, then authenticate. The first authenticated use of an enabled endpoint 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 MCP is Cursor-side (`.cursor/mcp.json` or a host-injected connector), not part of this Codex Cloud @@ -299,9 +303,28 @@ Railway + Supabase allowlist. Production Supabase stays project-scoped and `read_only=true`, with `default_tools_approval_mode = "prompt"` so every production metadata/read call requires -confirmation. Do not use unrestricted SQL or query clinical rows. Railway, Figma, and Sentry -write-capable tools remain approval-gated. OAuth credentials stay in the host store—never the -tracked files or agent environment. +confirmation. Do not use unrestricted SQL or query clinical rows. Optional Figma and Sentry +entries also use `prompt` approval so issue/file reads stay confirmation-gated; Railway +write-capable tools remain approval-gated via `writes`. OAuth credentials stay in the host +store—never the tracked files or agent environment. + +Local registrations resolve into Cloud capabilities as follows. Exact machine adapters are not +copied when the Cloud host already supplies the capability or when copying them would require +Docker Desktop, a local filesystem path, or a raw secret. + +| Local registration | Connected Cloud resolution | +| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `figma_cloud` | Opt-in managed `figma_connected` hosted MCP (`CODEX_CLOUD_ENABLE_FIGMA=1`); OAuth remains in the host store | +| `frontendchecklist_cloud` | Opt-in managed `frontendchecklist_connected` hosted MCP (`CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST=1`); calls prompt | +| `github` and Docker `github-official` | Native Codex GitHub connector; no PAT is exposed to the shell | +| `railway` / `railway_cloud` | Managed `railway_connected` hosted MCP; local CLI state is not reused | +| `sentry`, `sentry_bridge`, and Docker `sentry-remote` | Opt-in managed `sentry_connected` hosted MCP (`CODEX_CLOUD_ENABLE_SENTRY=1`); reads stay prompt-gated | +| `supabase` / `supabase_cloud` | Managed project-scoped, read-only `supabase_connected` hosted MCP | +| `node_repl`, Docker `playwright`, and `MCP_DOCKER` image servers | Cloud-native agent, shell, and installed browser capabilities; no machine path or Docker Desktop profile is copied | + +Other Docker-profile catalog servers are optional local tooling rather than project trust-boundary +dependencies. Add a hosted endpoint to the audited project surface only when a Database task needs +it; never copy Docker Desktop secret-store values into Cloud variables. In a fresh connected task, first run the raw-shell probe and repository acceptance, then inspect the actual callable tool inventory. A configured URL or `enabled = false` template is not runtime @@ -322,15 +345,20 @@ 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. + `ALLOW_PROVIDER_TESTS`. Connected setup writes Railway and constrained Supabase into the managed + host MCP block by default; optional Figma, Frontend Checklist, and Sentry endpoints require the + matching `CODEX_CLOUD_ENABLE_*=1` environment opt-in before setup/maintenance writes the host + MCP block (changing the flag mid-task has no effect until re-provision). Setup never writes + OAuth tokens. 2. **Grant the host integrations.** Authorize the Codex GitHub connector for `BigSimmo/Database` with repository write access. Complete Railway OAuth only for workspace `bigsimmo's Projects` and project `Database` (`5deaad0b-675a-4c13-978e-5ca2b5b877f9`). Complete Supabase OAuth only for the organization containing `Clinical KB Database`; retain project ref `sjrfecxgysukkwxsowpy`, `read_only=true`, and the 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. + production Supabase MCP to write access. Complete Figma and Sentry OAuth only when the matching + `CODEX_CLOUD_ENABLE_*=1` environment opt-in is set before setup for the intended workspace and + organization; their tools remain prompt-gated. Frontend Checklist requires no repository + credential and remains prompt-gated. 3. **Start a fresh task.** OAuth tools and environment values are fixed when the task starts. A setup rerun inside an already-running offline task can validate a generated connected profile, but it cannot inject host MCP tools or retroactively grant OAuth. Restart the MCP client or open @@ -346,7 +374,9 @@ copying credentials into the checkout. 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 - without querying clinical row contents. Report only non-secret identity and status metadata. + without querying clinical row contents. For Figma and Sentry, use non-mutating identity or + organization metadata. For Frontend Checklist, list or invoke one read-only rule lookup. Report + only non-secret identity and status metadata. OpenAI has no generic connected-profile credential: leave `RAG_PROVIDER_MODE=offline` until a separately approved paid canary or protected workflow supplies its own credential boundary. 6. **Publish a task branch safely.** Work on a task-specific non-protected branch. Format, stage, and commit diff --git a/scripts/check-codex-cloud-raw-env.sh b/scripts/check-codex-cloud-raw-env.sh index cd37bbe435..c82debef8d 100644 --- a/scripts/check-codex-cloud-raw-env.sh +++ b/scripts/check-codex-cloud-raw-env.sh @@ -14,6 +14,7 @@ provider_variables=( RAILWAY_API_TOKEN RAILWAY_TOKEN GH_TOKEN GITHUB_TOKEN CODEX_CLOUD_GITHUB_PAT GITLAB_TOKEN GLAB_TOKEN CODEX_TRIGGER_TOKEN HEALTH_DEEP_PROBE_SECRET INDEXING_V3_AGENT_SECRET + SENTRY_AUTH_TOKEN SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN E2E_AUTH_ENABLED E2E_USER_EMAIL E2E_USER_PASSWORD ALLOW_PROVIDER_TESTS ) diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index 432f192e89..d4ba022507 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -32,7 +32,11 @@ export const expectedMcpConfiguration = Object.freeze({ export const expectedCodexProjectMcpServers = Object.freeze({ figma_cloud: Object.freeze({ url: "https://mcp.figma.com/mcp", - approvalMode: "writes", + approvalMode: "prompt", + }), + frontendchecklist_cloud: Object.freeze({ + url: "https://mcp.frontendchecklist.io", + approvalMode: "prompt", }), railway_cloud: Object.freeze({ url: expectedMcpConfiguration.railwayUrl, @@ -40,7 +44,7 @@ export const expectedCodexProjectMcpServers = Object.freeze({ }), sentry_cloud: Object.freeze({ url: "https://mcp.sentry.dev/mcp", - approvalMode: "writes", + approvalMode: "prompt", }), supabase_cloud: Object.freeze({ // URL validated with the same project/read-only/feature rules as `.mcp.json`. @@ -178,7 +182,9 @@ export function validateCodexProjectMcpConfiguration(text) { const reason = expected.approvalMode === "writes" ? "write-capable tools require explicit approval" - : "the production server is constrained read-only"; + : expected.kind === "supabase" + ? "the production server is constrained read-only" + : "external read tools require explicit approval"; errors.push(`${label} must set default_tools_approval_mode = "${expected.approvalMode}" because ${reason}.`); } for (const key of Object.keys(server)) { @@ -658,6 +664,22 @@ export function validateCodexCloudSetup() { ], [/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."], + [ + /unset SENTRY_AUTH_TOKEN SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN/, + "Cloud setup must scrub supported Sentry credentials from the agent shell.", + ], + [ + /CODEX_CLOUD_ENABLE_FIGMA/, + "Connected Cloud setup must gate optional Figma MCP registration behind an explicit opt-in.", + ], + [ + /CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST/, + "Connected Cloud setup must gate optional Frontend Checklist MCP registration behind an explicit opt-in.", + ], + [ + /CODEX_CLOUD_ENABLE_SENTRY/, + "Connected Cloud setup must gate optional Sentry MCP registration behind an explicit opt-in.", + ], [/\.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."], diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index c34d81d709..d86ba81673 100644 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -42,8 +42,10 @@ expected_cloud_python="3.12" [[ -n "$expected_npm_version" ]] || fail "Could not read the npm version from package.json." # Codex Cloud supplies standards-based proxy variables as well. Remove npm's -# deprecated lowercase aliases before the first npm invocation. +# deprecated lowercase aliases before the first npm invocation, and tell Node +# subprocesses (including CLI postinstall downloaders) to use the Cloud proxy. unset npm_config_http_proxy npm_config_https_proxy npm_config_proxy +export NODE_USE_ENV_PROXY=1 install_npm_cli() { local package_name="$1" @@ -98,11 +100,34 @@ else rag_provider_mode="offline" fi +# Resolve hosted MCP URLs from the audited project template so connected setup +# cannot drift from `.codex/config.toml` (validated by check:codex-cloud). +resolve_codex_mcp_url() { + local section="$1" + sed -n "/^\\[mcp_servers\\.${section}\\]\$/,/^\\[mcp_servers\\./ s/^url = \"\\(.*\\)\"\$/\\1/p" .codex/config.toml | head -n 1 +} + connected_supabase_mcp_url="" +connected_railway_mcp_url="" +connected_figma_mcp_url="" +connected_frontendchecklist_mcp_url="" +connected_sentry_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="$(resolve_codex_mcp_url supabase_cloud)" + connected_railway_mcp_url="$(resolve_codex_mcp_url railway_cloud)" + connected_figma_mcp_url="$(resolve_codex_mcp_url figma_cloud)" + connected_frontendchecklist_mcp_url="$(resolve_codex_mcp_url frontendchecklist_cloud)" + connected_sentry_mcp_url="$(resolve_codex_mcp_url sentry_cloud)" [[ "$connected_supabase_mcp_url" = https://mcp.supabase.com/mcp\?* ]] || fail "Could not resolve the audited Supabase MCP URL from .codex/config.toml." + [[ "$connected_railway_mcp_url" = https://mcp.railway.com ]] || + fail "Could not resolve the audited Railway MCP URL from .codex/config.toml." + [[ "$connected_figma_mcp_url" = https://mcp.figma.com/mcp ]] || + fail "Could not resolve the audited Figma MCP URL from .codex/config.toml." + [[ "$connected_frontendchecklist_mcp_url" = https://mcp.frontendchecklist.io ]] || + fail "Could not resolve the audited Frontend Checklist MCP URL from .codex/config.toml." + [[ "$connected_sentry_mcp_url" = https://mcp.sentry.dev/mcp ]] || + fail "Could not resolve the audited Sentry MCP URL from .codex/config.toml." fi runtime_profile="$HOME/.clinical-kb-codex-cloud.sh" @@ -120,6 +145,8 @@ export CODEX_CLOUD_ACCESS_PROFILE="${access_profile}" export NEXT_PUBLIC_DEMO_MODE="\${NEXT_PUBLIC_DEMO_MODE:-true}" export PLAYWRIGHT_OFFLINE_MODE="\${PLAYWRIGHT_OFFLINE_MODE:-true}" unset npm_config_http_proxy npm_config_https_proxy npm_config_proxy +# Keep Node subprocesses (npm, npx, CLI downloaders) on the Cloud proxy path. +export NODE_USE_ENV_PROXY=1 # Connected access is provided by OAuth-backed MCP servers and the GitHub # connector, never by raw provider variables in the agent shell. Scrub the # complete inventory in both profiles in case setup-only secrets were inherited. @@ -132,6 +159,7 @@ unset CROSS_TENANT_SERVICE_ROLE_KEY unset RAILWAY_API_TOKEN RAILWAY_TOKEN unset GH_TOKEN GITHUB_TOKEN GITLAB_TOKEN GLAB_TOKEN CODEX_TRIGGER_TOKEN CODEX_CLOUD_GITHUB_PAT unset HEALTH_DEEP_PROBE_SECRET INDEXING_V3_AGENT_SECRET +unset SENTRY_AUTH_TOKEN SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN unset E2E_AUTH_ENABLED E2E_USER_EMAIL E2E_USER_PASSWORD ALLOW_PROVIDER_TESTS if [ "\$CODEX_CLOUD_ACCESS_PROFILE" = "connected" ]; then export RAG_PROVIDER_MODE="${rag_provider_mode}" @@ -160,6 +188,7 @@ codex_shell_policy_excludes=( RAILWAY_API_TOKEN RAILWAY_TOKEN GH_TOKEN GITHUB_TOKEN GITLAB_TOKEN GLAB_TOKEN CODEX_TRIGGER_TOKEN CODEX_CLOUD_GITHUB_PAT HEALTH_DEEP_PROBE_SECRET INDEXING_V3_AGENT_SECRET + SENTRY_AUTH_TOKEN SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN E2E_AUTH_ENABLED E2E_USER_EMAIL E2E_USER_PASSWORD ALLOW_PROVIDER_TESTS ) codex_exclude_toml="" @@ -207,14 +236,37 @@ trap 'rm -f "$codex_config_candidate"' EXIT printf 'ignore_default_excludes = false\n' printf 'exclude = [%s]\n' "$codex_exclude_toml" if [[ "$access_profile" = "connected" ]]; then + # Baseline connected providers: Railway + constrained Supabase. + # Optional OAuth-backed providers stay off unless the task explicitly opts in. + # URLs come from the audited `.codex/config.toml` registrations above. printf '\n%s\n' '[mcp_servers.railway_connected]' - printf '%s\n' 'url = "https://mcp.railway.com"' + printf 'url = "%s"\n' "$connected_railway_mcp_url" printf '%s\n' 'enabled = true' printf '%s\n' 'default_tools_approval_mode = "writes"' printf '\n%s\n' '[mcp_servers.supabase_connected]' printf 'url = "%s"\n' "$connected_supabase_mcp_url" printf '%s\n' 'enabled = true' printf '%s\n' 'default_tools_approval_mode = "prompt"' + if [[ "${CODEX_CLOUD_ENABLE_FIGMA:-0}" = "1" ]]; then + printf '\n%s\n' '[mcp_servers.figma_connected]' + printf 'url = "%s"\n' "$connected_figma_mcp_url" + printf '%s\n' 'enabled = true' + # prompt: Figma file reads stay confirmation-gated, not auto-approved. + printf '%s\n' 'default_tools_approval_mode = "prompt"' + fi + if [[ "${CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST:-0}" = "1" ]]; then + printf '\n%s\n' '[mcp_servers.frontendchecklist_connected]' + printf 'url = "%s"\n' "$connected_frontendchecklist_mcp_url" + printf '%s\n' 'enabled = true' + printf '%s\n' 'default_tools_approval_mode = "prompt"' + fi + if [[ "${CODEX_CLOUD_ENABLE_SENTRY:-0}" = "1" ]]; then + printf '\n%s\n' '[mcp_servers.sentry_connected]' + printf 'url = "%s"\n' "$connected_sentry_mcp_url" + printf '%s\n' 'enabled = true' + # prompt: Sentry issue/event reads can include request/user context. + printf '%s\n' 'default_tools_approval_mode = "prompt"' + fi fi printf '%s\n' "$codex_policy_end" } > "$codex_config_candidate" diff --git a/scripts/test-environment.mjs b/scripts/test-environment.mjs index 3320132727..ca694ec16b 100644 --- a/scripts/test-environment.mjs +++ b/scripts/test-environment.mjs @@ -22,6 +22,9 @@ const providerEnvironmentKeys = Object.freeze([ "E2E_AUTH_ENABLED", "E2E_USER_EMAIL", "E2E_USER_PASSWORD", + "SENTRY_AUTH_TOKEN", + "SENTRY_DSN", + "NEXT_PUBLIC_SENTRY_DSN", "ALLOW_PROVIDER_TESTS", ]); @@ -32,6 +35,13 @@ const offlineUrlValues = Object.freeze({ DATABASE_URL: "postgresql://offline:offline@127.0.0.1:1/offline", }); +// Feature flags, not credentials: force off in offline wrappers without joining +// the Cloud credential inventory (which would require setup/raw-env scrubbing). +const offlineSentryControlFlags = Object.freeze({ + SENTRY_ENABLE_LOGS: "false", + SENTRY_SEND_TEST_LOG: "false", +}); + /** * @param {Record} source * @param {Record} overrides @@ -41,7 +51,15 @@ export function offlineTestEnvironment(source = process.env, overrides = {}) { // Explicit values both scrub inherited secrets and prevent Next/Vite from // repopulating the same names from a repository-local env file. URL-shaped // settings use inert loopback values so the runtime env schema still parses. - for (const key of providerEnvironmentKeys) environment[key] = offlineUrlValues[key] ?? ""; + // SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN are blanked (not deleted or faked): an + // absent key lets Next reload a live DSN from `.env.local` during + // Playwright/Lighthouse production starts, and a truthy inert URL keeps app + // Sentry gates enabled. Empty string stays falsy for `SENTRY_DSN?.trim()` / + // public DSN checks and is coerced to unset by the env schema. + for (const key of providerEnvironmentKeys) { + environment[key] = offlineUrlValues[key] ?? ""; + } + Object.assign(environment, offlineSentryControlFlags); return { ...environment, diff --git a/src/lib/env.ts b/src/lib/env.ts index 7b0e37cc7a..7950af68a9 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -5,6 +5,9 @@ import { resolvePythonBin } from "@/lib/python-bin"; import { assertExpectedSupabaseProjectConfig, checkSupabaseProjectConfig } from "@/lib/supabase/project"; import { MAX_UPLOAD_MB_CEILING } from "@/lib/upload-limits"; +/** Treat blank/whitespace as unset so offline scrub can pin `SENTRY_DSN=""`. */ +const blankOptionalUrl = (value: unknown) => (typeof value === "string" && value.trim() === "" ? undefined : value); + const envSchema = z.object({ NEXT_PUBLIC_SUPABASE_URL: z.string().url().optional(), NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: z.string().optional(), @@ -40,7 +43,8 @@ const envSchema = z.object({ LOCAL_NO_AUTH_OWNER_EMAIL: z.string().optional(), LOCAL_NO_AUTH_OWNER_ID: z.string().uuid().optional(), NEXT_PUBLIC_MOCKUPS_ENABLED: z.enum(["true", "false"]).optional(), - NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(), + // Keep `z.`-prefixed declarations so scripts/check-env-parity.mjs collects names. + NEXT_PUBLIC_SENTRY_DSN: z.preprocess(blankOptionalUrl, z.string().url().optional()), NEXT_PUBLIC_SENTRY_RELEASE: z.string().optional(), // Optional release tag for Sentry production readability and source-map correlation // (for example: a short git SHA or deployment ID). @@ -50,7 +54,7 @@ const envSchema = z.object({ SENTRY_PROJECT: z.string().optional(), SENTRY_AUTH_TOKEN: z.string().optional(), OPENAI_API_KEY: z.string().optional(), - SENTRY_DSN: z.string().url().optional(), + SENTRY_DSN: z.preprocess(blankOptionalUrl, z.string().url().optional()), OPENAI_EMBEDDING_MODEL: z.string().default("text-embedding-3-small"), // Must match the vector(N) dimension in supabase/schema.sql. Changing the embedding // model without updating this (and the schema) silently corrupts ingestion (IDX-C2). diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index de77427370..4233eef5b9 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -71,6 +71,9 @@ const requiredPolicyExcludes = [ "CODEX_TRIGGER_TOKEN", "HEALTH_DEEP_PROBE_SECRET", "INDEXING_V3_AGENT_SECRET", + "SENTRY_AUTH_TOKEN", + "SENTRY_DSN", + "NEXT_PUBLIC_SENTRY_DSN", "E2E_AUTH_ENABLED", "E2E_USER_EMAIL", "E2E_USER_PASSWORD", @@ -356,18 +359,54 @@ describe("Codex Cloud environment contract", () => { ); expect( validateCodexProjectMcpConfiguration( - tracked.replace('default_tools_approval_mode = "writes"', 'default_tools_approval_mode = "auto"'), + tracked.replace( + /(\[mcp_servers\.railway_cloud\][\s\S]*?)default_tools_approval_mode = "writes"/, + '$1default_tools_approval_mode = "auto"', + ), ), ).toContain( - `.codex/config.toml figma_cloud must set default_tools_approval_mode = "writes" because write-capable tools require explicit approval.`, + `.codex/config.toml railway_cloud must set default_tools_approval_mode = "writes" because write-capable tools require explicit approval.`, ); expect( validateCodexProjectMcpConfiguration( - tracked.replace('default_tools_approval_mode = "prompt"', 'default_tools_approval_mode = "auto"'), + tracked.replace( + /(\[mcp_servers\.supabase_cloud\][\s\S]*?)default_tools_approval_mode = "prompt"/, + '$1default_tools_approval_mode = "auto"', + ), ), ).toContain( `.codex/config.toml supabase_cloud must set default_tools_approval_mode = "prompt" because the production server is constrained read-only.`, ); + expect( + validateCodexProjectMcpConfiguration( + tracked.replace( + /(\[mcp_servers\.frontendchecklist_cloud\][\s\S]*?)default_tools_approval_mode = "prompt"/, + '$1default_tools_approval_mode = "auto"', + ), + ), + ).toContain( + `.codex/config.toml frontendchecklist_cloud must set default_tools_approval_mode = "prompt" because external read tools require explicit approval.`, + ); + expect( + validateCodexProjectMcpConfiguration( + tracked.replace( + /(\[mcp_servers\.figma_cloud\][\s\S]*?)default_tools_approval_mode = "prompt"/, + '$1default_tools_approval_mode = "auto"', + ), + ), + ).toContain( + `.codex/config.toml figma_cloud must set default_tools_approval_mode = "prompt" because external read tools require explicit approval.`, + ); + expect( + validateCodexProjectMcpConfiguration( + tracked.replace( + /(\[mcp_servers\.sentry_cloud\][\s\S]*?)default_tools_approval_mode = "prompt"/, + '$1default_tools_approval_mode = "auto"', + ), + ), + ).toContain( + `.codex/config.toml sentry_cloud must set default_tools_approval_mode = "prompt" because external read tools require explicit approval.`, + ); expect( validateCodexProjectMcpConfiguration( tracked.replace( @@ -461,6 +500,9 @@ describe("Codex Cloud environment contract", () => { expect(setup).toContain('if [[ "$actual_version" != "$expected_version" ]]'); expect(setup).toContain('"$HOME/.bash_profile"'); expect(setup.match(/unset npm_config_http_proxy npm_config_https_proxy npm_config_proxy/g)).toHaveLength(2); + // Setup process + generated agent profile both export the Cloud proxy flag. + expect(setup.match(/export NODE_USE_ENV_PROXY=1/g)).toHaveLength(2); + expect(setup.indexOf("export NODE_USE_ENV_PROXY=1")).toBeLessThan(setup.indexOf("npm install --global")); expect(setup.indexOf("unset OPENAI_API_KEY")).toBeLessThan( setup.indexOf('if [ "\\$CODEX_CLOUD_ACCESS_PROFILE" = "connected" ]'), ); @@ -519,6 +561,9 @@ describe("Codex Cloud environment contract", () => { expect(config).toContain('inherit = "all"'); expect(config).not.toContain("[mcp_servers.railway_connected]"); expect(config).not.toContain("[mcp_servers.supabase_connected]"); + expect(config).not.toContain("[mcp_servers.figma_connected]"); + expect(config).not.toContain("[mcp_servers.frontendchecklist_connected]"); + expect(config).not.toContain("[mcp_servers.sentry_connected]"); for (const name of requiredPolicyExcludes) { expect(config).toContain(`"${name}"`); } @@ -543,18 +588,52 @@ describe("Codex Cloud environment contract", () => { const connected = runSetupPolicyOnly(connectedHome, { CODEX_CLOUD_ACCESS_PROFILE: "connected", RAG_PROVIDER_MODE: "offline", + // Pin opt-ins off so ambient host/CI ENABLE_*=1 cannot flip baseline asserts. + CODEX_CLOUD_ENABLE_FIGMA: "0", + CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST: "0", + CODEX_CLOUD_ENABLE_SENTRY: "0", }); expect(connected.status, connected.stderr || connected.stdout).toBe(0); const connectedProfile = readRuntimeProfile(connectedHome); const connectedConfig = readCodexConfig(connectedHome); + // Restricted connected profile: Railway + Supabase only unless opted in. expect(connectedConfig).toContain("[mcp_servers.railway_connected]"); expect(connectedConfig).toContain("[mcp_servers.supabase_connected]"); expect(connectedConfig).toContain("features=docs%2Cdevelopment"); + expect(connectedConfig).not.toContain("[mcp_servers.figma_connected]"); + expect(connectedConfig).not.toContain("[mcp_servers.frontendchecklist_connected]"); + expect(connectedConfig).not.toContain("[mcp_servers.sentry_connected]"); expect(connectedConfig.match(/^enabled = true$/gm)).toHaveLength(2); - expect(connectedConfig).toContain('default_tools_approval_mode = "prompt"'); + expect(connectedConfig.match(/^default_tools_approval_mode = "writes"$/gm)).toHaveLength(1); + expect(connectedConfig.match(/^default_tools_approval_mode = "prompt"$/gm)).toHaveLength(1); expect(connectedProfile).toContain('export CODEX_CLOUD_ACCESS_PROFILE="connected"'); expect(connectedProfile).toContain('export RAG_PROVIDER_MODE="offline"'); + expect(connectedProfile).toContain("export NODE_USE_ENV_PROXY=1"); expect(connectedProfile).not.toContain("${RAG_PROVIDER_MODE:-auto}"); + expect(connectedProfile).toContain("unset SENTRY_AUTH_TOKEN SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN"); + + const optedInHome = temporaryDirectory("codex-cloud-connected-opt-in-"); + const optedIn = runSetupPolicyOnly(optedInHome, { + CODEX_CLOUD_ACCESS_PROFILE: "connected", + RAG_PROVIDER_MODE: "offline", + CODEX_CLOUD_ENABLE_FIGMA: "1", + CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST: "1", + CODEX_CLOUD_ENABLE_SENTRY: "1", + }); + expect(optedIn.status, optedIn.stderr || optedIn.stdout).toBe(0); + const optedInConfig = readCodexConfig(optedInHome); + expect(optedInConfig).toContain("[mcp_servers.figma_connected]"); + expect(optedInConfig).toContain('url = "https://mcp.figma.com/mcp"'); + expect(optedInConfig).toContain("[mcp_servers.frontendchecklist_connected]"); + expect(optedInConfig).toContain('url = "https://mcp.frontendchecklist.io"'); + expect(optedInConfig).toContain("[mcp_servers.railway_connected]"); + expect(optedInConfig).toContain("[mcp_servers.supabase_connected]"); + expect(optedInConfig).toContain("[mcp_servers.sentry_connected]"); + expect(optedInConfig).toContain('url = "https://mcp.sentry.dev/mcp"'); + expect(optedInConfig.match(/^enabled = true$/gm)).toHaveLength(5); + // Railway keeps writes; Figma, Frontend Checklist, Supabase, and Sentry use prompt. + expect(optedInConfig.match(/^default_tools_approval_mode = "writes"$/gm)).toHaveLength(1); + expect(optedInConfig.match(/^default_tools_approval_mode = "prompt"$/gm)).toHaveLength(4); const unmanagedHome = temporaryDirectory("codex-cloud-unmanaged-"); mkdirSync(path.join(unmanagedHome, ".codex"), { recursive: true }); diff --git a/tests/env-sentry.test.ts b/tests/env-sentry.test.ts index 0bbbddb4b4..ffe1dc7901 100644 --- a/tests/env-sentry.test.ts +++ b/tests/env-sentry.test.ts @@ -28,6 +28,19 @@ describe("requireSentryEnv", () => { expect(() => requireSentryEnv()).not.toThrow(); }); + it("treats blank DSN env values as unset", async () => { + const { env, requireSentryEnv } = await loadEnv({ + NEXT_PUBLIC_SENTRY_DSN: "", + SENTRY_DSN: "", + SENTRY_ORG: undefined, + SENTRY_PROJECT: undefined, + SENTRY_AUTH_TOKEN: undefined, + }); + expect(env.SENTRY_DSN).toBeUndefined(); + expect(env.NEXT_PUBLIC_SENTRY_DSN).toBeUndefined(); + expect(() => requireSentryEnv()).not.toThrow(); + }); + it("does not throw on partial build-time sourcemap credentials at runtime", async () => { const { requireSentryEnv } = await loadEnv({ NEXT_PUBLIC_SENTRY_DSN: undefined, diff --git a/tests/repo-hygiene.test.ts b/tests/repo-hygiene.test.ts index 0397ac0361..ed13becbbc 100644 --- a/tests/repo-hygiene.test.ts +++ b/tests/repo-hygiene.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { computeParity, @@ -44,7 +45,9 @@ describe("check-env-parity name parsing", () => { " RAG_PERSIST_RAW_QUERY_TEXT: z", ' .enum(["true", "false"])', ' .default("false"),', + " SENTRY_DSN: z.preprocess(blankOptionalUrl, z.string().url().optional()),", " notAKey: 3,", + " DROPPED_SHARED: optionalUrlEnv,", "});", ].join("\n"); const names = parseEnvSchemaNames(text); @@ -52,7 +55,15 @@ describe("check-env-parity name parsing", () => { expect(names).toContain("SUPABASE_SERVICE_ROLE_KEY"); expect(names).toContain("OPENAI_MAX_OUTPUT_TOKENS"); expect(names).toContain("RAG_PERSIST_RAW_QUERY_TEXT"); + expect(names).toContain("SENTRY_DSN"); expect(names).not.toContain("notAKey"); + expect(names).not.toContain("DROPPED_SHARED"); + }); + + it("keeps Sentry DSN keys in the live env.ts schema name contract", () => { + const envTs = readFileSync(new URL("../src/lib/env.ts", import.meta.url), "utf8"); + const names = parseEnvSchemaNames(envTs); + expect(names).toEqual(expect.arrayContaining(["SENTRY_DSN", "NEXT_PUBLIC_SENTRY_DSN"])); }); it("extracts names from check-ci-env quoted literals and process.env access", () => { diff --git a/tests/sentry-logging.test.ts b/tests/sentry-logging.test.ts index a5312984db..87b2f9f03c 100644 --- a/tests/sentry-logging.test.ts +++ b/tests/sentry-logging.test.ts @@ -16,8 +16,13 @@ describe("isSentryLoggingEnabled", () => { }); it("requires a DSN and defaults on when the flag is unset", () => { + // Default-parameter path: undefined reads process.env.SENTRY_ENABLE_LOGS. + vi.stubEnv("SENTRY_ENABLE_LOGS", ""); expect(isSentryLoggingEnabled(undefined, undefined)).toBe(false); expect(isSentryLoggingEnabled(undefined, "https://example.ingest.sentry.io/1")).toBe(true); + // Explicit empty flag (same semantics as unset) and configured values. + expect(isSentryLoggingEnabled("", undefined)).toBe(false); + expect(isSentryLoggingEnabled("", "https://example.ingest.sentry.io/1")).toBe(true); expect(isSentryLoggingEnabled("false", "https://example.ingest.sentry.io/1")).toBe(false); expect(isSentryLoggingEnabled("0", "https://example.ingest.sentry.io/1")).toBe(false); expect(isSentryLoggingEnabled("true", "https://example.ingest.sentry.io/1")).toBe(true); diff --git a/tests/test-runner-safety.test.ts b/tests/test-runner-safety.test.ts index b91b72990b..3df82a8f7e 100644 --- a/tests/test-runner-safety.test.ts +++ b/tests/test-runner-safety.test.ts @@ -558,7 +558,20 @@ describe("provider-safe test environment", () => { }); for (const key of providerEnvironmentKeys) { expect(environment[key]).toBe(offlineUrlValues[key as keyof typeof offlineUrlValues] ?? ""); + // Explicit (including blank) values keep Next/Vite from reloading secrets. + expect(Object.hasOwn(environment, key)).toBe(true); } + expect(providerEnvironmentKeys).toEqual( + expect.arrayContaining(["SENTRY_AUTH_TOKEN", "SENTRY_DSN", "NEXT_PUBLIC_SENTRY_DSN"]), + ); + expect(providerEnvironmentKeys).not.toContain("SENTRY_ENABLE_LOGS"); + expect(providerEnvironmentKeys).not.toContain("SENTRY_SEND_TEST_LOG"); + expect(environment.SENTRY_AUTH_TOKEN).toBe(""); + // Explicit blank pins the key so Next/Vite cannot reload a live DSN from .env.local. + expect(environment.SENTRY_DSN).toBe(""); + expect(environment.NEXT_PUBLIC_SENTRY_DSN).toBe(""); + expect(environment.SENTRY_ENABLE_LOGS).toBe("false"); + expect(environment.SENTRY_SEND_TEST_LOG).toBe("false"); }); it("requires explicit permission before live tests can run", () => {