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
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1004,13 +1004,20 @@ branch before it is pushed for review: Codex when run from Claude Code, and
Claude Code when run from Codex. `--harness` names the reviewer instead; an
unknown harness, or a missing or logged-out CLI, is an error rather than a
same-model review, as is a login the API rejects mid-round, such as an expired
or revoked token. The reviewer uses its CLI's own stored login; a CLI with
none keeps the credential it reads from the environment (`ANTHROPIC_API_KEY`,
`ANTHROPIC_AUTH_TOKEN`, or `CLAUDE_CODE_OAUTH_TOKEN` for Claude Code,
`CODEX_API_KEY` for Codex), unless a stripped `ANTHROPIC_BASE_URL` or
`CLAUDE_CODE_USE_BEDROCK`, `_VERTEX`, or `_FOUNDRY` would send it elsewhere.
or revoked token. The reviewer reaches its model as the user's own CLI does:
Claude Code keeps `ANTHROPIC_BASE_URL`, `ANTHROPIC_CUSTOM_HEADERS`, and its
cloud providers' `CLAUDE_CODE_USE_*` switches and settings, so a gateway such
as CLIProxyAPI, Bedrock, Vertex, or Foundry works, and Codex keeps
`CODEX_CA_CERTIFICATE`. It uses the CLI's own stored login; the credential the
CLI reads from the environment (`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, or
`CLAUDE_CODE_OAUTH_TOKEN` for Claude Code, `CODEX_API_KEY` or
`CODEX_ACCESS_TOKEN` for Codex) passes only when the CLI has no stored login or
a gateway or provider is set. A provider's own keys (`ANTHROPIC_AWS_API_KEY`,
`ANTHROPIC_FOUNDRY_API_KEY`, `ANTHROPIC_FOUNDRY_AUTH_TOKEN`) pass only while a
gateway or provider is set. Variables that would change the reviewer's plugins,
permissions, model, or configuration stay stripped.
A logged-out error names the profile the CLI checked, `CLAUDE_CONFIG_DIR` or
`CODEX_HOME`, or the default when unset.
`CODEX_HOME`, or the default when unset, and any gateway or provider.
Every review runs with a pinned model and effort that `quest review --help`
lists; `--model` and `--effort` override them.

Expand Down
1 change: 0 additions & 1 deletion quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,4 @@ ownership-manifest design:
- [Agent workflow helpers](/quest/m0/helpers/README.md) - CLI commands for CI waits, PR status, claims, quest moves, and serialized merges
- [Worktree hooks here](/quest/m0/worktree-hooks.md) - `quest` reaches Claude Code's hooks and the merge driver in this repository, then `quest init` installs the hooks
- [Remote control worktrees](/quest/m0/worktree-routes.md) - live-check `claude remote-control --spawn worktree` against the hooks, from a session spawned on claude.ai
- [Review through a gateway](/quest/m0/review-gateway.md) - `quest review` keeps gateway, cloud-provider, and Codex connection variables
- [opencode 2.x](/quest/m0/opencode-sdk.md) - the runner drives the opencode 2.x binary through `@opencode/client`, removing the interim 1.19.0 pin
33 changes: 0 additions & 33 deletions quest/m0/review-gateway.md

This file was deleted.

13 changes: 8 additions & 5 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -944,11 +944,14 @@ export function main(argv: string[], out: Output): number | Promise<number> {
.description(
"Review the checkout's committed branch against its base with the other harness: Codex when run from " +
"Claude Code, Claude Code when run from Codex, detected from the variables each sets. --harness names " +
"the reviewer instead; there is no other fallback. The reviewer uses its CLI's own stored login or, " +
"without one, the credential it reads from the environment (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, or " +
"CLAUDE_CODE_OAUTH_TOKEN for Claude Code, CODEX_API_KEY for Codex) unless a stripped ANTHROPIC_BASE_URL or " +
"CLAUDE_CODE_USE_BEDROCK, _VERTEX, or _FOUNDRY would send it elsewhere. A logged-out error names the " +
"profile it checked, CLAUDE_CONFIG_DIR or CODEX_HOME, or the default when unset. It runs " +
"the reviewer instead; there is no other fallback. The reviewer reaches its model as the user's own CLI " +
"does: Claude Code keeps ANTHROPIC_BASE_URL, ANTHROPIC_CUSTOM_HEADERS, and its cloud providers' " +
"CLAUDE_CODE_USE_* switches and settings, and Codex keeps CODEX_CA_CERTIFICATE. It uses the CLI's own " +
"stored login; the credential the CLI reads from the environment (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, " +
"or CLAUDE_CODE_OAUTH_TOKEN for Claude Code, CODEX_API_KEY or CODEX_ACCESS_TOKEN for Codex) passes only " +
"when the CLI has no stored login or a gateway or provider is set, and a provider's own keys " +
"(ANTHROPIC_AWS_API_KEY, ANTHROPIC_FOUNDRY_API_KEY, ANTHROPIC_FOUNDRY_AUTH_TOKEN) only while one is set. A logged-out error names the profile it checked, " +
"CLAUDE_CONFIG_DIR or CODEX_HOME, or the default when unset, and any gateway or provider. It runs " +
`with ${models}. --model and --effort override them.\n\n` +
"Every turn reviews only the branch's own change, from the merge base of HEAD with the base's tip, so " +
"a branch behind its base draws no findings on the base's newer code. The first round runs the " +
Expand Down
114 changes: 80 additions & 34 deletions src/cli/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,24 @@ interface Reviewer {
login: string;
/** The variable naming the configuration directory, and so the stored login, the status check reads. */
profile: string;
/** Credentials the CLI reads from the environment, passed only when it has no stored login. */
/**
* Credentials the CLI reads from the environment, passed when it has no
* stored login or a route is set, since a route's credential is meant for it.
*/
credentials: string[];
/**
* The variables in `env` that send the CLI's requests, and their credentials,
* to another endpoint or provider. They stay stripped, and while one is set
* no credential passes either, since it is meant for that endpoint.
* Credentials only a gateway or provider reads, passed only while a route is
* set, so an unused provider key never reaches the reviewer.
*/
routeCredentials: string[];
/**
* Variables that set how the CLI reaches its model: a gateway, a cloud
* provider and its settings, headers, or a CA certificate. They always pass,
* so the review goes where the user's own CLI sessions do.
*/
reroutes(env: NodeJS.ProcessEnv): string[];
connection: string[];
/** The variables in `env` that send the CLI's requests to a gateway or another provider. */
routes(env: NodeJS.ProcessEnv): string[];
/** Matches the CLI's error for a login or credential the API rejected: expired, revoked, or invalid. */
unauthorized: RegExp;
review(context: Context, session: string): Turn;
Expand All @@ -217,6 +227,9 @@ const record = (value: unknown) =>
const count = (value: unknown) => (typeof value === "number" ? value : 0);
const text = (value: unknown) => (typeof value === "string" ? value : undefined);

/** The cloud providers Claude Code reaches its model through, each with a `CLAUDE_CODE_USE_` switch. */
const CLAUDE_PROVIDERS = ["BEDROCK", "VERTEX", "FOUNDRY", "MANTLE", "ANTHROPIC_AWS", "ANTHROPIC_GOOGLE_CLOUD"];

/**
* Claude Code reads no settings files: the checkout's would let a change
* instruct or hook its reviewer, and an allow rule in any of them, such as the
Expand All @@ -230,10 +243,34 @@ const claude: Reviewer = {
login: "claude auth login",
profile: "CLAUDE_CONFIG_DIR",
credentials: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"],
// Claude Code turns a provider on only for `1`, `true`, `yes`, or `on`, so `0` or `false` reroutes nothing.
reroutes: (env) => [
// Claude Code lists these with its credentials, and reads them only while a provider's switch is on.
routeCredentials: ["ANTHROPIC_AWS_API_KEY", "ANTHROPIC_FOUNDRY_API_KEY", "ANTHROPIC_FOUNDRY_AUTH_TOKEN"],
// A provider's own settings take effect only while its switch is on. Model choices,
// such as ANTHROPIC_MODEL, stay stripped: the review pins its own.
connection: [
"ANTHROPIC_BASE_URL",
"ANTHROPIC_CUSTOM_HEADERS",
...CLAUDE_PROVIDERS.flatMap((provider) => [`CLAUDE_CODE_USE_${provider}`, `CLAUDE_CODE_SKIP_${provider}_AUTH`]),
"ANTHROPIC_AWS_BASE_URL",
"ANTHROPIC_AWS_WORKSPACE_ID",
"ANTHROPIC_BEDROCK_BASE_URL",
"ANTHROPIC_BEDROCK_MANTLE_BASE_URL",
"ANTHROPIC_BEDROCK_REGION_PREFIX",
"ANTHROPIC_BEDROCK_SERVICE_TIER",
"ANTHROPIC_FOUNDRY_BASE_URL",
"ANTHROPIC_FOUNDRY_RESOURCE",
"ANTHROPIC_GOOGLE_CLOUD_BASE_URL",
"ANTHROPIC_GOOGLE_CLOUD_LOCATION",
"ANTHROPIC_GOOGLE_CLOUD_PROJECT",
"ANTHROPIC_GOOGLE_CLOUD_WORKSPACE_ID",
"ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION",
"ANTHROPIC_VERTEX_BASE_URL",
"ANTHROPIC_VERTEX_PROJECT_ID",
],
// Claude Code turns a provider on only for `1`, `true`, `yes`, or `on`, so `0` or `false` routes nothing.
routes: (env) => [
...(env.ANTHROPIC_BASE_URL ? ["ANTHROPIC_BASE_URL"] : []),
...["CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY"].filter((name) =>
...CLAUDE_PROVIDERS.map((provider) => `CLAUDE_CODE_USE_${provider}`).filter((name) =>
/^\s*(1|true|yes|on)\s*$/i.test(env[name] ?? ""),
),
],
Expand Down Expand Up @@ -327,9 +364,12 @@ const codex: Reviewer = {
login: "codex login",
profile: "CODEX_HOME",
// `codex exec` reads CODEX_API_KEY, though `codex login status` does not count it; it ignores OPENAI_API_KEY.
credentials: ["CODEX_API_KEY"],
// OPENAI_BASE_URL is never stripped, so a passed key goes where the CLI already sends its requests.
reroutes: () => [],
// CODEX_ACCESS_TOKEN is an agent identity token, which both read.
credentials: ["CODEX_API_KEY", "CODEX_ACCESS_TOKEN"],
routeCredentials: [],
// A gateway that intercepts TLS needs its CA. OPENAI_BASE_URL is never stripped.
connection: ["CODEX_CA_CERTIFICATE"],
routes: () => [],
// Such as "unexpected status 401 Unauthorized: ..." or "Your access token could not be refreshed
// because your refresh token was revoked. Please log out and sign in again."
unauthorized: /\b401 Unauthorized\b|sign in again/,
Expand Down Expand Up @@ -499,37 +539,43 @@ function profile(reviewer: Reviewer, env: NodeJS.ProcessEnv): string {
return dir ? `the profile ${reviewer.profile}=${dir}` : `its default profile (${reviewer.profile} is unset)`;
}

/** The variables among `names` that `env` sets, with their values. */
const pick = (env: NodeJS.ProcessEnv, names: string[]) =>
Object.fromEntries(names.filter((name) => env[name] !== undefined).map((name) => [name, env[name]]));

/**
* Prepare the reviewer: the user's environment minus `STRIPPED`, so the CLI
* uses its stored login, as any launched agent does. A CLI with no stored
* login keeps the credentials it would read from the environment instead,
* unless a stripped variable would have sent them elsewhere. A shell's API key
* never overrides a stored subscription login, so a review is billed as the
* user's own CLI sessions are.
* uses its stored login, as any launched agent does, plus the variables that
* connect it to its model, so it reaches it the way the user's own CLI does.
* A CLI with no stored login, or one sent to a gateway or another provider,
* also keeps the credentials it would read from the environment. Otherwise a
* shell's API key never overrides a stored subscription login, so a review is
* billed as the user's own CLI sessions are.
*/
async function prepare(harness: HarnessName, cwd: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<Cli> {
const reviewer = REVIEWERS[harness];
const stripped = strippedEnvironment(env);
const notLoggedIn = `${harness} is not logged in to ${profile(reviewer, env)}`;
const loggedOut = `${notLoggedIn}; run \`${reviewer.login}\``;
if ((await run(reviewer.status, cwd, stripped, "", undefined, signal)).code === 0) {
return { reviewer, env: stripped, unauthorized: loggedOut, signal };
const connected = { ...strippedEnvironment(env), ...pick(env, reviewer.connection) };
const routes = reviewer.routes(env);
const through = routes.length > 0 ? ` through ${names(routes)}` : "";
const loggedOut = `${harness} is not logged in to ${profile(reviewer, env)}${through}; run \`${reviewer.login}\``;
const credentials = [...reviewer.credentials, ...(routes.length > 0 ? reviewer.routeCredentials : [])].filter(
(name) => env[name],
);
const withCredentials = { ...connected, ...pick(env, credentials) };
const rejected = `${harness} rejected ${names(credentials)}${through}; replace it, or run \`${reviewer.login}\``;
if (routes.length > 0) {
if ((await run(reviewer.status, cwd, withCredentials, "", undefined, signal)).code !== 0) {
throw new ReviewInputError(loggedOut);
}
return { reviewer, env: withCredentials, unauthorized: credentials.length > 0 ? rejected : loggedOut, signal };
}
const reroutes = reviewer.reroutes(env);
if (reroutes.length > 0) {
throw new ReviewInputError(
`${notLoggedIn}, and quest review strips ${names(reroutes)}, which would reroute it; ` +
`run \`${reviewer.login}\``,
);
if ((await run(reviewer.status, cwd, connected, "", undefined, signal)).code === 0) {
const ignored =
credentials.length > 0 ? `; quest review strips ${names(credentials)} while a stored login exists` : "";
return { reviewer, env: connected, unauthorized: `${loggedOut}${ignored}`, signal };
}
const credentials = reviewer.credentials.filter((name) => env[name]);
if (credentials.length === 0) throw new ReviewInputError(loggedOut);
return {
reviewer,
env: { ...stripped, ...Object.fromEntries(credentials.map((name) => [name, env[name]])) },
unauthorized: `${harness} rejected ${names(credentials)}; replace it, or run \`${reviewer.login}\``,
signal,
};
return { reviewer, env: withCredentials, unauthorized: rejected, signal };
}

/** The environment `quest review` runs `harness`'s CLI in, for a caller that runs it with the same login. */
Expand Down
Loading
Loading