Compare commits

...
Author SHA1 Message Date
Brent Vatne 12d40f4196 auth: the shared review token env is anthropic-owned
Close the gap the self-review found on #59: the new scaffolded default
CLAUDE_CODE_REVIEW_SHARED_API_TOKEN was in neither PROVIDER_KEY_ENV nor
ANTHROPIC_TOKEN_ENVS, so the cross-provider ownership guard resolved no
owner and a root config could point a non-anthropic provider at it —
prepareAuth would then forward the Anthropic credential to that provider
(both previous defaults were covered; the new name lost that binding).
Adding it to ANTHROPIC_TOKEN_ENVS makes such an entry refused, while the
scaffolded anthropic entry itself still passes.
2026-08-04 18:29:51 -07:00
Brent Vatne 0d215761f1 readme: document the Claude-first defaults
Match the template alignment: first-time setup describes the claude login /
setup-token flow and the CLAUDE_CODE_REVIEW_SHARED_API_TOKEN CI secret;
config/routing/frontmatter examples use anthropic model ids; model-selection
guidance recommends sonnet 5 with opus for security (and the coordinator, per
the scaffold); the providers section leads with Anthropic as the default and
keeps the OpenAI api-key/subscription setups as alternatives.
2026-08-04 18:19:50 -07:00
Brent Vatne 3f212182a4 templates: default new scaffolds to Claude (sonnet 5, opus for security/coordinator)
Align `ecr init` output with the fleet migration (this repo #43, eas-cli,
euxy): default model anthropic/claude-sonnet-5; the security agent and the
coordinator keep their deliberate stronger-tier pins, now as
anthropic/claude-opus-5; auth defaults to providers.anthropic with tokenEnv
CLAUDE_CODE_REVIEW_SHARED_API_TOKEN (the shared review credential the other
repos use). The openai api-key and ChatGPT/Codex oauth setups remain as
documented alternatives in the config template.

The workflow/command/atlantis templates install the Claude Code CLI pinned
to 2.1.212 and forward the new secret; DEFAULT_TOKEN_ENV and
substituteTokenEnv's credential-block marker move with them (the marker
must stay byte-identical to the block in workflow.yml AND command.yml).
dismiss.yml is untouched — it runs no model.
2026-08-04 18:12:57 -07:00
11 changed files with 182 additions and 140 deletions

No files matched your search

+58 -53
View File
@@ -48,21 +48,19 @@ npx @expo/code-review-cli setup-auth
npx @expo/code-review-cli doctor
```
`setup-auth` reads the repo's config and walks through each credential it needs:
an OpenAI **API key** (the scaffolded default — it prints where to create the key
and the exact restricted permissions to grant), and/or a **ChatGPT/Codex
subscription** sign-in (it runs OpenCode's browser login and extracts the token
for you). `doctor` offers to run it whenever a credential is missing.
`setup-auth` reads the repo's config and walks through each credential it needs.
The scaffolded default is **Anthropic via the Claude Code CLI**: locally your
`claude` login is enough, and for CI it helps you mint a token with
`claude setup-token`. It also handles the alternatives (an OpenAI **API key**,
a **ChatGPT/Codex subscription** sign-in). `doctor` offers to run it whenever a
credential is missing.
In CI, store the same values as repo secrets (`OPENAI_API_KEY`; plus
`CODEX_OAUTH_ACCESS_TOKEN` for the mixed setup) — the scaffolded workflow
forwards them.
In CI, store the credential as the repo secret the scaffolded workflow forwards
(`CLAUDE_CODE_REVIEW_SHARED_API_TOKEN` by default — an `sk-ant-oat…` token from
`claude setup-token`, or an `sk-ant-api…` Console key; the CLI reads either).
**Have a ChatGPT Plus/Pro (Codex) subscription? Use both.** The recommended
production setup pairs the subscription (runs the default models at no marginal
cost) with the usage-based key (covers only the pro-tier models the subscription
excludes) — see [the mixed setup](#other-providers) below. Prefer
**Anthropic/Claude** or another provider? Same section.
Prefer **OpenAI** (API key, or a ChatGPT/Codex subscription, or both mixed) or
another provider? See [Other providers & auth modes](#other-providers) below.
### Reviewing (already configured)
@@ -196,7 +194,7 @@ your-monorepo/
// Central guardrails every scope inherits and CANNOT override.
"defaults": {
// The ONLY place auth/tokenEnv is honored (besides the root config.jsonc).
"auth": { "mode": "api-key", "provider": "openai", "tokenEnv": "OPENAI_API_KEY" },
"auth": { "providers": { "anthropic": { "tokenEnv": "CLAUDE_CODE_REVIEW_SHARED_API_TOKEN" } } },
"enforceAgents": ["security"], // always runs on every scope, roster or not
"commentTag": "expo-ai-code-reviewer" // per-scope markers derive from this
},
@@ -214,7 +212,7 @@ your-monorepo/
// apps/api/.expo-code-review/config.jsonc (the api team owns this)
{
// NO "auth" block — locked centrally; a tokenEnv here is rejected by loader + CI guard.
"model": "openai/gpt-5.5",
"model": "anthropic/claude-sonnet-5",
"policy": { "includeSuggestions": false },
"noise": { "additionalIgnores": ["apps/api/**/__generated__/**"] }
// shared.md, coordinator.md, agents/*.md live beside this file — the api team's roster.
@@ -392,7 +390,7 @@ show writes (there is nothing to re-read within the run).
---
description: One line the router uses to decide relevance.
alwaysRun: true # run even when the router would skip this agent
model: openai/gpt-5.5-pro # override the default model
model: anthropic/claude-opus-5 # override the default model
temperature: 0.1
---
@@ -401,14 +399,14 @@ temperature: 0.1
For a real-world example, see eas-cli's
[`.expo-code-review/`](https://github.com/expo/eas-cli/tree/main/.expo-code-review)
— correctness/security/consistency agents, a stronger model for security + the
coordinator, and per-repo `noise.additionalIgnores`.
— correctness/security/consistency agents, a stronger model for security, and
per-repo `noise.additionalIgnores`.
`config.jsonc` (JSONC — comments + trailing commas supported):
```jsonc
{
"model": "openai/gpt-5.5", // default model for the specialists
"model": "anthropic/claude-sonnet-5", // default model for the specialists
"policy": { "includeSuggestions": false }, // suppress suggestion-severity findings
"chunk": { "maxChangedLines": 1000, "maxFiles": 20 }, // concurrency defaults: 6 (API key) / 3 (subscription)
"noise": { "additionalIgnores": ["packages/*/build/**"] },
@@ -417,8 +415,8 @@ coordinator, and per-repo `noise.additionalIgnores`.
"skipLabel": "ai-review:skip" }, // "label" (only labeled PRs)
"breakGlass": { "marker": "/skip-review" }, // PR body marker that skips the review
"commentTag": "expo-ai-code-reviewer", // hidden tag used to find/update the comment
"auth": { "mode": "api-key", "provider": "openai",
"tokenEnv": "OPENAI_API_KEY" }
"auth": { "providers": {
"anthropic": { "tokenEnv": "CLAUDE_CODE_REVIEW_SHARED_API_TOKEN" } } }
}
```
@@ -432,15 +430,17 @@ Precedence: **`REVIEWER_MODEL` env** (global override) → per-file **frontmatte
setup, and a developer can override everything locally.
- **Specialist agents** (correctness/security/consistency) benefit from a
reasoning-tier model — **`openai/gpt-5.5`** is the quality/speed sweet spot
(the scaffolded default). The **pro tier** finds more but is slower and more
expensive, so scope it to the highest-stakes agent: **security runs on
`openai/gpt-5.5-pro`** (set in `security.md` frontmatter), the rest on the default.
reasoning-tier model — **`anthropic/claude-sonnet-5`** is the quality/speed
sweet spot (the scaffolded default). The **Opus tier** finds more but is slower
and more expensive, so scope it to the highest-stakes agent: **security runs on
`anthropic/claude-opus-5`** (set in `security.md` frontmatter), the rest on the
default.
- **The coordinator** makes the final call (dedupe / re-judge / decide) — worth a
strong model; set it in `coordinator.md` frontmatter.
strong model; the scaffold pins it to `anthropic/claude-opus-5` in
`coordinator.md` frontmatter.
- If latency/timeouts dominate on big PRs, moving the specialists to a faster model
(e.g. `openai/gpt-5.4-mini`) is the most direct lever (a real recall tradeoff —
measure it).
(e.g. `anthropic/claude-haiku-4-5`) is the most direct lever (a real recall
tradeoff — measure it).
- **Every run logs which model actually answered each pass** — in the job log
(`Models used — …`), the Actions step summary table, and the run log's
`agentModels` — and warns loudly if a pass ran on a different model than
@@ -655,11 +655,34 @@ visible even in CI (where the run log is ephemeral).
<details>
<summary><b>Other providers & auth modes</b></summary>
The recommended setup is an OpenAI API key — see Usage above. Alternatives, all
set in `config.auth` (credentials come from OpenCode):
The scaffolded default is **Anthropic via the Claude Code CLI** — see First-time
setup above and the Anthropic bullet below for how it authenticates and is
sandboxed. Everything is set in `config.auth`; non-anthropic providers get their
credentials through OpenCode.
- **ChatGPT/Codex subscription (OAuth) + usage-based API key — the recommended
mix.** OpenAI permits subscription auth in third-party tools, and OpenCode
- **Anthropic / Claude (the default)** — use `anthropic/...` model ids and every
anthropic pass runs through the **Claude Code CLI** (`claude -p
--output-format json`), inferred from the model. The credential is (in order) a
`tokenEnv` you name, an ambient `CLAUDE_CODE_OAUTH_TOKEN`, or your local
`claude` login — an `auth` entry is entirely optional. Run `claude setup-token`
for a Max/Team subscription token or point `tokenEnv` at an Anthropic Console
API key (`sk-ant-api…`, forwarded as `ANTHROPIC_API_KEY`); the CLI reads
either. `ecr setup-auth` walks you through it. Each pass is trust-isolated and
read-only: it runs with `--safe-mode` (no `CLAUDE.md`/hooks/MCP/plugins),
`--strict-mcp-config`, `--permission-mode dontAsk`, and only the
`Read`/`Grep`/`Glob` tools — never `Bash`/`Edit`/`Write`/`WebFetch`/`WebSearch`.
The child env is an allowlist that omits ambient `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN`
(only the configured credential is re-injected).
```jsonc
// The scaffolded default. No anthropic entry at all falls back to your
// `claude` login.
"auth": { "providers": {
"anthropic": { "tokenEnv": "CLAUDE_CODE_REVIEW_SHARED_API_TOKEN" }
} }
```
- **OpenAI: ChatGPT/Codex subscription (OAuth) + usage-based API key — the
recommended mix if you review with OpenAI.** OpenAI permits subscription auth
in third-party tools, and OpenCode
ships the plugin for it — so the reviewer runs its default models on the
subscription (zero marginal cost) and reserves the metered key for pro-tier
models the subscription doesn't offer (`gpt-5.5-pro` is subscription-excluded).
@@ -698,25 +721,6 @@ set in `config.auth` (credentials come from OpenCode):
One caveat: OpenCode can't price alias models (they're config-declared), so
pro passes report `$0` in the run log's cost column — token counts are
correct, and the OpenAI project dashboard is the source of truth for spend.
- **Anthropic / Claude** — use `anthropic/...` model ids and every anthropic pass
runs through the **Claude Code CLI** (`claude -p --output-format json`), inferred
from the model. The credential is (in order) a `tokenEnv` you name, an ambient
`CLAUDE_CODE_OAUTH_TOKEN`, or your local `claude` login — an `auth` entry is
entirely optional. Run `claude setup-token` for a Max/Team subscription token
(forwarded as `CLAUDE_CODE_OAUTH_TOKEN`) or point `tokenEnv` at an Anthropic
Console API key (`sk-ant-api…`, forwarded as `ANTHROPIC_API_KEY`); the CLI reads
either. `ecr setup-auth` walks you through it. Each pass is trust-isolated and
read-only: it runs with `--safe-mode` (no `CLAUDE.md`/hooks/MCP/plugins),
`--strict-mcp-config`, `--permission-mode dontAsk`, and only the
`Read`/`Grep`/`Glob` tools — never `Bash`/`Edit`/`Write`/`WebFetch`/`WebSearch`.
The child env is an allowlist that omits ambient `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN`
(only the configured credential is re-injected).
```jsonc
// Optional — no anthropic entry at all falls back to your `claude` login.
"auth": { "providers": {
"anthropic": { "tokenEnv": "CLAUDE_CODE_OAUTH_TOKEN" }
} }
```
- **Another provider** — the current path is the `REVIEWER_MODEL`
env override: `opencode auth login` once (pick the provider), then run with
e.g. `REVIEWER_MODEL=google/gemini-3-pro`. It overrides every agent's model
@@ -729,8 +733,9 @@ any other) OpenCode provider, and each agent's `model` selects its engine.
`REVIEWER_MODEL` still overrides every agent's model (and therefore every agent's
engine), converging the whole run onto one engine.
There is no shared fallback key; if a run fails for lack of credentials, authenticate
a provider in OpenCode. `ecr doctor` diagnoses setup.
There is no shared fallback key; if a run fails for lack of credentials, log in
with `claude` (the default) or authenticate a provider in OpenCode. `ecr doctor`
diagnoses setup.
**Setup errors fail fast, with the fix in the message.** A bad credential or model id
would otherwise fail every pass identically — a run that spends its whole budget
+18 -2
View File
@@ -188,8 +188,14 @@ test("checkProviderAuth: FORBIDDEN + cross-provider guards still fire for an ant
test("checkProviderAuth: a non-anthropic entry naming CLAUDE_CODE_OAUTH_TOKEN is refused", () => {
// The long-lived Claude Max/Team subscription token would otherwise be forwarded to
// a FOREIGN provider (here openai) as its bearer — it is neither in FORBIDDEN nor a
// PROVIDER_KEY_ENV value, so only the anthropic-owned guard catches it.
for (const tokenEnv of ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_AUTH_TOKEN"]) {
// PROVIDER_KEY_ENV value, so only the anthropic-owned guard catches it. The
// scaffolded default CLAUDE_CODE_REVIEW_SHARED_API_TOKEN is anthropic-owned the
// same way: it always holds an Anthropic credential (templates/config.jsonc).
for (const tokenEnv of [
"CLAUDE_CODE_OAUTH_TOKEN",
"ANTHROPIC_AUTH_TOKEN",
"CLAUDE_CODE_REVIEW_SHARED_API_TOKEN",
]) {
const r = checkProviderAuth(cfg({ mode: "api-key", provider: "openai", tokenEnv }), {
[tokenEnv]: `sk-ant-oat01-${"x".repeat(95)}`,
});
@@ -207,6 +213,16 @@ test("checkProviderAuth: an anthropic entry naming CLAUDE_CODE_OAUTH_TOKEN is st
expect(r.ok).toBe(true);
});
test("checkProviderAuth: the scaffolded default setup (anthropic + shared token env) passes", () => {
// The exact shape `ecr init` scaffolds (templates/config.jsonc) — anthropic owns
// the shared env, so its own entry must not trip the ownership guard.
const r = checkProviderAuth(
cfg({ provider: "anthropic", tokenEnv: "CLAUDE_CODE_REVIEW_SHARED_API_TOKEN" }),
{ CLAUDE_CODE_REVIEW_SHARED_API_TOKEN: `sk-ant-oat01-${"x".repeat(95)}` },
);
expect(r.ok).toBe(true);
});
test("checkProviderAuth: an upstream=anthropic alias may name CLAUDE_CODE_OAUTH_TOKEN", () => {
// upstream is anthropic, so the owner matches the upstream and the token is not
// being sent to a foreign provider.
+8 -8
View File
@@ -132,7 +132,7 @@ test("init --token-env refuses when review workflows already exist without --for
path.join(root, ".github", "workflows", "expo-code-review.yml"),
"utf8",
);
expect(text).toContain("OPENAI_API_KEY");
expect(text).toContain("CLAUDE_CODE_REVIEW_SHARED_API_TOKEN");
expect(text).not.toContain("CLAUDE_CODE_OAUTH_TOKEN");
});
@@ -143,7 +143,7 @@ test("init --token-env --force rewrites existing review workflows", async () =>
for (const file of ["expo-code-review.yml", "expo-code-review-command.yml"]) {
const text = await readFile(path.join(root, ".github", "workflows", file), "utf8");
expect(text).toContain("CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}");
expect(text).not.toContain("OPENAI_API_KEY");
expect(text).not.toContain("CLAUDE_CODE_REVIEW_SHARED_API_TOKEN");
}
});
@@ -210,7 +210,7 @@ test("init --force-workflows refuses when only the forwarded secret line is non-
// The fallback alone reads as default, so the guard must also see the secret line.
const wf = path.join(root, ".github", "workflows", "expo-code-review.yml");
const patched = (await readFile(wf, "utf8")).replaceAll(
"OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}",
"CLAUDE_CODE_REVIEW_SHARED_API_TOKEN: ${{ secrets.CLAUDE_CODE_REVIEW_SHARED_API_TOKEN }}",
"ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}",
);
await writeFile(wf, patched, "utf8");
@@ -220,7 +220,7 @@ test("init --force-workflows refuses when only the forwarded secret line is non-
process.exitCode = 0;
expect(err).toContain("ANTHROPIC_API_KEY");
expect(err).toContain("--token-env");
// Untouched: no silent reversion to the default OpenAI wiring.
// Untouched: no silent reversion to the default wiring.
expect(await readFile(wf, "utf8")).toContain(
"ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}",
);
@@ -229,7 +229,7 @@ test("init --force-workflows refuses when only the forwarded secret line is non-
test("init --force-workflows without --token-env is fine on a default-credential repo", async () => {
const root = await freshRepo();
await initCommand([]);
// Default OPENAI_API_KEY wiring: nothing to revert, so no refusal.
// Default credential wiring: nothing to revert, so no refusal.
await initCommand(["--force-workflows"]);
expect(process.exitCode ?? 0).toBe(0);
expect(
@@ -240,8 +240,8 @@ test("init --force-workflows without --token-env is fine on a default-credential
test("init --token-env prints the config.jsonc auth edit as a next step", async () => {
await freshRepo();
const out = await captureStdout(() => initCommand(["--token-env", "CLAUDE_CODE_OAUTH_TOKEN"]));
// The scaffolded config.jsonc still declares OPENAI_API_KEY, so without this
// step CI's verify-config fails and the user has no pointer to why.
// The scaffolded config.jsonc still declares the default tokenEnv, so without
// this step CI's verify-config fails and the user has no pointer to why.
expect(out).toContain("config.jsonc at this credential");
expect(out).toContain("refuses to review until the config names `CLAUDE_CODE_OAUTH_TOKEN`");
});
@@ -275,7 +275,7 @@ test("substituteTokenEnv fails loudly when the template markers drift", () => {
});
test("parseTokenEnvs validates names and refuses well-known unrelated secrets", () => {
expect(parseTokenEnvs(undefined)).toEqual(["OPENAI_API_KEY"]);
expect(parseTokenEnvs(undefined)).toEqual(["CLAUDE_CODE_REVIEW_SHARED_API_TOKEN"]);
expect(parseTokenEnvs("A_TOKEN, B_TOKEN")).toEqual(["A_TOKEN", "B_TOKEN"]);
expect(() => parseTokenEnvs("lower_case")).toThrow(/UPPER_SNAKE_CASE/);
expect(() => parseTokenEnvs("GH_TOKEN")).toThrow(/unrelated secret/);
+18 -13
View File
@@ -27,9 +27,10 @@ Options:
--no-workflow Skip writing the CI workflows (review, command, and dismiss
under .github/workflows/)
--token-env <name[,name…]>
Env var(s) holding the model credential (default OPENAI_API_KEY,
e.g. CLAUDE_CODE_OAUTH_TOKEN). The scaffolded workflows forward
the matching repo secret(s) and expect this tokenEnv
Env var(s) holding the model credential (default
CLAUDE_CODE_REVIEW_SHARED_API_TOKEN, e.g. OPENAI_API_KEY). The
scaffolded workflows forward the matching repo secret(s) and
expect this tokenEnv
--force Overwrite existing files
--force-workflows
Overwrite only the CI workflow files, keeping your customized
@@ -38,7 +39,7 @@ Options:
-h, --help Show this help
`;
const DEFAULT_TOKEN_ENV = "OPENAI_API_KEY";
const DEFAULT_TOKEN_ENV = "CLAUDE_CODE_REVIEW_SHARED_API_TOKEN";
export async function initCommand(argv: string[]): Promise<void> {
if (argv.includes("-h") || argv.includes("--help")) {
@@ -107,7 +108,7 @@ async function scaffold(argv: string[]): Promise<void> {
// The reverse of the guard above: --force-workflows (or --force) always rewrites
// the review workflows from the pristine template, which forwards the default
// OPENAI_API_KEY unless --token-env names the credential again. An adopter who
// Anthropic token env unless --token-env names the credential again. An adopter who
// scaffolded with a non-default tokenEnv and later refreshes the workflow YAML
// without re-passing --token-env would silently lose the forwarded secret, and
// CI would keep passing the auth lock but run with an empty credential. Refuse
@@ -121,7 +122,7 @@ async function scaffold(argv: string[]): Promise<void> {
`${baked}, but this run has no --token-env, so rewriting them would restore the ` +
`default ${DEFAULT_TOKEN_ENV} and CI would run with an empty credential. Re-run with ` +
`--token-env ${baked} to keep the current credential, or --token-env ${DEFAULT_TOKEN_ENV} ` +
`to reset to OpenAI on purpose.`,
`to reset to the default on purpose.`,
);
}
}
@@ -226,14 +227,15 @@ async function scaffold(argv: string[]): Promise<void> {
const steps = [
`Customize ${CONFIG_DIRNAME}/agents/*.md (and shared.md, coordinator.md) for this repo.`,
// --token-env only rewires the workflows; the scaffolded config.jsonc still
// declares OPENAI_API_KEY, and CI's `ecr verify-config` refuses to review
// until the config's tokenEnv set matches the workflow's expected set.
// declares the default Anthropic tokenEnv, and CI's `ecr verify-config`
// refuses to review until the config's tokenEnv set matches the workflow's
// expected set.
...(tokenEnvs.join(",") !== DEFAULT_TOKEN_ENV
? [
`Point ${CONFIG_DIRNAME}/config.jsonc at ${tokenEnvs.length > 1 ? "these credentials" : "this credential"}: set \`auth\` (and \`model\`) per the file's comments — CI's \`ecr verify-config\` refuses to review until the config names ${names}.`,
]
: []),
"Configure a model provider in OpenCode (or set REVIEWER_MODEL).",
"Log in with `claude` (or run `ecr setup-auth`) so local runs have a credential.",
"Run `ecr doctor`, then `ecr review`.",
withWorkflow
? `Add the ${names} repo secret${tokenEnvs.length > 1 ? "s" : ""} referenced by the workflow, then add an \`ai-review\` label to a PR.`
@@ -639,11 +641,14 @@ export function substituteTokenEnv(raw: string, tokenEnvs: string[]): string {
return raw;
}
const expectedFallback = `vars.ECR_EXPECTED_TOKEN_ENV || '${DEFAULT_TOKEN_ENV}'`;
// Byte-identical to the credential block in templates/workflow.yml AND
// templates/command.yml (both flow through this function).
const credentialBlock = [
" # OpenAI API key — the env var named by auth.tokenEnv in config.jsonc.",
" # Store it as a repo secret; a project-scoped key restricted to model",
" # inference (with a spend limit) is all the reviewer needs.",
" OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}",
" # Anthropic review credential — the env var named by auth.tokenEnv in",
" # config.jsonc. Store it as a repo secret: an `sk-ant-oat…` token minted",
" # by `claude setup-token`, or an `sk-ant-api…` Console key (the Claude",
" # Code CLI reads either).",
" CLAUDE_CODE_REVIEW_SHARED_API_TOKEN: ${{ secrets.CLAUDE_CODE_REVIEW_SHARED_API_TOKEN }}",
].join("\n");
if (!raw.includes(expectedFallback) || !raw.includes(credentialBlock)) {
throw new Error("workflow template drifted: tokenEnv markers not found (report this bug)");
+4 -1
View File
@@ -21,7 +21,9 @@ const PROVIDER_KEY_ENV: Record<string, string> = {
* Provider-owned credential env vars BEYOND the x-api-key ones above: Anthropic's
* OAuth/subscription bearer envs. CLAUDE_CODE_OAUTH_TOKEN holds the long-lived (1-year)
* Claude Max/Team subscription token that `ecr setup-auth`/`claude setup-token` export,
* and ANTHROPIC_AUTH_TOKEN is Anthropic's documented bearer var. They belong to
* ANTHROPIC_AUTH_TOKEN is Anthropic's documented bearer var, and
* CLAUDE_CODE_REVIEW_SHARED_API_TOKEN is the SCAFFOLDED DEFAULT tokenEnv (see
* templates/config.jsonc) — it always holds an Anthropic credential. They belong to
* anthropic, so the cross-provider guard below refuses a non-anthropic entry that names
* one — without this, `{provider:"openai", tokenEnv:"CLAUDE_CODE_OAUTH_TOKEN"}` passes
* (neither a FORBIDDEN secret nor a PROVIDER_KEY_ENV value) and prepareAuth forwards the
@@ -31,6 +33,7 @@ const PROVIDER_KEY_ENV: Record<string, string> = {
const ANTHROPIC_TOKEN_ENVS: Record<string, string> = {
CLAUDE_CODE_OAUTH_TOKEN: "anthropic",
ANTHROPIC_AUTH_TOKEN: "anthropic",
CLAUDE_CODE_REVIEW_SHARED_API_TOKEN: "anthropic",
};
/**
+3 -3
View File
@@ -3,11 +3,11 @@
description: Security and secrets. Injection, credential or secret leakage, unsafe shell/child-process use, missing validation at trust boundaries.
alwaysRun: true
# Security is the highest-stakes agent and benefits most from stronger threat-model
# reasoning, so it runs on the pro tier even though the other specialists use the
# reasoning, so it runs on the Opus tier even though the other specialists use the
# default model. Scoped to this one agent to limit the extra latency/rate-limit cost;
# subdivide-on-timeout + the per-fetch deadline keep a slow pro pass from hanging.
# subdivide-on-timeout + the per-fetch deadline keep a slow Opus pass from hanging.
# @ref LLP 0009#config-and-prompt-templates [implements]
model: openai/gpt-5.5-pro
model: anthropic/claude-opus-5
---
# Security & secrets
+11 -5
View File
@@ -81,6 +81,11 @@ jobs:
# disable setup-node's auto package-manager cache.
package-manager-cache: false
# The anthropic/… models run through the Claude Code CLI (claude-code
# engine). Pinned exactly — see workflow.yml (expo-code-review.yml).
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/[email protected]
# SECURITY: the base-ref checkout includes every .expo-code-review/ config,
# whose auth.tokenEnv names the forwarded model credential. `ecr verify-config`
# sweeps every config and refuses unless tokenEnv appears exactly once, in a
@@ -89,7 +94,7 @@ jobs:
# @ref LLP 0009#guard-step-ordering-and-job-budgets [implements] — same $ECR_VERSION feeds guard and review
- name: Guard config tokenEnv (root + routing + all scopes)
env:
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'OPENAI_API_KEY' }}
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'CLAUDE_CODE_REVIEW_SHARED_API_TOKEN' }}
run: npx --yes -p "@expo/code-review-cli@$ECR_VERSION" ecr verify-config
# Running via `issue_comment` makes this a manual /review, which the CLI detects
@@ -101,10 +106,11 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Layer-1 auth lock: keep in sync with the guard's EXPECTED.
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'OPENAI_API_KEY' }}
# OpenAI API key — the env var named by auth.tokenEnv in config.jsonc.
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Optional: override the model for every agent (uses your OpenCode login).
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'CLAUDE_CODE_REVIEW_SHARED_API_TOKEN' }}
# Anthropic review credential — the env var named by auth.tokenEnv in
# config.jsonc (see workflow.yml for the accepted token shapes).
CLAUDE_CODE_REVIEW_SHARED_API_TOKEN: ${{ secrets.CLAUDE_CODE_REVIEW_SHARED_API_TOKEN }}
# Optional: override the model for every agent.
REVIEWER_MODEL: ${{ vars.REVIEWER_MODEL }}
run: |
# Build the flags as a bash array so the path never word-splits.
+14 -7
View File
@@ -111,6 +111,12 @@ jobs:
# to save an empty cache and error).
package-manager-cache: false
# The anthropic/… models run through the Claude Code CLI (claude-code
# engine). Pinned exactly — see workflow.yml (expo-code-review.yml).
- name: Install Claude Code CLI
if: steps.cmd.outputs.run == 'true'
run: npm install -g @anthropic-ai/[email protected]
# SECURITY: the base-ref checkout above includes every .expo-code-review/
# config.jsonc + routing.jsonc, whose auth.tokenEnv names the env var the CLI
# forwards as the model credential. The canonical guard ships with the CLI:
@@ -126,7 +132,7 @@ jobs:
if: steps.cmd.outputs.run == 'true'
env:
# (Comma-separated set for a multi-credential auth.providers config.)
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'OPENAI_API_KEY' }}
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'CLAUDE_CODE_REVIEW_SHARED_API_TOKEN' }}
run: npx --yes -p "@expo/code-review-cli@$ECR_VERSION" ecr verify-config
- name: Run AI review
@@ -136,12 +142,13 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Layer-1 auth lock: the CLI refuses to run when the tokenEnv it would honor
# differs from this. Keep it in sync with the guard's EXPECTED.
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'OPENAI_API_KEY' }}
# OpenAI API key — the env var named by auth.tokenEnv in config.jsonc.
# Store it as a repo secret; a project-scoped key restricted to model
# inference (with a spend limit) is all the reviewer needs.
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Optional: override the model for every agent (uses your OpenCode login).
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'CLAUDE_CODE_REVIEW_SHARED_API_TOKEN' }}
# Anthropic review credential — the env var named by auth.tokenEnv in
# config.jsonc. Store it as a repo secret: an `sk-ant-oat…` token minted
# by `claude setup-token`, or an `sk-ant-api…` Console key (the Claude
# Code CLI reads either).
CLAUDE_CODE_REVIEW_SHARED_API_TOKEN: ${{ secrets.CLAUDE_CODE_REVIEW_SHARED_API_TOKEN }}
# Optional: override the model for every agent.
REVIEWER_MODEL: ${{ vars.REVIEWER_MODEL }}
AGENTS: ${{ steps.cmd.outputs.agents }}
ROUTE: ${{ steps.cmd.outputs.route }}
+31 -39
View File
@@ -1,15 +1,17 @@
// @ref LLP 0009#config-and-prompt-templates — root config: agent roster by filename, phase-1 defaults, auth
{
// Default model for every agent. Override per-agent via frontmatter in the
// agent's markdown, or at runtime with the REVIEWER_MODEL env var
// (e.g. REVIEWER_MODEL=openai/gpt-5.4-mini).
"model": "openai/gpt-5.5",
// Default model for every agent. Anthropic models run through the Claude Code
// CLI engine (`claude -p`), inferred per agent from the model id — see `auth`
// below for the credential. Override per-agent via frontmatter in the agent's
// markdown, or at runtime with the REVIEWER_MODEL env var
// (e.g. REVIEWER_MODEL=anthropic/claude-haiku-4-5).
"model": "anthropic/claude-sonnet-5",
// Agents: every markdown file in agents/ is one reviewer (id = filename).
// Add or remove files to change the roster — no list needed here.
// shared.md (prepended to every agent + coordinator) and coordinator.md are
// reserved filenames. Per-agent overrides go in each file's YAML frontmatter,
// e.g. `---\nmodel: openai/gpt-5.5-pro\n---`.
// e.g. `---\nmodel: anthropic/claude-opus-5\n---`.
// @ref LLP 0009#config-and-prompt-templates [implements] — suggestions off by default, not a schema limit
"policy": {
@@ -46,49 +48,39 @@
// HTML marker used to find + update the single PR comment. Keep it stable.
"commentTag": "expo-ai-code-reviewer",
// How model credentials are provided. Default: an OpenAI API key.
// How model credentials are provided. Default: Anthropic via the Claude Code
// CLI. Anthropic models always run through that CLI — the engine is inferred
// per agent from the `anthropic/…` model id, so this entry only supplies the
// credential. In CI the tokenEnv holds the review credential (an `sk-ant-oat…`
// token minted by `claude setup-token`, or an `sk-ant-api…` Console key — the
// CLI reads either); locally an active `claude` login is enough, and the entry
// could even be omitted. ECR_EXPECTED_TOKEN_ENV (repo variable / workflow
// fallback) must equal this tokenEnv. `ecr setup-auth` walks you through it.
//
// Other providers run through OpenCode:
// "api-key": tokenEnv names the env var holding a provider API key. In CI,
// store the key as a repo secret and pass it under that env var
// (the scaffolded workflow does). If you omit `auth` entirely,
// OpenCode's own login / ambient provider env vars are used.
// For Anthropic/Claude, see the dedicated block below (it now runs through the
// Claude Code CLI). For another provider, omit `auth` and set REVIEWER_MODEL
// after an `opencode auth login` for that provider.
// store the key as a repo secret and pass it under that env var.
// "auth": { "mode": "api-key", "provider": "openai", "tokenEnv": "OPENAI_API_KEY" }
//
// MIXED setup (a ChatGPT/Codex subscription for the default models, plus a
// metered API key for pro-tier models the subscription doesn't offer): use the
// per-provider map instead, reference `openai-api/...` models in the frontmatter
// of the agents that need the pro tier, and set ECR_EXPECTED_TOKEN_ENV in the
// workflow to the comma-separated set of both env names.
// ChatGPT/Codex subscription ("oauth"): tokenEnv holds the ACCESS token from
// an `opencode auth login` ChatGPT sign-in — `ecr setup-auth` extracts it.
// NEVER share the refresh token: it is single-use and dies on first rotation.
// "auth": { "providers": {
// "openai": { "mode": "oauth", "tokenEnv": "CODEX_OAUTH_ACCESS_TOKEN" },
// "openai-api": { "mode": "api-key", "tokenEnv": "OPENAI_API_KEY", "upstream": "openai" }
// } }
// (openai oauth: tokenEnv holds the ACCESS token from an `opencode auth login`
// ChatGPT sign-in — `ecr setup-auth` extracts it. NEVER share the refresh
// token: it is single-use and dies on first rotation.)
//
// Anthropic runs through the Claude Code CLI, inferred from the model — set your
// models to "anthropic/…" (e.g. anthropic/claude-opus-5, anthropic/claude-sonnet-5)
// and run `claude setup-token`, exporting the token as CLAUDE_CODE_OAUTH_TOKEN (CI)
// or rely on your local `claude` login. An auth entry is OPTIONAL (tokenEnv just
// names the credential env); `ecr setup-auth` walks you through it.
// "auth": { "providers": {
// "anthropic": { "tokenEnv": "CLAUDE_CODE_OAUTH_TOKEN" }
// "openai": { "mode": "oauth", "tokenEnv": "CODEX_OAUTH_ACCESS_TOKEN" }
// } }
//
// MIXING engines is supported: the engine is inferred per agent from its model, so
// an anthropic entry may coexist with an openai (or any other) OpenCode provider.
// Each agent's `model` selects its engine — an `anthropic/…` agent runs through the
// Claude Code CLI while an `openai/…` agent runs through OpenCode, in the SAME run.
// MIXING engines is supported: the engine is inferred per agent from its model,
// so this anthropic entry may coexist with an openai (or any other) OpenCode
// provider — an `anthropic/…` agent runs through the Claude Code CLI while an
// `openai/…` agent runs through OpenCode, in the SAME run:
// "auth": { "providers": {
// "anthropic": { "tokenEnv": "CLAUDE_CODE_OAUTH_TOKEN" },
// "anthropic": { "tokenEnv": "CLAUDE_CODE_REVIEW_SHARED_API_TOKEN" },
// "openai": { "mode": "oauth", "tokenEnv": "CODEX_OAUTH_ACCESS_TOKEN" }
// } }
"auth": {
"mode": "api-key",
"provider": "openai",
"tokenEnv": "OPENAI_API_KEY"
"providers": {
"anthropic": { "tokenEnv": "CLAUDE_CODE_REVIEW_SHARED_API_TOKEN" }
}
},
// Stack-aware requalification (ROOT-ONLY; off by default). When on, `ecr ci` walks
+2 -2
View File
@@ -1,11 +1,11 @@
<!-- @ref LLP 0009#config-and-prompt-templates — pro tier pinned on purpose: consolidation quality over serial-tail latency -->
---
# The coordinator makes the final call — de-duping, re-judging severity, and
# deciding — so it runs on the pro tier: consolidation quality matters more here
# deciding — so it runs on the Opus tier: consolidation quality matters more here
# than the small serial-tail latency it adds (no repo tools, one bounded pass).
# Override with a cheaper model if you'd rather trade decision quality for latency.
# @ref LLP 0009#config-and-prompt-templates [implements]
model: openai/gpt-5.5-pro
model: anthropic/claude-opus-5
---
# Coordinator — consolidation & decision
+15 -7
View File
@@ -67,6 +67,13 @@ jobs:
# to save an empty cache and error).
package-manager-cache: false
# The anthropic/… models in .expo-code-review/config.jsonc run through the
# Claude Code CLI (the claude-code engine). Pinned exactly — an unpinned CLI
# would silently drift on every CI run; bump it deliberately. Remove this
# step if you switch every model to an OpenCode-served provider.
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/[email protected]
# SECURITY: the TRUSTED BASE checkout above includes every
# .expo-code-review/config.jsonc + routing.jsonc, whose auth.tokenEnv names the
# env var the CLI forwards as the model credential. The canonical guard ships
@@ -87,7 +94,7 @@ jobs:
- name: Guard config tokenEnv (root + routing + all scopes)
env:
# (Comma-separated set for a multi-credential auth.providers config.)
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'OPENAI_API_KEY' }}
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'CLAUDE_CODE_REVIEW_SHARED_API_TOKEN' }}
run: npx --yes -p "@expo/code-review-cli@$ECR_VERSION" ecr verify-config
- name: Run AI review
@@ -101,12 +108,13 @@ jobs:
# honor (root config.jsonc, or routing.jsonc defaults.auth) differs from
# this — it catches what the guard step above can't. Keep it in sync
# with the guard.
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'OPENAI_API_KEY' }}
# OpenAI API key — the env var named by auth.tokenEnv in config.jsonc.
# Store it as a repo secret; a project-scoped key restricted to model
# inference (with a spend limit) is all the reviewer needs.
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Optional: override the model for every agent (uses your OpenCode login).
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'CLAUDE_CODE_REVIEW_SHARED_API_TOKEN' }}
# Anthropic review credential — the env var named by auth.tokenEnv in
# config.jsonc. Store it as a repo secret: an `sk-ant-oat…` token minted
# by `claude setup-token`, or an `sk-ant-api…` Console key (the Claude
# Code CLI reads either).
CLAUDE_CODE_REVIEW_SHARED_API_TOKEN: ${{ secrets.CLAUDE_CODE_REVIEW_SHARED_API_TOKEN }}
# Optional: override the model for every agent.
REVIEWER_MODEL: ${{ vars.REVIEWER_MODEL }}
# Observability: the per-run log (token/cache/cost totals + per-pass timing +