Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .codex/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Secret-free, project-scoped MCP registrations for trusted Codex hosts.
# OAuth credentials remain in each host credential store — never commit them here.
#
# checked-in policy:
# - enabled = false so ordinary/offline Codex sessions do not initialize providers
# - default_tools_approval_mode = "auto" so once a connected Cloud/host layer enables
# a server for a task, agents use it without per-tool permission prompts
# - Paid API canaries (eval:rag, eval:retrieval:quality, eval:quality, verify:release,
# test:live, check:supabase-project) still need explicit confirmation per AGENTS.md
# Runtime Cloud MCP remains `.mcp.json` (Railway + read-only Supabase only).

[mcp_servers.figma_cloud]
url = "https://mcp.figma.com/mcp"
Comment thread
cursor[bot] marked this conversation as resolved.
enabled = false
default_tools_approval_mode = "auto"

[mcp_servers.supabase_cloud]
# Production project, read_only=true, approved feature groups only.
# OAuth scopes come from the hosted Supabase consent screen; do not broaden this URL.
url = "https://mcp.supabase.com/mcp?project_ref=sjrfecxgysukkwxsowpy&read_only=true&features=docs%2Cdatabase%2Cdebugging%2Cdevelopment"
enabled = false
default_tools_approval_mode = "auto"

[mcp_servers.railway_cloud]
url = "https://mcp.railway.com"
enabled = false
default_tools_approval_mode = "auto"

[mcp_servers.sentry_cloud]
url = "https://mcp.sentry.dev/mcp"
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
enabled = false
default_tools_approval_mode = "auto"
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
/.playwright-cli/
/playwright/.auth/
/.local/
/.codex/
# Track only the secret-free project MCP template. Host-local Codex files under
# .codex/ stay ignored. Never put OAuth tokens, bearer headers, or env secrets in
# config.toml — credentials belong in the host credential store. CI Gitleaks and
# check:codex-cloud reject secret-bearing keys in the tracked template.
/.codex/*
!/.codex/config.toml
Comment thread
cursor[bot] marked this conversation as resolved.
/.agents/*
!/.agents/plugins/
/.agents/plugins/**
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,12 @@ Use `docs/codex-cloud.md` as the environment contract:
- Configure maintenance as `bash scripts/maintain-codex-cloud.sh && bash scripts/install-codex-cloud-command-shims.sh`.
- Default to `CODEX_CLOUD_ACCESS_PROFILE=offline` for ordinary and protected RAG work.
Use `connected` only when the user explicitly authorizes the required provider access.
- When MCP tools are already available in a Cloud session and the task needs them, use them
without asking for per-tool permission. Paid API canaries (`eval:rag`,
`eval:retrieval:quality`, `eval:quality`, `verify:release`, `test:live`,
`check:supabase-project`) still need explicit confirmation. Project `.codex/config.toml`
keeps MCP entries `enabled = false` so ordinary/offline Codex hosts do not initialize them;
runtime Cloud MCP remains `.mcp.json` plus the host environment.
- 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.
Expand Down
25 changes: 13 additions & 12 deletions docs/codebase-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +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 |
| `.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` holds secret-free MCP registrations (Figma, read-only Supabase, Railway, Sentry) with `enabled = false` and `default_tools_approval_mode = "auto"`. Runtime Cloud MCP stays `.mcp.json`. Other `.codex/*` stays gitignored; 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.

Expand Down
14 changes: 14 additions & 0 deletions docs/codex-cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,20 @@ and storage mutations require a separately configured non-production project or
broaden the production entry. OpenAI generation, Supabase live data, Railway changes, hosted CI
reruns, ingestion, deployment, and release workflows remain separate explicit actions.

Project `.codex/config.toml` is a second, project-scoped MCP template that trusted Codex
hosts load in addition to `$CODEX_HOME/config.toml` (where `setup-codex-cloud.sh` writes the
shell-environment policy). It is not inert documentation: Codex applies project-local
`.codex/config.toml` when the project is trusted. The tracked template lists Figma
(`https://mcp.figma.com/mcp`), Railway, read-only Supabase, and Sentry
(`https://mcp.sentry.dev/mcp`) as URL-only registrations with `enabled = false` and
`default_tools_approval_mode = "auto"`. Ordinary/offline sessions therefore do not initialize
those providers. When a connected Cloud or host layer enables a server for a task, tools are
auto-approved so agents avoid per-tool permission prompts; paid API canaries, Railway
mutations/deploys, and other cost-or-write provider actions still require explicit confirmation
per AGENTS.md. Figma and Sentry OAuth credentials stay in the host credential store — never in
the tracked file. Runtime Cloud MCP allowlist remains `.mcp.json` (Railway + read-only Supabase
only). `npm run check:codex-cloud` validates both files.

In a fresh connected Cloud session, run `npm run check:codex-cloud -- --environment` before any
provider call. The sanitized report must show `CODEX_CLOUD_ACCESS_PROFILE=connected`, every
provider environment variable as `present=false`, the credential-free `BigSimmo/Database` origin,
Expand Down
155 changes: 154 additions & 1 deletion scripts/check-codex-cloud-setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,163 @@ export const expectedCloudCliVersions = Object.freeze({
});

export const expectedMcpConfiguration = Object.freeze({
railwayUrl: "https://mcp.railway.com/",
// Canonical form matches `.mcp.json` (no trailing slash).
railwayUrl: "https://mcp.railway.com",
supabaseUrl: "https://mcp.supabase.com/mcp",
supabaseProjectRef: "sjrfecxgysukkwxsowpy",
supabaseFeatures: Object.freeze(["database", "debugging", "development", "docs"]),
});

/** Project `.codex/config.toml` registrations — disabled by default, secret-free URLs only. */
export const expectedCodexProjectMcpServers = Object.freeze({
figma_cloud: Object.freeze({ url: "https://mcp.figma.com/mcp" }),
railway_cloud: Object.freeze({ url: expectedMcpConfiguration.railwayUrl }),
sentry_cloud: Object.freeze({ url: "https://mcp.sentry.dev/mcp" }),
supabase_cloud: Object.freeze({
// URL validated with the same project/read-only/feature rules as `.mcp.json`.
kind: "supabase",
}),
});

const forbiddenCodexProjectMcpKeys = Object.freeze([
"bearer_token_env_var",
"command",
"env",
"env_http_headers",
"env_vars",
"headers",
"http_headers",
]);

function parseTomlScalar(value) {
const trimmed = value.trim();
if (trimmed === "true") return true;
if (trimmed === "false") return false;
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
return trimmed.slice(1, -1);
}
return trimmed;
}

/**
* Parse `[mcp_servers.name]` tables from a Codex project config.toml.
* Supports only the scalar keys this gate governs.
* @param {string} text
* @returns {Record<string, Record<string, unknown>>}
*/
export function parseCodexProjectMcpServers(text) {
/** @type {Record<string, Record<string, unknown>>} */
const servers = {};
let current = null;
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) continue;

const nested = line.match(/^\[mcp_servers\.([A-Za-z0-9_-]+)\./);
if (nested) {
current = nested[1];
servers[current] ??= {};
servers[current].__hasNestedTables = true;
continue;
}

const table = line.match(/^\[mcp_servers\.([A-Za-z0-9_-]+)\]$/);
if (table) {
current = table[1];
servers[current] ??= {};
continue;
}

if (line.startsWith("[")) {
current = null;
continue;
}
if (!current) continue;

const kv = line.match(/^([A-Za-z0-9_]+)\s*=\s*(.+)$/);
if (!kv) continue;
servers[current][kv[1]] = parseTomlScalar(kv[2]);
}
return servers;
}

function validateSupabaseMcpUrl(urlString, label, errors) {
try {
const url = new URL(urlString);
if (`${url.origin}${url.pathname}` !== expectedMcpConfiguration.supabaseUrl) {
errors.push(`${label} must use the official hosted endpoint.`);
}
if (url.searchParams.get("project_ref") !== expectedMcpConfiguration.supabaseProjectRef) {
errors.push(`${label} must be scoped to the expected project.`);
}
if (url.searchParams.get("read_only") !== "true") {
errors.push(`${label} must keep the production project read-only.`);
}
const queryNames = [...url.searchParams.keys()].sort();
if (JSON.stringify(queryNames) !== JSON.stringify(["features", "project_ref", "read_only"])) {
errors.push(`${label} must not include additional query parameters.`);
}
const features = (url.searchParams.get("features") ?? "").split(",").filter(Boolean).sort();
if (JSON.stringify(features) !== JSON.stringify(expectedMcpConfiguration.supabaseFeatures)) {
errors.push(`${label} must expose only the approved read-only feature groups.`);
}
} catch {
errors.push(`${label} URL must be valid.`);
}
}

/**
* Project `.codex/config.toml` must register the approved MCP surface as disabled
* URL-only templates so offline/ordinary Codex hosts do not initialize providers.
* @param {string} text
* @returns {string[]}
*/
export function validateCodexProjectMcpConfiguration(text) {
const errors = [];
const servers = parseCodexProjectMcpServers(text);
const expectedNames = Object.keys(expectedCodexProjectMcpServers).sort();
const actualNames = Object.keys(servers).sort();
if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) {
errors.push(`.codex/config.toml must register exactly these MCP servers: ${expectedNames.join(", ")}.`);
}

for (const name of expectedNames) {
const server = servers[name];
if (!server) continue;
const label = `.codex/config.toml ${name}`;

if (server.enabled !== false) {
errors.push(`${label} must set enabled = false (host/connected layers opt in).`);
}
if (server.default_tools_approval_mode !== "auto") {
errors.push(
`${label} must set default_tools_approval_mode = "auto" so connected Cloud tasks avoid per-tool prompts.`,
);
}
for (const key of forbiddenCodexProjectMcpKeys) {
if (server[key] !== undefined) {
errors.push(`${label} must not embed ${key}; keep OAuth credentials in the host store.`);
}
}
if (server.__hasNestedTables) {
errors.push(`${label} must not declare nested tool override tables in the shared project config.`);
}
if (typeof server.url !== "string" || !server.url) {
errors.push(`${label} must declare a secret-free url.`);
continue;
}

const expected = expectedCodexProjectMcpServers[name];
if (expected.kind === "supabase") {
validateSupabaseMcpUrl(server.url, label, errors);
} else if (server.url !== expected.url) {
errors.push(`${label} must use the pinned endpoint ${expected.url}.`);
}
}

return errors;
}

export const providerCredentialVariables = Object.freeze([
...providerEnvironmentKeys,
"RAILWAY_API_TOKEN",
Expand Down Expand Up @@ -319,6 +470,7 @@ export function validateCodexCloudSetup() {
const envExample = read(".env.example");
const gitignore = read(".gitignore");
const mcp = read(".mcp.json");
const codexProjectConfig = read(".codex/config.toml");

if (packageJson.engines?.node !== `${nodeVersion}.x`) {
errors.push(`package.json engines.node must match .node-version (${nodeVersion}.x).`);
Expand Down Expand Up @@ -465,6 +617,7 @@ export function validateCodexCloudSetup() {
errors.push(error instanceof Error ? error.message : String(error));
}
errors.push(...validateMcpConfiguration(mcp));
errors.push(...validateCodexProjectMcpConfiguration(codexProjectConfig));

for (const command of [
"check:supabase-project",
Expand Down
Loading
Loading