From e2bb0ec0bb06f0b4e872031383f5a3e8cd55125c Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 23:33:44 +0800 Subject: [PATCH 01/16] feat(cloud): enable hosted MCP parity Activate the audited Figma, Frontend Checklist, Railway, read-only Supabase, and Sentry MCP endpoints in connected Cloud profiles while preserving offline defaults and host-managed OAuth. Verified with the focused Cloud setup suite and static Cloud contract check. --- .codex/config.toml | 5 ++++ docs/codex-cloud.md | 37 ++++++++++++++++++++++------- scripts/check-codex-cloud-setup.mjs | 8 ++++++- scripts/setup-codex-cloud.sh | 12 ++++++++++ tests/codex-cloud-setup.test.ts | 18 ++++++++++++-- 5 files changed, 69 insertions(+), 11 deletions(-) diff --git a/.codex/config.toml b/.codex/config.toml index 90047c7796..6145b578f5 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -14,6 +14,11 @@ url = "https://mcp.figma.com/mcp" enabled = false default_tools_approval_mode = "writes" +[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. # OAuth scopes come from the hosted Supabase consent screen; do not broaden this URL. diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index cc91207436..6eacc79447 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,8 +288,8 @@ 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. +setup activates audited Figma, Frontend Checklist, Railway, constrained Supabase, and Sentry URLs +in its managed `$CODEX_HOME/config.toml` block; the first authenticated 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 @@ -301,6 +301,24 @@ confirmation. Do not use unrestricted SQL or query clinical rows. Railway, Figma write-capable tools remain approval-gated. 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` | Managed `figma_connected` hosted MCP; OAuth remains in the host store | +| `frontendchecklist` | Managed `frontendchecklist_connected` hosted MCP; provider 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` | Managed `sentry_connected` hosted MCP | +| `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 proof. Verify Railway and Supabase with read-only identity/project metadata calls and report only @@ -320,15 +338,16 @@ 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 only the five audited hosted endpoints into the + managed host MCP block; it never writes OAuth tokens. 2. **Grant the host integrations.** Authorize the Codex GitHub connector for `BigSimmo/Database` with repository write access. Complete Railway OAuth only for workspace `bigsimmo's Projects` and project `Database` (`5deaad0b-675a-4c13-978e-5ca2b5b877f9`). Complete Supabase OAuth only for the organization containing `Clinical KB Database`; retain project ref `sjrfecxgysukkwxsowpy`, `read_only=true`, and the 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 for the intended + workspace and organization; their write-capable tools remain approval-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 @@ -344,7 +363,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-setup.mjs b/scripts/check-codex-cloud-setup.mjs index 432f192e89..a80293643d 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -34,6 +34,10 @@ export const expectedCodexProjectMcpServers = Object.freeze({ url: "https://mcp.figma.com/mcp", approvalMode: "writes", }), + frontendchecklist_cloud: Object.freeze({ + url: "https://mcp.frontendchecklist.io", + approvalMode: "prompt", + }), railway_cloud: Object.freeze({ url: expectedMcpConfiguration.railwayUrl, approvalMode: "writes", @@ -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)) { diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index c34d81d709..f05dcee21c 100644 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -207,6 +207,14 @@ 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 + printf '\n%s\n' '[mcp_servers.figma_connected]' + printf '%s\n' 'url = "https://mcp.figma.com/mcp"' + printf '%s\n' 'enabled = true' + printf '%s\n' 'default_tools_approval_mode = "writes"' + printf '\n%s\n' '[mcp_servers.frontendchecklist_connected]' + printf '%s\n' 'url = "https://mcp.frontendchecklist.io"' + printf '%s\n' 'enabled = true' + printf '%s\n' 'default_tools_approval_mode = "prompt"' printf '\n%s\n' '[mcp_servers.railway_connected]' printf '%s\n' 'url = "https://mcp.railway.com"' printf '%s\n' 'enabled = true' @@ -215,6 +223,10 @@ trap 'rm -f "$codex_config_candidate"' EXIT printf 'url = "%s"\n' "$connected_supabase_mcp_url" printf '%s\n' 'enabled = true' printf '%s\n' 'default_tools_approval_mode = "prompt"' + printf '\n%s\n' '[mcp_servers.sentry_connected]' + printf '%s\n' 'url = "https://mcp.sentry.dev/mcp"' + printf '%s\n' 'enabled = true' + printf '%s\n' 'default_tools_approval_mode = "writes"' fi printf '%s\n' "$codex_policy_end" } > "$codex_config_candidate" diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index de77427370..603f9d9ff4 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -363,7 +363,10 @@ describe("Codex Cloud environment contract", () => { ); 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.`, @@ -519,6 +522,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}"`); } @@ -547,10 +553,18 @@ 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.figma_connected]"); + expect(connectedConfig).toContain('url = "https://mcp.figma.com/mcp"'); + expect(connectedConfig).toContain("[mcp_servers.frontendchecklist_connected]"); + expect(connectedConfig).toContain('url = "https://mcp.frontendchecklist.io"'); expect(connectedConfig).toContain("[mcp_servers.railway_connected]"); expect(connectedConfig).toContain("[mcp_servers.supabase_connected]"); + expect(connectedConfig).toContain("[mcp_servers.sentry_connected]"); + expect(connectedConfig).toContain('url = "https://mcp.sentry.dev/mcp"'); expect(connectedConfig).toContain("features=docs%2Cdevelopment"); - expect(connectedConfig.match(/^enabled = true$/gm)).toHaveLength(2); + expect(connectedConfig.match(/^enabled = true$/gm)).toHaveLength(5); + expect(connectedConfig.match(/^default_tools_approval_mode = "writes"$/gm)).toHaveLength(3); + expect(connectedConfig.match(/^default_tools_approval_mode = "prompt"$/gm)).toHaveLength(2); expect(connectedConfig).toContain('default_tools_approval_mode = "prompt"'); expect(connectedProfile).toContain('export CODEX_CLOUD_ACCESS_PROFILE="connected"'); expect(connectedProfile).toContain('export RAG_PROVIDER_MODE="offline"'); From 772e9f838ab12e37fb86a2dded0cf2ca7155a511 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 23:55:00 +0800 Subject: [PATCH 02/16] fix(cloud): route CLI downloads through proxy --- scripts/setup-codex-cloud.sh | 4 +++- tests/codex-cloud-setup.test.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index f05dcee21c..44b984eab8 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" diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index 603f9d9ff4..aabe23c55b 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -464,6 +464,7 @@ 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); + expect(setup.match(/export NODE_USE_ENV_PROXY=1/g)).toHaveLength(1); expect(setup.indexOf("unset OPENAI_API_KEY")).toBeLessThan( setup.indexOf('if [ "\\$CODEX_CLOUD_ACCESS_PROFILE" = "connected" ]'), ); From 0e4035fe31bca11f73934ca7d914c467d00a239a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 4 Aug 2026 17:06:50 +0000 Subject: [PATCH 03/16] fix(cloud): gate optional MCP and scrub Sentry credentials Connected setup now enables only Railway and constrained Supabase by default. Figma, Frontend Checklist, and Sentry require explicit CODEX_CLOUD_ENABLE_*=1 opt-ins and use prompt approval when enabled. Scrub SENTRY_AUTH_TOKEN and SENTRY_DSN from the agent shell inventory, and align AGENTS.md plus Cloud docs with the narrowed contract. Co-authored-by: BigSimmo --- AGENTS.md | 12 +++++--- docs/codex-cloud.md | 34 ++++++++++++---------- scripts/check-codex-cloud-raw-env.sh | 1 + scripts/check-codex-cloud-setup.mjs | 15 ++++++++++ scripts/setup-codex-cloud.sh | 36 +++++++++++++++-------- tests/codex-cloud-setup.test.ts | 43 +++++++++++++++++++++------- 6 files changed, 100 insertions(+), 41 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e8d53509df..0669390d72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -877,10 +877,14 @@ Use `docs/codex-cloud.md` as the environment contract: 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 task sets + `CODEX_CLOUD_ENABLE_FIGMA=1`, `CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST=1`, or + `CODEX_CLOUD_ENABLE_SENTRY=1`; 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/codex-cloud.md b/docs/codex-cloud.md index 6eacc79447..379827d0bd 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -288,18 +288,21 @@ 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 activates audited Figma, Frontend Checklist, Railway, constrained Supabase, and Sentry URLs -in its managed `$CODEX_HOME/config.toml` block; the first authenticated 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 environment sets `CODEX_CLOUD_ENABLE_FIGMA=1`, +`CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST=1`, or `CODEX_CLOUD_ENABLE_SENTRY=1` for that task; 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. 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 @@ -307,11 +310,11 @@ Docker Desktop, a local filesystem path, or a raw secret. | Local registration | Connected Cloud resolution | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `figma_cloud` | Managed `figma_connected` hosted MCP; OAuth remains in the host store | -| `frontendchecklist` | Managed `frontendchecklist_connected` hosted MCP; provider calls prompt | +| `figma_cloud` | Opt-in managed `figma_connected` hosted MCP (`CODEX_CLOUD_ENABLE_FIGMA=1`); OAuth remains in the host store | +| `frontendchecklist` | 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` | Managed `sentry_connected` hosted MCP | +| `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 | @@ -338,16 +341,17 @@ 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 five audited hosted 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` opt-in for that task. 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. Complete Figma and Sentry OAuth only for the intended - workspace and organization; their write-capable tools remain approval-gated. Frontend Checklist - requires no repository credential and remains prompt-gated. + production Supabase MCP to write access. Complete Figma and Sentry OAuth only when the matching + `CODEX_CLOUD_ENABLE_*=1` opt-in is set 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 diff --git a/scripts/check-codex-cloud-raw-env.sh b/scripts/check-codex-cloud-raw-env.sh index cd37bbe435..ec82df4c11 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 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 a80293643d..5d8a91591d 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -229,6 +229,8 @@ export const providerCredentialVariables = Object.freeze([ "HEALTH_DEEP_PROBE_SECRET", "INDEXING_V3_AGENT_SECRET", "CROSS_TENANT_SERVICE_ROLE_KEY", + "SENTRY_AUTH_TOKEN", + "SENTRY_DSN", ]); function read(relativePath) { @@ -664,6 +666,19 @@ 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/, "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 44b984eab8..8c1f8413c1 100644 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -134,6 +134,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 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}" @@ -162,6 +163,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 E2E_AUTH_ENABLED E2E_USER_EMAIL E2E_USER_PASSWORD ALLOW_PROVIDER_TESTS ) codex_exclude_toml="" @@ -209,14 +211,8 @@ 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 - printf '\n%s\n' '[mcp_servers.figma_connected]' - printf '%s\n' 'url = "https://mcp.figma.com/mcp"' - printf '%s\n' 'enabled = true' - printf '%s\n' 'default_tools_approval_mode = "writes"' - printf '\n%s\n' '[mcp_servers.frontendchecklist_connected]' - printf '%s\n' 'url = "https://mcp.frontendchecklist.io"' - printf '%s\n' 'enabled = true' - printf '%s\n' 'default_tools_approval_mode = "prompt"' + # Baseline connected providers: Railway + constrained Supabase. + # Optional OAuth-backed providers stay off unless the task explicitly opts in. printf '\n%s\n' '[mcp_servers.railway_connected]' printf '%s\n' 'url = "https://mcp.railway.com"' printf '%s\n' 'enabled = true' @@ -225,10 +221,26 @@ trap 'rm -f "$codex_config_candidate"' EXIT printf 'url = "%s"\n' "$connected_supabase_mcp_url" printf '%s\n' 'enabled = true' printf '%s\n' 'default_tools_approval_mode = "prompt"' - printf '\n%s\n' '[mcp_servers.sentry_connected]' - printf '%s\n' 'url = "https://mcp.sentry.dev/mcp"' - printf '%s\n' 'enabled = true' - printf '%s\n' 'default_tools_approval_mode = "writes"' + if [[ "${CODEX_CLOUD_ENABLE_FIGMA:-0}" = "1" ]]; then + printf '\n%s\n' '[mcp_servers.figma_connected]' + printf '%s\n' 'url = "https://mcp.figma.com/mcp"' + 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 '%s\n' 'url = "https://mcp.frontendchecklist.io"' + 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 '%s\n' 'url = "https://mcp.sentry.dev/mcp"' + 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/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index aabe23c55b..071039630f 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -71,6 +71,8 @@ const requiredPolicyExcludes = [ "CODEX_TRIGGER_TOKEN", "HEALTH_DEEP_PROBE_SECRET", "INDEXING_V3_AGENT_SECRET", + "SENTRY_AUTH_TOKEN", + "SENTRY_DSN", "E2E_AUTH_ENABLED", "E2E_USER_EMAIL", "E2E_USER_PASSWORD", @@ -554,22 +556,43 @@ 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.figma_connected]"); - expect(connectedConfig).toContain('url = "https://mcp.figma.com/mcp"'); - expect(connectedConfig).toContain("[mcp_servers.frontendchecklist_connected]"); - expect(connectedConfig).toContain('url = "https://mcp.frontendchecklist.io"'); + // 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("[mcp_servers.sentry_connected]"); - expect(connectedConfig).toContain('url = "https://mcp.sentry.dev/mcp"'); expect(connectedConfig).toContain("features=docs%2Cdevelopment"); - expect(connectedConfig.match(/^enabled = true$/gm)).toHaveLength(5); - expect(connectedConfig.match(/^default_tools_approval_mode = "writes"$/gm)).toHaveLength(3); - expect(connectedConfig.match(/^default_tools_approval_mode = "prompt"$/gm)).toHaveLength(2); - expect(connectedConfig).toContain('default_tools_approval_mode = "prompt"'); + 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.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).not.toContain("${RAG_PROVIDER_MODE:-auto}"); + expect(connectedProfile).toContain("unset SENTRY_AUTH_TOKEN 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 }); From 24fd23efb0757f8345cd7be2146fd669280a13ab Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:31:57 +0800 Subject: [PATCH 04/16] fix: scrub Sentry from offline test environments --- scripts/test-environment.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/test-environment.mjs b/scripts/test-environment.mjs index 3320132727..16f3d16126 100644 --- a/scripts/test-environment.mjs +++ b/scripts/test-environment.mjs @@ -22,6 +22,8 @@ const providerEnvironmentKeys = Object.freeze([ "E2E_AUTH_ENABLED", "E2E_USER_EMAIL", "E2E_USER_PASSWORD", + "SENTRY_AUTH_TOKEN", + "SENTRY_DSN", "ALLOW_PROVIDER_TESTS", ]); From 344813ea3ebd6969f3a1390523ef06c39304ecbd Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:32:04 +0800 Subject: [PATCH 05/16] refactor: centralize Sentry credential inventory --- scripts/check-codex-cloud-setup.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/check-codex-cloud-setup.mjs b/scripts/check-codex-cloud-setup.mjs index 5d8a91591d..d3fbb7a108 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -229,8 +229,6 @@ export const providerCredentialVariables = Object.freeze([ "HEALTH_DEEP_PROBE_SECRET", "INDEXING_V3_AGENT_SECRET", "CROSS_TENANT_SERVICE_ROLE_KEY", - "SENTRY_AUTH_TOKEN", - "SENTRY_DSN", ]); function read(relativePath) { From 8a4ad8ff53072bc796c81c0039006222bca6c068 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:32:13 +0800 Subject: [PATCH 06/16] test: pin Sentry offline credential scrubbing --- tests/test-runner-safety.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test-runner-safety.test.ts b/tests/test-runner-safety.test.ts index b91b72990b..2eaa10251a 100644 --- a/tests/test-runner-safety.test.ts +++ b/tests/test-runner-safety.test.ts @@ -559,6 +559,9 @@ describe("provider-safe test environment", () => { for (const key of providerEnvironmentKeys) { expect(environment[key]).toBe(offlineUrlValues[key as keyof typeof offlineUrlValues] ?? ""); } + expect(providerEnvironmentKeys).toEqual(expect.arrayContaining(["SENTRY_AUTH_TOKEN", "SENTRY_DSN"])); + expect(environment.SENTRY_AUTH_TOKEN).toBe(""); + expect(environment.SENTRY_DSN).toBe(""); }); it("requires explicit permission before live tests can run", () => { From 299b8c2845c1cf0da8404d4155420bd438a7c83a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:40:07 +0800 Subject: [PATCH 07/16] fix: use a valid inert Sentry DSN offline --- scripts/test-environment.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/test-environment.mjs b/scripts/test-environment.mjs index 16f3d16126..37ff5e5c43 100644 --- a/scripts/test-environment.mjs +++ b/scripts/test-environment.mjs @@ -32,6 +32,7 @@ const offlineUrlValues = Object.freeze({ SUPABASE_URL: "http://127.0.0.1:1", SUPABASE_DB_URL: "postgresql://offline:offline@127.0.0.1:1/offline", DATABASE_URL: "postgresql://offline:offline@127.0.0.1:1/offline", + SENTRY_DSN: "http://127.0.0.1:1", }); /** From 93b240d55a07c93098565837772c99b581b4f3ac Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:40:16 +0800 Subject: [PATCH 08/16] test: expect inert offline Sentry endpoint --- tests/test-runner-safety.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-runner-safety.test.ts b/tests/test-runner-safety.test.ts index 2eaa10251a..8477bb1760 100644 --- a/tests/test-runner-safety.test.ts +++ b/tests/test-runner-safety.test.ts @@ -561,7 +561,7 @@ describe("provider-safe test environment", () => { } expect(providerEnvironmentKeys).toEqual(expect.arrayContaining(["SENTRY_AUTH_TOKEN", "SENTRY_DSN"])); expect(environment.SENTRY_AUTH_TOKEN).toBe(""); - expect(environment.SENTRY_DSN).toBe(""); + expect(environment.SENTRY_DSN).toBe("http://127.0.0.1:1"); }); it("requires explicit permission before live tests can run", () => { From 8a869266fc9a31b696adb2e61006dbe51f126255 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:52:51 +0800 Subject: [PATCH 09/16] fix(tests): keep offline Sentry logging disabled --- scripts/test-environment.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/test-environment.mjs b/scripts/test-environment.mjs index 37ff5e5c43..ad75964c49 100644 --- a/scripts/test-environment.mjs +++ b/scripts/test-environment.mjs @@ -24,6 +24,8 @@ const providerEnvironmentKeys = Object.freeze([ "E2E_USER_PASSWORD", "SENTRY_AUTH_TOKEN", "SENTRY_DSN", + "SENTRY_ENABLE_LOGS", + "SENTRY_SEND_TEST_LOG", "ALLOW_PROVIDER_TESTS", ]); @@ -33,6 +35,8 @@ const offlineUrlValues = Object.freeze({ SUPABASE_DB_URL: "postgresql://offline:offline@127.0.0.1:1/offline", DATABASE_URL: "postgresql://offline:offline@127.0.0.1:1/offline", SENTRY_DSN: "http://127.0.0.1:1", + SENTRY_ENABLE_LOGS: "false", + SENTRY_SEND_TEST_LOG: "false", }); /** From 68c1f17909802c1d0e7b8e999de2259de4072dc7 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:52:52 +0800 Subject: [PATCH 10/16] test: pin offline Sentry disable flags --- tests/test-runner-safety.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test-runner-safety.test.ts b/tests/test-runner-safety.test.ts index 8477bb1760..04294d3194 100644 --- a/tests/test-runner-safety.test.ts +++ b/tests/test-runner-safety.test.ts @@ -559,9 +559,13 @@ describe("provider-safe test environment", () => { for (const key of providerEnvironmentKeys) { expect(environment[key]).toBe(offlineUrlValues[key as keyof typeof offlineUrlValues] ?? ""); } - expect(providerEnvironmentKeys).toEqual(expect.arrayContaining(["SENTRY_AUTH_TOKEN", "SENTRY_DSN"])); + expect(providerEnvironmentKeys).toEqual( + expect.arrayContaining(["SENTRY_AUTH_TOKEN", "SENTRY_DSN", "SENTRY_ENABLE_LOGS", "SENTRY_SEND_TEST_LOG"]), + ); expect(environment.SENTRY_AUTH_TOKEN).toBe(""); expect(environment.SENTRY_DSN).toBe("http://127.0.0.1:1"); + expect(environment.SENTRY_ENABLE_LOGS).toBe("false"); + expect(environment.SENTRY_SEND_TEST_LOG).toBe("false"); }); it("requires explicit permission before live tests can run", () => { From 8e116a85d2643f492210c67a37bfc5a28219e65c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 09:47:43 +0000 Subject: [PATCH 11/16] fix(tests): scrub Sentry offline without fake DSN or flag inventory Keep SENTRY_AUTH_TOKEN/SENTRY_DSN in the shared credential inventory, but delete SENTRY_DSN in offline wrappers instead of faking a URL that keeps app gates enabled. Leave SENTRY_ENABLE_LOGS/SENTRY_SEND_TEST_LOG as offline control flags outside providerCredentialVariables so Cloud setup inventory checks stay aligned. Assert unset logging with an explicit empty flag rather than undefined (which reads process.env defaults). Co-authored-by: BigSimmo --- scripts/test-environment.mjs | 19 +++++++++++++++---- tests/sentry-logging.test.ts | 6 ++++-- tests/test-runner-safety.test.ts | 12 ++++++++---- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/scripts/test-environment.mjs b/scripts/test-environment.mjs index ad75964c49..8a1ad2931f 100644 --- a/scripts/test-environment.mjs +++ b/scripts/test-environment.mjs @@ -24,8 +24,6 @@ const providerEnvironmentKeys = Object.freeze([ "E2E_USER_PASSWORD", "SENTRY_AUTH_TOKEN", "SENTRY_DSN", - "SENTRY_ENABLE_LOGS", - "SENTRY_SEND_TEST_LOG", "ALLOW_PROVIDER_TESTS", ]); @@ -34,7 +32,11 @@ const offlineUrlValues = Object.freeze({ SUPABASE_URL: "http://127.0.0.1:1", SUPABASE_DB_URL: "postgresql://offline:offline@127.0.0.1:1/offline", DATABASE_URL: "postgresql://offline:offline@127.0.0.1:1/offline", - SENTRY_DSN: "http://127.0.0.1:1", +}); + +// 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", }); @@ -48,7 +50,16 @@ 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 is deleted (not blanked or faked): blank fails Zod `.url()`, and a + // truthy inert URL keeps app Sentry gates enabled in offline Vitest/Playwright. + for (const key of providerEnvironmentKeys) { + if (key === "SENTRY_DSN") { + delete environment[key]; + continue; + } + environment[key] = offlineUrlValues[key] ?? ""; + } + Object.assign(environment, offlineSentryControlFlags); return { ...environment, diff --git a/tests/sentry-logging.test.ts b/tests/sentry-logging.test.ts index a5312984db..4ea1c8820e 100644 --- a/tests/sentry-logging.test.ts +++ b/tests/sentry-logging.test.ts @@ -16,8 +16,10 @@ describe("isSentryLoggingEnabled", () => { }); it("requires a DSN and defaults on when the flag is unset", () => { - expect(isSentryLoggingEnabled(undefined, undefined)).toBe(false); - expect(isSentryLoggingEnabled(undefined, "https://example.ingest.sentry.io/1")).toBe(true); + // Pass "" for an explicit unset flag. Calling with `undefined` reads + // process.env.SENTRY_ENABLE_LOGS (default param), which offline Vitest forces off. + 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 04294d3194..8fb85d0ed4 100644 --- a/tests/test-runner-safety.test.ts +++ b/tests/test-runner-safety.test.ts @@ -557,13 +557,17 @@ describe("provider-safe test environment", () => { NEXT_PUBLIC_DEMO_MODE: "true", }); for (const key of providerEnvironmentKeys) { + if (key === "SENTRY_DSN") { + expect(environment[key]).toBeUndefined(); + continue; + } expect(environment[key]).toBe(offlineUrlValues[key as keyof typeof offlineUrlValues] ?? ""); } - expect(providerEnvironmentKeys).toEqual( - expect.arrayContaining(["SENTRY_AUTH_TOKEN", "SENTRY_DSN", "SENTRY_ENABLE_LOGS", "SENTRY_SEND_TEST_LOG"]), - ); + expect(providerEnvironmentKeys).toEqual(expect.arrayContaining(["SENTRY_AUTH_TOKEN", "SENTRY_DSN"])); + expect(providerEnvironmentKeys).not.toContain("SENTRY_ENABLE_LOGS"); + expect(providerEnvironmentKeys).not.toContain("SENTRY_SEND_TEST_LOG"); expect(environment.SENTRY_AUTH_TOKEN).toBe(""); - expect(environment.SENTRY_DSN).toBe("http://127.0.0.1:1"); + expect(environment.SENTRY_DSN).toBeUndefined(); expect(environment.SENTRY_ENABLE_LOGS).toBe("false"); expect(environment.SENTRY_SEND_TEST_LOG).toBe("false"); }); From aa5592efe3bde0d986198b77ee10b1518b1a340f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 11:51:01 +0000 Subject: [PATCH 12/16] fix(tests): pin blank Sentry DSN offline and cover default flag path (#1613) Blank SENTRY_DSN in offline wrappers so Next cannot reload a live DSN from .env.local, while optionalUrlEnv coerces empty strings to unset for Zod. Exercise isSentryLoggingEnabled's default-parameter path via stubEnv. Co-authored-by: BigSimmo --- scripts/test-environment.mjs | 10 ++++------ src/lib/env.ts | 10 ++++++++-- tests/env-sentry.test.ts | 13 +++++++++++++ tests/sentry-logging.test.ts | 7 +++++-- tests/test-runner-safety.test.ts | 7 ++----- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/scripts/test-environment.mjs b/scripts/test-environment.mjs index 8a1ad2931f..4470acd80f 100644 --- a/scripts/test-environment.mjs +++ b/scripts/test-environment.mjs @@ -50,13 +50,11 @@ 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. - // SENTRY_DSN is deleted (not blanked or faked): blank fails Zod `.url()`, and a - // truthy inert URL keeps app Sentry gates enabled in offline Vitest/Playwright. + // SENTRY_DSN is 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()` and is coerced to unset by the env schema. for (const key of providerEnvironmentKeys) { - if (key === "SENTRY_DSN") { - delete environment[key]; - continue; - } environment[key] = offlineUrlValues[key] ?? ""; } Object.assign(environment, offlineSentryControlFlags); diff --git a/src/lib/env.ts b/src/lib/env.ts index 7b0e37cc7a..7cd011d795 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -5,6 +5,12 @@ import { resolvePythonBin } from "@/lib/python-bin"; import { assertExpectedSupabaseProjectConfig, checkSupabaseProjectConfig } from "@/lib/supabase/project"; import { MAX_UPLOAD_MB_CEILING } from "@/lib/upload-limits"; +/** Optional URL env: treat blank/whitespace as unset so offline scrub can pin "". */ +const optionalUrlEnv = z.preprocess( + (value) => (typeof value === "string" && value.trim() === "" ? undefined : value), + z.string().url().optional(), +); + const envSchema = z.object({ NEXT_PUBLIC_SUPABASE_URL: z.string().url().optional(), NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: z.string().optional(), @@ -40,7 +46,7 @@ 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(), + NEXT_PUBLIC_SENTRY_DSN: optionalUrlEnv, 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 +56,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: optionalUrlEnv, 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/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/sentry-logging.test.ts b/tests/sentry-logging.test.ts index 4ea1c8820e..87b2f9f03c 100644 --- a/tests/sentry-logging.test.ts +++ b/tests/sentry-logging.test.ts @@ -16,8 +16,11 @@ describe("isSentryLoggingEnabled", () => { }); it("requires a DSN and defaults on when the flag is unset", () => { - // Pass "" for an explicit unset flag. Calling with `undefined` reads - // process.env.SENTRY_ENABLE_LOGS (default param), which offline Vitest forces off. + // 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); diff --git a/tests/test-runner-safety.test.ts b/tests/test-runner-safety.test.ts index 8fb85d0ed4..73929ccb40 100644 --- a/tests/test-runner-safety.test.ts +++ b/tests/test-runner-safety.test.ts @@ -557,17 +557,14 @@ describe("provider-safe test environment", () => { NEXT_PUBLIC_DEMO_MODE: "true", }); for (const key of providerEnvironmentKeys) { - if (key === "SENTRY_DSN") { - expect(environment[key]).toBeUndefined(); - continue; - } expect(environment[key]).toBe(offlineUrlValues[key as keyof typeof offlineUrlValues] ?? ""); } expect(providerEnvironmentKeys).toEqual(expect.arrayContaining(["SENTRY_AUTH_TOKEN", "SENTRY_DSN"])); expect(providerEnvironmentKeys).not.toContain("SENTRY_ENABLE_LOGS"); expect(providerEnvironmentKeys).not.toContain("SENTRY_SEND_TEST_LOG"); expect(environment.SENTRY_AUTH_TOKEN).toBe(""); - expect(environment.SENTRY_DSN).toBeUndefined(); + // Explicit blank pins the key so Next/Vite cannot reload a live DSN from .env.local. + expect(environment.SENTRY_DSN).toBe(""); expect(environment.SENTRY_ENABLE_LOGS).toBe("false"); expect(environment.SENTRY_SEND_TEST_LOG).toBe("false"); }); From 9cc575c4af4a125f281b9bea093c81551710388c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 11:52:08 +0000 Subject: [PATCH 13/16] fix(cloud): address PR 1613 review findings for offline Sentry and MCP setup Pin blank SENTRY_DSN in offline wrappers so Next cannot reload a live DSN, treat empty optional URLs as unset in env parsing, resolve connected MCP URLs from the audited config template, and harden related docs/tests. Co-authored-by: BigSimmo --- .codex/config.toml | 5 +++-- docs/codex-cloud.md | 2 +- scripts/setup-codex-cloud.sh | 36 +++++++++++++++++++++++++++----- scripts/test-environment.mjs | 10 ++++----- src/lib/env.ts | 20 +++++++++++------- tests/codex-cloud-setup.test.ts | 19 ++++++++++++++++- tests/env-sentry.test.ts | 13 ++++++++++++ tests/sentry-logging.test.ts | 8 +++---- tests/test-runner-safety.test.ts | 8 +++---- 9 files changed, 90 insertions(+), 31 deletions(-) diff --git a/.codex/config.toml b/.codex/config.toml index 6145b578f5..f27d710036 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" +# - production Supabase and Frontend Checklist use "prompt" so every metadata/read +# operation requires explicit approval; write-capable Figma/Railway/Sentry tools +# use "writes" in this template (connected opt-ins may further tighten to "prompt") # - 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. diff --git a/docs/codex-cloud.md b/docs/codex-cloud.md index 2d205a4a0a..3e185be8fe 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -313,7 +313,7 @@ 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` | Opt-in managed `frontendchecklist_connected` hosted MCP (`CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST=1`); calls prompt | +| `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 | diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index 8c1f8413c1..3a177c345a 100644 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -100,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" @@ -122,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. @@ -213,8 +238,9 @@ trap 'rm -f "$codex_config_candidate"' EXIT 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]' @@ -223,20 +249,20 @@ trap 'rm -f "$codex_config_candidate"' EXIT 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 '%s\n' 'url = "https://mcp.figma.com/mcp"' + 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 '%s\n' 'url = "https://mcp.frontendchecklist.io"' + 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 '%s\n' 'url = "https://mcp.sentry.dev/mcp"' + 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"' diff --git a/scripts/test-environment.mjs b/scripts/test-environment.mjs index 8a1ad2931f..3c8cd79b17 100644 --- a/scripts/test-environment.mjs +++ b/scripts/test-environment.mjs @@ -50,13 +50,11 @@ 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. - // SENTRY_DSN is deleted (not blanked or faked): blank fails Zod `.url()`, and a - // truthy inert URL keeps app Sentry gates enabled in offline Vitest/Playwright. + // SENTRY_DSN is blanked (not deleted or faked): delete lets @next/env reload a + // real DSN from `.env.local` into Playwright/Lighthouse builds, and a truthy + // inert URL keeps app Sentry gates enabled. Empty string stays set so Next will + // not override it; `src/lib/env.ts` treats blank Sentry DSNs as unset. for (const key of providerEnvironmentKeys) { - if (key === "SENTRY_DSN") { - delete environment[key]; - continue; - } environment[key] = offlineUrlValues[key] ?? ""; } Object.assign(environment, offlineSentryControlFlags); diff --git a/src/lib/env.ts b/src/lib/env.ts index 7b0e37cc7a..7e5d372245 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -5,8 +5,14 @@ import { resolvePythonBin } from "@/lib/python-bin"; import { assertExpectedSupabaseProjectConfig, checkSupabaseProjectConfig } from "@/lib/supabase/project"; import { MAX_UPLOAD_MB_CEILING } from "@/lib/upload-limits"; +/** Treat blank env values as unset so offline scrubbers can pin empty strings. */ +const optionalUrl = z.preprocess( + (value) => (typeof value === "string" && value.trim() === "" ? undefined : value), + z.string().url().optional(), +); + const envSchema = z.object({ - NEXT_PUBLIC_SUPABASE_URL: z.string().url().optional(), + NEXT_PUBLIC_SUPABASE_URL: optionalUrl, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: z.string().optional(), SUPABASE_PROJECT_REF: z.string().optional(), SUPABASE_PROJECT_NAME: z.string().optional(), @@ -16,7 +22,7 @@ const envSchema = z.object({ SUPABASE_STAGING_PROJECT_REF: z.string().optional(), SUPABASE_STAGING_PROJECT_NAME: z.string().optional(), SUPABASE_SERVICE_ROLE_KEY: z.string().optional(), - SUPABASE_DB_URL: z.string().url().optional(), + SUPABASE_DB_URL: optionalUrl, HEALTH_DEEP_PROBE_SECRET: z.string().min(16).optional(), // Inbound webhook receivers. Each shared secret gates a machine-to-machine // endpoint under /api/webhooks/* and fails closed when unset (the route 503s @@ -32,15 +38,15 @@ const envSchema = z.object({ // Optional outbound chat destinations shared by every /api/webhooks/* forwarder // and the CI-failure GitHub workflow. Set either, both, or neither; a receiver // with no destination configured accepts the event and reports it undelivered. - SLACK_WEBHOOK_URL: z.string().url().optional(), - DISCORD_WEBHOOK_URL: z.string().url().optional(), - WORKER_FAILURE_WEBHOOK_URL: z.string().url().optional(), + SLACK_WEBHOOK_URL: optionalUrl, + DISCORD_WEBHOOK_URL: optionalUrl, + WORKER_FAILURE_WEBHOOK_URL: optionalUrl, NEXT_PUBLIC_LOCAL_NO_AUTH: z.enum(["true", "false"]).optional().default("false"), LOCAL_NO_AUTH: z.enum(["true", "false"]).optional().default("false"), 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(), + NEXT_PUBLIC_SENTRY_DSN: optionalUrl, 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 +56,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: optionalUrl, 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 071039630f..4a4675d0e9 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -373,6 +373,16 @@ describe("Codex Cloud environment contract", () => { ).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( @@ -466,7 +476,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); - expect(setup.match(/export NODE_USE_ENV_PROXY=1/g)).toHaveLength(1); + // 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" ]'), ); @@ -552,6 +564,10 @@ 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); @@ -568,6 +584,7 @@ describe("Codex Cloud environment contract", () => { 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"); diff --git a/tests/env-sentry.test.ts b/tests/env-sentry.test.ts index 0bbbddb4b4..dcb3508ac5 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 Sentry DSNs as unset so offline scrubbers can pin empty strings", 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/sentry-logging.test.ts b/tests/sentry-logging.test.ts index 4ea1c8820e..390044b5c5 100644 --- a/tests/sentry-logging.test.ts +++ b/tests/sentry-logging.test.ts @@ -16,10 +16,10 @@ describe("isSentryLoggingEnabled", () => { }); it("requires a DSN and defaults on when the flag is unset", () => { - // Pass "" for an explicit unset flag. Calling with `undefined` reads - // process.env.SENTRY_ENABLE_LOGS (default param), which offline Vitest forces off. - expect(isSentryLoggingEnabled("", undefined)).toBe(false); - expect(isSentryLoggingEnabled("", "https://example.ingest.sentry.io/1")).toBe(true); + // Stub the env default and pass undefined so the default-parameter path runs. + vi.stubEnv("SENTRY_ENABLE_LOGS", ""); + expect(isSentryLoggingEnabled(undefined, undefined)).toBe(false); + expect(isSentryLoggingEnabled(undefined, "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 8fb85d0ed4..0327c0c67c 100644 --- a/tests/test-runner-safety.test.ts +++ b/tests/test-runner-safety.test.ts @@ -557,17 +557,15 @@ describe("provider-safe test environment", () => { NEXT_PUBLIC_DEMO_MODE: "true", }); for (const key of providerEnvironmentKeys) { - if (key === "SENTRY_DSN") { - expect(environment[key]).toBeUndefined(); - continue; - } 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"])); expect(providerEnvironmentKeys).not.toContain("SENTRY_ENABLE_LOGS"); expect(providerEnvironmentKeys).not.toContain("SENTRY_SEND_TEST_LOG"); expect(environment.SENTRY_AUTH_TOKEN).toBe(""); - expect(environment.SENTRY_DSN).toBeUndefined(); + expect(environment.SENTRY_DSN).toBe(""); expect(environment.SENTRY_ENABLE_LOGS).toBe("false"); expect(environment.SENTRY_SEND_TEST_LOG).toBe("false"); }); From c35f61400078ded68bf34e96a882e8a3711061c8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 12:03:42 +0000 Subject: [PATCH 14/16] fix(env): keep Sentry DSN keys visible to env-parity name parser Declare blank-tolerant Sentry DSN fields with inline z.preprocess so check-env-parity still collects them, and cover the parser contract in repo-hygiene tests. Record the PR #1613 babysit review ledger row. Co-authored-by: BigSimmo --- docs/branch-review-ledger.md | 1 + src/lib/env.ts | 12 +++++------- tests/repo-hygiene.test.ts | 11 +++++++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index c4c89484cc..b90e70fe5a 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -624,3 +624,4 @@ 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 | diff --git a/src/lib/env.ts b/src/lib/env.ts index 7cd011d795..7950af68a9 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -5,11 +5,8 @@ import { resolvePythonBin } from "@/lib/python-bin"; import { assertExpectedSupabaseProjectConfig, checkSupabaseProjectConfig } from "@/lib/supabase/project"; import { MAX_UPLOAD_MB_CEILING } from "@/lib/upload-limits"; -/** Optional URL env: treat blank/whitespace as unset so offline scrub can pin "". */ -const optionalUrlEnv = z.preprocess( - (value) => (typeof value === "string" && value.trim() === "" ? undefined : value), - z.string().url().optional(), -); +/** 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(), @@ -46,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: optionalUrlEnv, + // 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). @@ -56,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: optionalUrlEnv, + 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/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", () => { From c390d810c23dfca48bfa6ef7e9c8d11c020c7ed2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 12:04:16 +0000 Subject: [PATCH 15/16] docs(ledger): supersede PR #1613 babysit review at env-parity fix head Co-authored-by: BigSimmo --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index b90e70fe5a..a77daed34c 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -625,3 +625,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 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 | From e9d2a822a832cbe09cfdd4acfa978b729ed73b1f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 12:14:00 +0000 Subject: [PATCH 16/16] fix(cloud): align MCP prompt posture and setup-time opt-in docs Match Figma/Sentry project template approval to connected prompt gating, scrub NEXT_PUBLIC_SENTRY_DSN offline like the server DSN, and clarify that CODEX_CLOUD_ENABLE_* flags are environment/setup-time only. Co-authored-by: BigSimmo --- .codex/config.toml | 10 +++++----- AGENTS.md | 11 ++++++---- docs/codebase-index.md | 26 ++++++++++++------------ docs/codex-cloud.md | 21 +++++++++++-------- scripts/check-codex-cloud-raw-env.sh | 2 +- scripts/check-codex-cloud-setup.mjs | 9 ++++++--- scripts/setup-codex-cloud.sh | 4 ++-- scripts/test-environment.mjs | 10 ++++++---- tests/codex-cloud-setup.test.ts | 30 +++++++++++++++++++++++++--- tests/test-runner-safety.test.ts | 5 ++++- 10 files changed, 84 insertions(+), 44 deletions(-) diff --git a/.codex/config.toml b/.codex/config.toml index f27d710036..8b4f9bb811 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -3,9 +3,9 @@ # # checked-in policy: # - enabled = false so ordinary/offline Codex sessions do not initialize providers -# - production Supabase and Frontend Checklist use "prompt" so every metadata/read -# operation requires explicit approval; write-capable Figma/Railway/Sentry tools -# use "writes" in this template (connected opt-ins may further tighten to "prompt") +# - 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. @@ -13,7 +13,7 @@ [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" @@ -35,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 9e01146229..11d4ca5b27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -873,15 +873,18 @@ 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 (`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 task sets - `CODEX_CLOUD_ENABLE_FIGMA=1`, `CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST=1`, or - `CODEX_CLOUD_ENABLE_SENTRY=1`; when enabled they use `prompt` approval so reads stay + 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. 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 3e185be8fe..764b6a04fc 100644 --- a/docs/codex-cloud.md +++ b/docs/codex-cloud.md @@ -289,11 +289,13 @@ 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 activates Railway and constrained Supabase by default. Optional Figma, Frontend Checklist, -and Sentry hosted endpoints stay off unless the environment sets `CODEX_CLOUD_ENABLE_FIGMA=1`, -`CODEX_CLOUD_ENABLE_FRONTENDCHECKLIST=1`, or `CODEX_CLOUD_ENABLE_SENTRY=1` for that task; 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. +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 @@ -345,15 +347,18 @@ copying credentials into the checkout. Do not add provider keys, database URLs, service-role credentials, test-user credentials, or `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` opt-in for that task. Setup never writes OAuth tokens. + 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. Complete Figma and Sentry OAuth only when the matching - `CODEX_CLOUD_ENABLE_*=1` opt-in is set for the intended workspace and organization; their tools - remain prompt-gated. Frontend Checklist requires no repository credential and remains prompt-gated. + `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 diff --git a/scripts/check-codex-cloud-raw-env.sh b/scripts/check-codex-cloud-raw-env.sh index ec82df4c11..c82debef8d 100644 --- a/scripts/check-codex-cloud-raw-env.sh +++ b/scripts/check-codex-cloud-raw-env.sh @@ -14,7 +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 + 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 d3fbb7a108..d4ba022507 100644 --- a/scripts/check-codex-cloud-setup.mjs +++ b/scripts/check-codex-cloud-setup.mjs @@ -32,7 +32,7 @@ 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", @@ -44,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`. @@ -664,7 +664,10 @@ 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/, "Cloud setup must scrub supported Sentry credentials 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.", diff --git a/scripts/setup-codex-cloud.sh b/scripts/setup-codex-cloud.sh index 3a177c345a..d86ba81673 100644 --- a/scripts/setup-codex-cloud.sh +++ b/scripts/setup-codex-cloud.sh @@ -159,7 +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 +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}" @@ -188,7 +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 + SENTRY_AUTH_TOKEN SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN E2E_AUTH_ENABLED E2E_USER_EMAIL E2E_USER_PASSWORD ALLOW_PROVIDER_TESTS ) codex_exclude_toml="" diff --git a/scripts/test-environment.mjs b/scripts/test-environment.mjs index 4470acd80f..ca694ec16b 100644 --- a/scripts/test-environment.mjs +++ b/scripts/test-environment.mjs @@ -24,6 +24,7 @@ const providerEnvironmentKeys = Object.freeze([ "E2E_USER_PASSWORD", "SENTRY_AUTH_TOKEN", "SENTRY_DSN", + "NEXT_PUBLIC_SENTRY_DSN", "ALLOW_PROVIDER_TESTS", ]); @@ -50,10 +51,11 @@ 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. - // SENTRY_DSN is 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()` and is coerced to unset by the env schema. + // 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] ?? ""; } diff --git a/tests/codex-cloud-setup.test.ts b/tests/codex-cloud-setup.test.ts index 4a4675d0e9..4233eef5b9 100644 --- a/tests/codex-cloud-setup.test.ts +++ b/tests/codex-cloud-setup.test.ts @@ -73,6 +73,7 @@ const requiredPolicyExcludes = [ "INDEXING_V3_AGENT_SECRET", "SENTRY_AUTH_TOKEN", "SENTRY_DSN", + "NEXT_PUBLIC_SENTRY_DSN", "E2E_AUTH_ENABLED", "E2E_USER_EMAIL", "E2E_USER_PASSWORD", @@ -358,10 +359,13 @@ 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( @@ -383,6 +387,26 @@ describe("Codex Cloud environment contract", () => { ).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( @@ -586,7 +610,7 @@ describe("Codex Cloud environment contract", () => { 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"); + 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, { diff --git a/tests/test-runner-safety.test.ts b/tests/test-runner-safety.test.ts index 976f8b919e..3df82a8f7e 100644 --- a/tests/test-runner-safety.test.ts +++ b/tests/test-runner-safety.test.ts @@ -561,12 +561,15 @@ describe("provider-safe test environment", () => { // 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"])); + 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"); });