@expo/code-review-cli 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -22
- package/build/commands/ci.js +156 -39
- package/build/commands/doctor.js +167 -33
- package/build/commands/review.js +5 -3
- package/build/commands/setup-auth.js +83 -11
- package/build/config/load.js +15 -0
- package/build/config/schema.js +7 -3
- package/build/core/auth.js +122 -9
- package/build/core/claude-code.js +680 -0
- package/build/core/exec.js +278 -9
- package/build/core/opencode.js +95 -15
- package/build/core/prompts.js +19 -2
- package/build/core/render.js +21 -2
- package/build/core/review.js +212 -37
- package/build/core/schema.js +6 -1
- package/build/core/scrub.js +120 -0
- package/build/core/throttle.js +10 -0
- package/build/core/util.js +17 -0
- package/build/core/verify.js +13 -1
- package/build/reporters/github.js +79 -13
- package/build/sources/github-pr.js +110 -22
- package/build/sources/local-git.js +3 -2
- package/package.json +3 -3
- package/templates/command.yml +7 -1
- package/templates/config.jsonc +21 -3
- package/templates/dismiss.yml +3 -0
- package/templates/shared.md +28 -0
- package/templates/workflow.yml +17 -5
package/build/core/auth.js
CHANGED
|
@@ -8,6 +8,21 @@ const PROVIDER_KEY_ENV = {
|
|
|
8
8
|
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
9
9
|
openrouter: "OPENROUTER_API_KEY",
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Provider-owned credential env vars BEYOND the x-api-key ones above: Anthropic's
|
|
13
|
+
* OAuth/subscription bearer envs. CLAUDE_CODE_OAUTH_TOKEN holds the long-lived (1-year)
|
|
14
|
+
* Claude Max/Team subscription token that `ecr setup-auth`/`claude setup-token` export,
|
|
15
|
+
* and ANTHROPIC_AUTH_TOKEN is Anthropic's documented bearer var. They belong to
|
|
16
|
+
* anthropic, so the cross-provider guard below refuses a non-anthropic entry that names
|
|
17
|
+
* one — without this, `{provider:"openai", tokenEnv:"CLAUDE_CODE_OAUTH_TOKEN"}` passes
|
|
18
|
+
* (neither a FORBIDDEN secret nor a PROVIDER_KEY_ENV value) and prepareAuth forwards the
|
|
19
|
+
* Anthropic subscription token to a foreign provider as its bearer. They are NOT in
|
|
20
|
+
* FORBIDDEN_TOKEN_ENVS because an anthropic entry may legitimately name them.
|
|
21
|
+
*/
|
|
22
|
+
const ANTHROPIC_TOKEN_ENVS = {
|
|
23
|
+
CLAUDE_CODE_OAUTH_TOKEN: "anthropic",
|
|
24
|
+
ANTHROPIC_AUTH_TOKEN: "anthropic",
|
|
25
|
+
};
|
|
11
26
|
/**
|
|
12
27
|
* Env vars that must NEVER be forwarded to a model provider. `auth.tokenEnv` names
|
|
13
28
|
* the env var whose value becomes the provider credential — but that config is
|
|
@@ -159,17 +174,58 @@ export function checkAuthEntry(entry, env = process.env) {
|
|
|
159
174
|
// point its provider/upstream somewhere else, sending one provider's key to a
|
|
160
175
|
// different provider.
|
|
161
176
|
if (tokenEnv) {
|
|
162
|
-
const keyOwner = Object.entries(PROVIDER_KEY_ENV).find(([, env]) => env === tokenEnv)?.[0]
|
|
177
|
+
const keyOwner = Object.entries(PROVIDER_KEY_ENV).find(([, env]) => env === tokenEnv)?.[0] ??
|
|
178
|
+
ANTHROPIC_TOKEN_ENVS[tokenEnv];
|
|
163
179
|
if (keyOwner && keyOwner !== provider && keyOwner !== upstream) {
|
|
164
180
|
return {
|
|
165
181
|
ok: false,
|
|
166
182
|
detail: `auth for ${provider} names tokenEnv "${tokenEnv}", which is ${keyOwner}'s ` +
|
|
167
|
-
`well-known
|
|
183
|
+
`well-known credential env — refusing to send one provider's credential to another. ` +
|
|
168
184
|
`Use a credential minted for ${provider}${upstream ? ` (upstream ${upstream})` : ""}, ` +
|
|
169
185
|
`or fix the provider/upstream mapping.`,
|
|
170
186
|
};
|
|
171
187
|
}
|
|
172
188
|
}
|
|
189
|
+
// anthropic is ALWAYS served by the Claude Code CLI (engine inferred from the
|
|
190
|
+
// `anthropic/…` model, not from `mode`), so `mode` is irrelevant here. The
|
|
191
|
+
// credential is the machine's `claude` login, an ambient CLAUDE_CODE_OAUTH_TOKEN,
|
|
192
|
+
// or a named tokenEnv. The CLI validates the token, so the only token-shape check
|
|
193
|
+
// here is a coarse exfil guard on a NAMED tokenEnv's value (below); the
|
|
194
|
+
// FORBIDDEN/cross-provider guards above already block the well-known secrets.
|
|
195
|
+
if (provider === "anthropic") {
|
|
196
|
+
// A named-but-unset tokenEnv is NOT fatal: startClaudeCode falls back to the
|
|
197
|
+
// machine's `claude` login (the common local case — the config names the CI
|
|
198
|
+
// secret). startClaudeCode still fails fast when neither credential exists.
|
|
199
|
+
if (tokenEnv && !env[tokenEnv]) {
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
detail: `anthropic via the Claude Code CLI; falling back to the local \`claude\` login`,
|
|
203
|
+
warning: `token env "${tokenEnv}" is not set — using the machine's \`claude\` login ` +
|
|
204
|
+
`(set it for CI/headless runs; mint with \`claude setup-token\`).`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// tokenEnv is set AND present: its value is forwarded to api.anthropic.com as the
|
|
208
|
+
// bearer. The destination is ALWAYS Anthropic, so a value that is not an Anthropic
|
|
209
|
+
// credential ("sk-ant-…" covers both the "sk-ant-oat" OAuth token and "sk-ant-api"
|
|
210
|
+
// keys) cannot authenticate there but CAN be a foreign CI secret the config named
|
|
211
|
+
// — refuse it. This meets the "cannot be valid" bar the shape heuristics use, and
|
|
212
|
+
// fires at both prepareAuth and startClaudeCode's forwarding-site recheck.
|
|
213
|
+
if (tokenEnv && env[tokenEnv] && !env[tokenEnv].startsWith("sk-ant-")) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
detail: `token env "${tokenEnv}" does not hold an Anthropic credential (expected "sk-ant-…", ` +
|
|
217
|
+
`the shape \`claude setup-token\` prints, or an Anthropic API key). anthropic is served ` +
|
|
218
|
+
`by the Claude Code CLI, which authenticates to Anthropic, so a value of another shape ` +
|
|
219
|
+
`cannot work there and would leak that secret to Anthropic — point auth.tokenEnv at a ` +
|
|
220
|
+
`token minted for Anthropic.`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
ok: true,
|
|
225
|
+
detail: `anthropic via the Claude Code CLI; ` +
|
|
226
|
+
(tokenEnv ? `token env ${tokenEnv} is set` : "using the local `claude` login"),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
173
229
|
if (mode === "oauth") {
|
|
174
230
|
if (!tokenEnv) {
|
|
175
231
|
return {
|
|
@@ -233,13 +289,49 @@ export function checkAuthEntry(entry, env = process.env) {
|
|
|
233
289
|
};
|
|
234
290
|
}
|
|
235
291
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
292
|
+
* The set of providers this config actually routes a model to: the `provider/`
|
|
293
|
+
* prefix of every agent model plus the coordinator's. Returns null when no model
|
|
294
|
+
* information is available (e.g. a bare config in a unit test) — callers read that
|
|
295
|
+
* as "can't scope, consider every entry". Every real config loaded by
|
|
296
|
+
* loadReviewConfig has agents, so scoping always applies at runtime.
|
|
297
|
+
*
|
|
298
|
+
* The fixed cross-cutting/verifier roles reuse an agent's model, so their provider
|
|
299
|
+
* is already covered by the agent set — mirrors engineForModel's `provider/` split.
|
|
300
|
+
*/
|
|
301
|
+
function providersInUse(config) {
|
|
302
|
+
const models = [];
|
|
303
|
+
for (const agent of config.agents ?? []) {
|
|
304
|
+
if (agent.model) {
|
|
305
|
+
models.push(agent.model);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (config.coordinator?.model) {
|
|
309
|
+
models.push(config.coordinator.model);
|
|
310
|
+
}
|
|
311
|
+
if (models.length === 0) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const providers = new Set();
|
|
315
|
+
for (const model of models) {
|
|
316
|
+
const slash = model.indexOf("/");
|
|
317
|
+
providers.add(slash > 0 ? model.slice(0, slash) : model);
|
|
318
|
+
}
|
|
319
|
+
return providers;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Decide whether EVERY configured provider credential a model actually uses is
|
|
323
|
+
* usable, WITHOUT mutating the environment. Shared by `prepareAuth` (fail fast
|
|
324
|
+
* before spinning up the server and every pass) and `doctor` (report), so the two
|
|
325
|
+
* never drift.
|
|
239
326
|
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
327
|
+
* Scoped to providers in use: an `auth` entry for a provider no agent routes to is
|
|
328
|
+
* dead config (the shipped default is `api-key`/openai, so a config that switches
|
|
329
|
+
* every model to `anthropic/…` but leaves — or omits — the `auth` block would
|
|
330
|
+
* otherwise be spuriously blocked demanding OPENAI_API_KEY, defeating the
|
|
331
|
+
* `claude`-login fallback). Only used entries gate the run; all of them must pass —
|
|
332
|
+
* a mixed setup with one broken credential would fail exactly the passes routed to
|
|
333
|
+
* it, which is the silent-degradation this check exists to prevent. `REVIEWER_MODEL`
|
|
334
|
+
* bypasses provider auth entirely.
|
|
243
335
|
*/
|
|
244
336
|
export function checkProviderAuth(config, env = process.env) {
|
|
245
337
|
if (env.REVIEWER_MODEL) {
|
|
@@ -248,9 +340,13 @@ export function checkProviderAuth(config, env = process.env) {
|
|
|
248
340
|
detail: `REVIEWER_MODEL override (${env.REVIEWER_MODEL}); using OpenCode's own login for that model`,
|
|
249
341
|
};
|
|
250
342
|
}
|
|
343
|
+
const inUse = providersInUse(config);
|
|
251
344
|
const details = [];
|
|
252
345
|
const warnings = [];
|
|
253
346
|
for (const entry of config.auth) {
|
|
347
|
+
if (inUse && !inUse.has(entry.provider)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
254
350
|
const readiness = checkAuthEntry(entry, env);
|
|
255
351
|
if (!readiness.ok) {
|
|
256
352
|
return readiness;
|
|
@@ -262,7 +358,10 @@ export function checkProviderAuth(config, env = process.env) {
|
|
|
262
358
|
}
|
|
263
359
|
return {
|
|
264
360
|
ok: true,
|
|
265
|
-
|
|
361
|
+
// No used entry (e.g. every model is anthropic and the only `auth` block is the
|
|
362
|
+
// shipped openai default): the run relies on the Claude Code CLI's own login.
|
|
363
|
+
detail: details.join("; ") ||
|
|
364
|
+
"no provider credential needed for the models in use (relying on the engine's own login)",
|
|
266
365
|
...(warnings.length > 0 ? { warning: warnings.join("; ") } : {}),
|
|
267
366
|
};
|
|
268
367
|
}
|
|
@@ -338,10 +437,24 @@ export async function prepareAuth(config) {
|
|
|
338
437
|
if (!readiness.ok) {
|
|
339
438
|
throw new Error(readiness.detail);
|
|
340
439
|
}
|
|
440
|
+
// Same scoping as checkProviderAuth: only forward credentials for providers a
|
|
441
|
+
// model actually routes to. A dead entry for an unused provider must not have its
|
|
442
|
+
// tokenEnv copied into a key env — checkProviderAuth skipped its guard, so
|
|
443
|
+
// forwarding it here would reintroduce the exact secret-forwarding it prevents.
|
|
444
|
+
const inUse = providersInUse(config);
|
|
341
445
|
const authJson = {};
|
|
342
446
|
for (const entry of config.auth) {
|
|
343
447
|
const { mode, provider, tokenEnv, upstream } = entry;
|
|
448
|
+
if (inUse && !inUse.has(provider)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
344
451
|
const value = tokenEnv ? process.env[tokenEnv] : undefined;
|
|
452
|
+
// anthropic is claude-engine-only: it never writes an OpenCode auth.json /
|
|
453
|
+
// XDG_DATA_HOME nor injects ANTHROPIC_API_KEY here — its credential is passed
|
|
454
|
+
// per-invocation via the child env built in startClaudeCode.
|
|
455
|
+
if (provider === "anthropic") {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
345
458
|
if (mode === "api-key") {
|
|
346
459
|
// Upstream aliases read {env:tokenEnv} from the synthesized provider block.
|
|
347
460
|
if (!upstream && tokenEnv && value) {
|