mirror of
https://github.com/expo/code-review-cli.git
synced 2026-11-15 03:38:09 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75fccfe3f4 | ||
|
|
621c354568 | ||
|
|
3af45ebf84 | ||
|
|
65ffc81014 |
No files matched your search
@@ -3,7 +3,7 @@ description: Security and secrets. Injection, credential or secret leakage, unsa
|
||||
alwaysRun: true
|
||||
# Security is the highest-stakes agent, so it runs on the Opus tier (included in
|
||||
# the Max/Team subscription the claude-code engine authenticates with).
|
||||
model: anthropic/claude-opus-5
|
||||
model: anthropic/claude-opus-5-5
|
||||
---
|
||||
|
||||
# Security & secrets
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
// 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=anthropic/claude-haiku-4-5).
|
||||
"model": "anthropic/claude-sonnet-5",
|
||||
"model": "anthropic/claude-opus-5-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: anthropic/claude-opus-5\n---`.
|
||||
// e.g. `---\nmodel: anthropic/claude-opus-5-5\n---`.
|
||||
|
||||
"policy": {
|
||||
// Phase 1: keep signal high by surfacing only critical/warning.
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
# like `opencode-ai` in package.json — an unpinned CLI would silently drift
|
||||
# on every CI run; bump it deliberately.
|
||||
- name: Install Claude Code CLI
|
||||
run: bun install -g @anthropic-ai/[email protected]12
|
||||
run: bun install -g @anthropic-ai/[email protected]80
|
||||
|
||||
# SECURITY: the TRUSTED BASE checkout above includes .expo-code-review/
|
||||
# config.jsonc, whose auth.tokenEnv names the env var the CLI forwards as the
|
||||
|
||||
@@ -328,7 +328,7 @@ your-monorepo/
|
||||
// apps/api/.expo-agents/code-review/config.jsonc (the api team owns this)
|
||||
{
|
||||
// NO "auth" block — locked centrally; a tokenEnv here is rejected by loader + CI guard.
|
||||
"model": "anthropic/claude-sonnet-5",
|
||||
"model": "anthropic/claude-opus-5-5",
|
||||
"policy": { "includeSuggestions": false },
|
||||
"noise": { "additionalIgnores": ["apps/api/**/__generated__/**"] }
|
||||
// shared.md, coordinator.md, agents/*.md live beside this file — the api team's roster.
|
||||
@@ -475,7 +475,7 @@ cacheable size (~1–4K tokens) show `cache read 0` — expected, not a bug.
|
||||
---
|
||||
description: One line the router uses to decide relevance.
|
||||
alwaysRun: true # run even when the router would skip this agent
|
||||
model: anthropic/claude-opus-5 # override the default model
|
||||
model: anthropic/claude-haiku-4-5 # override the default model
|
||||
temperature: 0.1
|
||||
---
|
||||
|
||||
@@ -491,7 +491,7 @@ per-repo `noise.additionalIgnores`.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-5", // default model for the specialists
|
||||
"model": "anthropic/claude-opus-5-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/**"] },
|
||||
@@ -514,15 +514,17 @@ Precedence: **`REVIEWER_MODEL` env** (global override) → per-file **frontmatte
|
||||
`model:`** → **`config.jsonc` `model`** (the default). So a repo can run a mixed
|
||||
setup, and a developer can override everything locally.
|
||||
|
||||
- **Specialist agents** (correctness/security/consistency) benefit from a
|
||||
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.
|
||||
- **Specialist agents** (correctness/security/consistency) run on
|
||||
**`anthropic/claude-opus-5-5`**, the scaffolded default. It finds more than the
|
||||
Sonnet tier but is slower and costs about twice as much per token, so
|
||||
`anthropic/claude-sonnet-5` is the step down when a repo needs faster or cheaper
|
||||
reviews. **Security stays pinned to `anthropic/claude-opus-5-5`** in `security.md`
|
||||
frontmatter, so a lower default does not move the highest-stakes agent.
|
||||
- **The coordinator** makes the final call (dedupe / re-judge / decide) — worth a
|
||||
strong model; the scaffold pins it to `anthropic/claude-opus-5` in
|
||||
strong model; the scaffold pins it to `anthropic/claude-opus-5-5` in
|
||||
`coordinator.md` frontmatter.
|
||||
- **Opus 5.5 needs Claude Code 2.1.280 or newer.** Older CLIs reject it with a 400,
|
||||
so keep the workflow's `@anthropic-ai/claude-code` pin at 2.1.280 or later.
|
||||
- If latency/timeouts dominate on big PRs, moving the specialists to a faster model
|
||||
(e.g. `anthropic/claude-haiku-4-5`) is the most direct lever (a real recall
|
||||
tradeoff — measure it).
|
||||
@@ -763,20 +765,25 @@ fallback key; `ecr doctor` diagnoses setup.
|
||||
} }
|
||||
```
|
||||
|
||||
- **Meta / Muse Spark 1.2 (public Model API).** Set the default model and any
|
||||
model-pinned agent frontmatter to `meta/muse-spark-1.2`. ECR supplies the fixed
|
||||
- **Meta / Muse Spark 1.3 (public Model API).** Set the default model and any
|
||||
model-pinned agent frontmatter to `meta/muse-spark-1.3`. ECR supplies the fixed
|
||||
public Responses endpoint and the `@ai-sdk/openai` adapter; the generic
|
||||
Chat-Completions compatibility adapter is intentionally not used because it
|
||||
loses Muse's reasoning continuity across tool turns. Standard and Contributor
|
||||
model ids are supported.
|
||||
|
||||
```jsonc
|
||||
"model": "meta/muse-spark-1.2",
|
||||
"model": "meta/muse-spark-1.3",
|
||||
"auth": { "providers": {
|
||||
"meta": { "mode": "api-key", "tokenEnv": "META_API_KEY" }
|
||||
} }
|
||||
```
|
||||
|
||||
Standard and Contributor 1.2/1.3 model ids have known capability metadata. Any
|
||||
future model explicitly named as `meta/<model>` is still routed to the fixed Meta
|
||||
endpoint with neutral metadata, so a renamed family such as `meta/muse-foo` does
|
||||
not require an ECR release; Meta validates an unknown id on its first request.
|
||||
|
||||
Create the key in the [Meta AI developer portal](https://developer.meta.com/ai/).
|
||||
Locally, export it as `META_API_KEY`. In each review-running workflow, replace
|
||||
the Anthropic credential line with
|
||||
|
||||
@@ -109,12 +109,15 @@ the Responses API and preserve encrypted reasoning across tool turns. The generi
|
||||
Completions path loses that continuity [observed:
|
||||
`opencode.ts` `META_MODEL_API_BASE_URL` / `META_MUSE_MODELS` / `buildOpencodeConfig`].
|
||||
|
||||
The declared model catalog is an allowlist (`muse-spark-1.2` and
|
||||
`muse-spark-1.2-contributor`), not a reflection of arbitrary config strings. This keeps
|
||||
the existing preflight honest: a misspelled or not-yet-supported Meta model remains
|
||||
absent from the server's provider model list and fails once before any pass. Both models
|
||||
declare their public context/output limits, multimodal inputs, high reasoning, and
|
||||
encrypted reasoning inclusion [observed: `opencode.ts` `META_MUSE_MODELS`].
|
||||
Provider selection is forward-compatible at the provider boundary, not coupled to the
|
||||
current Muse family name: every referenced non-empty `meta/<model>` id is declared on
|
||||
the synthesized Meta provider. Known Muse Spark 1.2 and 1.3 standard/Contributor ids
|
||||
receive their published limits, multimodal inputs, high reasoning, and encrypted
|
||||
reasoning inclusion from `META_MUSE_MODELS`; a future id such as `meta/muse-foo` is
|
||||
declared with neutral metadata rather than guessed capabilities. This deliberately
|
||||
moves validation of an unknown-but-well-formed Meta id from OpenCode's local preflight
|
||||
to its first Meta request, in exchange for new Meta names working without an ECR
|
||||
release [observed: `opencode.ts` `META_MUSE_MODELS` / `buildOpencodeConfig`].
|
||||
|
||||
## Claude Code CLI Containment
|
||||
|
||||
@@ -171,12 +174,22 @@ reads are denied by the unmatched-rule denial, and a withheld tool is denied by
|
||||
([observed] `claude-code.ts:241-321`; behavior verified against `claude 2.1.212`).
|
||||
Write/exec/network tools (`Bash`, `Edit`, `Write`, `NotebookEdit`, `WebFetch`,
|
||||
`WebSearch`, `Task`, …) are always denied for every pass regardless of role, via the
|
||||
`ALWAYS_DENIED_TOOLS` enumeration. A deny **enumeration** is used rather than an
|
||||
allow-only model because permission rules cannot fail closed for tools outside the read
|
||||
set and a `*` deny breaks tool calling outright — both verified against `claude 2.1.212`.
|
||||
The residual (a future CLI version shipping a new read-capable tool this list does not
|
||||
name) is bounded by pinning the CLI version, so **the list must be revisited on every
|
||||
pinned-version bump** ([observed] `claude-code.ts:86-111`).
|
||||
`ALWAYS_DENIED_TOOLS` enumeration. Permission rules alone cannot fail closed for tools
|
||||
outside the read set, and a `*` deny breaks tool calling outright — both verified
|
||||
against `claude 2.1.212`.
|
||||
|
||||
**`--tools` is the outer bound.** `--tools` is not a permission rule: it names the
|
||||
built-in tools the CLI **loads**. ecr passes exactly the granted read tools
|
||||
(`Read,Grep,Glob` or fewer), and `""` for the coordinator and the no-tools fallback.
|
||||
A tool that is not loaded cannot be called, so a new tool in a future CLI version
|
||||
stays out without anyone adding its name. The deny list was not enough on its own:
|
||||
against `claude 2.1.280`, `dontAsk` rejected `Monitor` and `Workflow`, but
|
||||
`EnterWorktree` created a git worktree, `RemoteTrigger` listed the logged-in
|
||||
account's cloud routines, and `SendMessage` and `Skill` ran, because those tools ask
|
||||
no permission. MCP tools (the research tools) and the CLI's `StructuredOutput` tool
|
||||
(used by `--json-schema`) are outside the built-in set and stay loaded — also verified
|
||||
against `2.1.280`. The scoped allow rules and `ALWAYS_DENIED_TOOLS` stay as inner
|
||||
layers ([observed] `claude-code.ts` `buildClaudeArgs`, `ALWAYS_DENIED_TOOLS`).
|
||||
|
||||
**The env is an allowlist.** The child env is built from `CHILD_ENV_ALLOWLIST` (PATH,
|
||||
HOME, locale, tmp, proxies, config dirs — what a CLI needs and nothing else), never a
|
||||
|
||||
@@ -56,7 +56,7 @@ The three workflows share a posture built entirely around one fact: a reviewer j
|
||||
**Concurrency defaults by auth mode.** The commented `chunk.concurrency` default is 6 for an API key and 3 for a subscription (OAuth) credential, "one account handles many parallel streams poorly, and several PRs may review on the same credential at once" [observed: `templates/config.jsonc:28-30`].
|
||||
|
||||
**Muse Spark is opt-in without weakening the default secret surface.** The root config
|
||||
documents `meta/muse-spark-1.2` plus a `META_API_KEY` auth entry, and each
|
||||
documents `meta/muse-spark-1.3` plus a `META_API_KEY` auth entry, and each
|
||||
review-running workflow shows the corresponding secret line commented out. It is not
|
||||
forwarded by the default scaffold: adopters replace the Anthropic line and set
|
||||
`ECR_EXPECTED_TOKEN_ENV=META_API_KEY`, so the model process receives only the
|
||||
|
||||
@@ -245,6 +245,9 @@ test("buildClaudeArgs: read-only, trust-isolated, subscription argv; never --bar
|
||||
);
|
||||
expect(joined).not.toContain(" Grep ");
|
||||
expect(joined).not.toContain(" Glob ");
|
||||
// --tools loads only the granted read tools, so an ungranted built-in (a new one
|
||||
// in a future CLI included) never reaches the model.
|
||||
expect(args[args.indexOf("--tools") + 1]).toBe("Read,Grep,Glob");
|
||||
// Write/exec/net tools are denied outright; out-of-tree reads are denied by
|
||||
// dontAsk's unmatched-rule denial (NO explicit Read deny: `Read(~/**)` would
|
||||
// deny the whole tree whenever the repo lives under the home directory).
|
||||
@@ -582,6 +585,7 @@ test("buildClaudeArgs: cross-cutting/verifier get Read+Grep scoped, Glob denied
|
||||
// withheld read tool must be denied BY NAME, not just left out of --allowedTools.
|
||||
expect(joined).toContain("--allowedTools Read(//work/repo/**) Grep(//work/repo/**)");
|
||||
expect(joined).not.toContain("Glob(/");
|
||||
expect(args[args.indexOf("--tools") + 1]).toBe("Read,Grep");
|
||||
expect(joined).toContain(
|
||||
"--disallowedTools Glob Bash Edit Write NotebookEdit NotebookRead WebFetch WebSearch Task TodoWrite BashOutput KillShell ExitPlanMode",
|
||||
);
|
||||
@@ -599,9 +603,9 @@ test("buildClaudeArgs: reviewer tools honor the configured read set (list ignore
|
||||
"--allowedTools Read(//work/repo/**) Grep(//work/repo/**) Glob(//work/repo/**)",
|
||||
);
|
||||
// `list` has no scoped Claude equivalent and is silently ignored (no crash, no rule).
|
||||
expect(joined).not.toContain("List");
|
||||
expect(args.some((arg) => arg.startsWith("List("))).toBe(false);
|
||||
expect(joined).toContain(
|
||||
"--disallowedTools Bash Edit Write NotebookEdit NotebookRead WebFetch WebSearch Task TodoWrite BashOutput KillShell ExitPlanMode",
|
||||
"--disallowedTools Bash Edit Write NotebookEdit NotebookRead WebFetch WebSearch Task TodoWrite BashOutput KillShell ExitPlanMode ListAgents",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -616,6 +620,8 @@ test("buildClaudeArgs: empty tools (coordinator/no-tools fallback) deny every re
|
||||
// No --allowedTools at all, and Read/Grep/Glob explicitly denied so dontAsk's
|
||||
// default-allow can't grant a read.
|
||||
expect(joined).not.toContain("--allowedTools");
|
||||
// An empty --tools value loads no built-in tool at all.
|
||||
expect(args[args.indexOf("--tools") + 1]).toBe("");
|
||||
expect(joined).toContain(
|
||||
"--disallowedTools Read Grep Glob Bash Edit Write NotebookEdit NotebookRead WebFetch WebSearch Task TodoWrite BashOutput KillShell ExitPlanMode",
|
||||
);
|
||||
@@ -643,6 +649,8 @@ test("buildClaudeArgs: explicit research MCP replaces safe mode without loading
|
||||
expect(joined).toContain("mcp__platform_docs__search_platform_docs");
|
||||
expect(joined).toContain("mcp__platform_docs__fetch_platform_doc");
|
||||
expect(joined).toContain("--mcp-config /private/mcp.json");
|
||||
// MCP tools sit outside the built-in set, so --tools does not remove them.
|
||||
expect(args[args.indexOf("--tools") + 1]).toBe("Read");
|
||||
expect(joined).toContain("--setting-sources --disable-slash-commands");
|
||||
expect(joined).toContain("--strict-mcp-config");
|
||||
expect(joined).toContain("--no-session-persistence");
|
||||
|
||||
@@ -89,9 +89,17 @@ test("inlineStubBody: neutral — never claims resolved or dismissed", () => {
|
||||
|
||||
// ---- oneLineRationale ----
|
||||
|
||||
test("oneLineRationale: collapses whitespace, escapes <, truncates at a word boundary", () => {
|
||||
test("oneLineRationale: strips HTML tags, collapses whitespace, escapes <, truncates at a word boundary", () => {
|
||||
expect(oneLineRationale("a\n\nb c")).toBe("a b c");
|
||||
expect(oneLineRationale("<details>x</details>")).toBe("<details>x</details>");
|
||||
expect(oneLineRationale("<details>x</details>")).toBe("x");
|
||||
expect(oneLineRationale("High — reason.<br>**Impact:** Medium<BR/>end")).toBe(
|
||||
"High — reason. **Impact:** Medium end",
|
||||
);
|
||||
expect(oneLineRationale("<details><summary>Why</summary>\n\nbody</details>")).toBe("Why body");
|
||||
// Non-formatting angle brackets stay, escaped.
|
||||
expect(oneLineRationale("returns `Promise<void>` when a < b")).toBe(
|
||||
"returns `Promise<void>` when a < b",
|
||||
);
|
||||
const long = Array.from({ length: 60 }, (_, i) => `word${i}`).join(" ");
|
||||
const short = oneLineRationale(long);
|
||||
expect(short.length).toBeLessThanOrEqual(161);
|
||||
|
||||
@@ -342,10 +342,10 @@ test("an upstream-alias auth entry synthesizes a provider block with exactly the
|
||||
expect(opencode.provider?.openai).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Meta auth synthesizes the public Muse Responses provider without embedding its key", () => {
|
||||
test("Meta auth synthesizes the public Muse 1.3 Responses provider without embedding its key", () => {
|
||||
const config = configWith({
|
||||
agents: [{ id: "correctness", model: "meta/muse-spark-1.2" }],
|
||||
coordinatorModel: "meta/muse-spark-1.2",
|
||||
agents: [{ id: "correctness", model: "meta/muse-spark-1.3" }],
|
||||
coordinatorModel: "meta/muse-spark-1.3-contributor",
|
||||
auth: [{ mode: "api-key", provider: "meta", tokenEnv: "META_API_KEY" }],
|
||||
});
|
||||
const opencode = buildOpencodeConfig(config) as {
|
||||
@@ -354,7 +354,14 @@ test("Meta auth synthesizes the public Muse Responses provider without embedding
|
||||
{
|
||||
npm: string;
|
||||
options: { baseURL: string; apiKey: string };
|
||||
models: Record<string, { reasoning?: boolean; options?: Record<string, unknown> }>;
|
||||
models: Record<
|
||||
string,
|
||||
{
|
||||
reasoning?: boolean;
|
||||
limit?: { context: number; output: number };
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
@@ -365,12 +372,17 @@ test("Meta auth synthesizes the public Muse Responses provider without embedding
|
||||
baseURL: META_MODEL_API_BASE_URL,
|
||||
apiKey: "{env:META_API_KEY}",
|
||||
});
|
||||
expect(Object.keys(meta!.models)).toEqual(["muse-spark-1.2"]);
|
||||
expect(meta!.models["muse-spark-1.2"]?.reasoning).toBe(true);
|
||||
expect(meta!.models["muse-spark-1.2"]?.options).toMatchObject({
|
||||
expect(Object.keys(meta!.models)).toEqual(["muse-spark-1.3", "muse-spark-1.3-contributor"]);
|
||||
expect(meta!.models["muse-spark-1.3"]?.reasoning).toBe(true);
|
||||
expect(meta!.models["muse-spark-1.3"]?.limit).toEqual({
|
||||
context: 1_048_576,
|
||||
output: 1_048_576,
|
||||
});
|
||||
expect(meta!.models["muse-spark-1.3"]?.options).toMatchObject({
|
||||
reasoningEffort: "high",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
});
|
||||
expect(meta!.models["muse-spark-1.3-contributor"]?.reasoning).toBe(true);
|
||||
});
|
||||
|
||||
test("a Meta-named upstream alias does not redirect the upstream credential to Meta", () => {
|
||||
@@ -400,25 +412,22 @@ test("a Meta-named upstream alias does not redirect the upstream credential to M
|
||||
expect(Object.keys(alias!.models)).toEqual(["muse-spark-1.2"]);
|
||||
});
|
||||
|
||||
test("Meta provider does not register an unknown Muse id, so preflight can reject it", () => {
|
||||
test("Meta provider registers future model families neutrally and only for the meta prefix", () => {
|
||||
const config = configWith({
|
||||
agents: [{ id: "correctness", model: "meta/muse-spark-typo" }],
|
||||
coordinatorModel: "meta/muse-spark-typo",
|
||||
agents: [
|
||||
{ id: "correctness", model: "meta/muse-foo" },
|
||||
{ id: "security", model: "openai/muse-foo" },
|
||||
],
|
||||
coordinatorModel: "meta/a-future-family-2",
|
||||
auth: [{ mode: "api-key", provider: "meta", tokenEnv: "META_API_KEY" }],
|
||||
});
|
||||
const opencode = buildOpencodeConfig(config) as {
|
||||
provider?: Record<string, { models: Record<string, unknown> }>;
|
||||
};
|
||||
expect(opencode.provider?.meta?.models).toEqual({});
|
||||
expect(
|
||||
findUnknownModels(["meta/muse-spark-typo"], { meta: ["muse-spark-1.2"] }, ["meta"]),
|
||||
).toEqual([
|
||||
{
|
||||
model: "meta/muse-spark-typo",
|
||||
reason: "model",
|
||||
suggestions: ["muse-spark-1.2"],
|
||||
},
|
||||
]);
|
||||
expect(opencode.provider?.meta?.models).toEqual({
|
||||
"muse-foo": {},
|
||||
"a-future-family-2": {},
|
||||
});
|
||||
});
|
||||
|
||||
test("buildOpencodeConfig registers the no-tools stack verifier (empty tool list)", () => {
|
||||
|
||||
+21
-8
@@ -92,17 +92,16 @@ const READ_TOOL_MAP: Record<string, "Read" | "Grep" | "Glob"> = {
|
||||
glob: "Glob",
|
||||
};
|
||||
const ALL_READ_TOOLS = ["Read", "Grep", "Glob"] as const;
|
||||
// @ref LLP 0003#claude-code-cli-containment [implements] — deny enumeration (not allow-only) because an empty/absent --allowedTools list default-ALLOWS reads; verified against claude 2.1.212, revisit on every CLI version bump
|
||||
// @ref LLP 0003#claude-code-cli-containment [implements] — inner deny layer behind --tools, because an empty/absent --allowedTools list default-ALLOWS reads; verified against claude 2.1.212, tool list re-checked against 2.1.280 (ListAgents added)
|
||||
/**
|
||||
* Tools never available to a review pass, whatever the role. A DENY enumeration is
|
||||
* the only workable containment: permission rules cannot fail closed here — reads
|
||||
* Tools never available to a review pass, whatever the role. The primary bound is
|
||||
* `--tools` (see buildClaudeArgs), which loads only the granted read tools; this
|
||||
* list is the inner layer behind it. Permission rules alone cannot fail closed — reads
|
||||
* inside the workspace are default-ALLOWED even when an allow list is present but
|
||||
* unmatched, and a `*` deny breaks tool calling outright (both verified against
|
||||
* claude 2.1.212). The residual risk — a FUTURE CLI version shipping a new
|
||||
* read-capable tool this list doesn't name — is bounded by pinning the CLI version
|
||||
* (the scaffolded workflow installs an exact @anthropic-ai/claude-code version;
|
||||
* bump it deliberately and revisit this list). Unknown names are ignored by the
|
||||
* CLI, so denying tools that don't exist in a given version is harmless.
|
||||
* claude 2.1.212). A FUTURE CLI version shipping a new tool this list doesn't name
|
||||
* is no longer the gap it was: `--tools` does not load it. Unknown names are ignored
|
||||
* by the CLI, so denying tools that don't exist in a given version is harmless.
|
||||
*/
|
||||
const ALWAYS_DENIED_TOOLS = [
|
||||
"Bash",
|
||||
@@ -117,6 +116,7 @@ const ALWAYS_DENIED_TOOLS = [
|
||||
"BashOutput",
|
||||
"KillShell",
|
||||
"ExitPlanMode",
|
||||
"ListAgents",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -273,6 +273,16 @@ export function claudeModelMatches(requested: string, actualKey: string): boolea
|
||||
* tree itself (paths resolve to absolute), and `Read(~/**)` denies the whole tree
|
||||
* whenever the repo lives under the home directory — the common case.
|
||||
*
|
||||
* `--tools` is the outer bound: it names the built-in tools the CLI LOADS, so
|
||||
* everything else (including tools a future CLI version adds) never reaches the
|
||||
* model. It is an availability allowlist, not a permission rule, so it fails closed
|
||||
* where `--allowedTools` does not. Verified against claude 2.1.280: `dontAsk` does
|
||||
* NOT stop tools that ask no permission — EnterWorktree created a git worktree,
|
||||
* RemoteTrigger listed the account's cloud routines, SendMessage and Skill ran — and
|
||||
* `--tools` removes them all. MCP tools (the research tools) and the CLI's own
|
||||
* StructuredOutput tool (`--json-schema`) are not in the built-in set and stay
|
||||
* loaded. The scoped allow rules and ALWAYS_DENIED_TOOLS stay as the inner layers.
|
||||
*
|
||||
* This is NOT, by itself, a boundary against a symlink committed inside the PR-head
|
||||
* tree (e.g. `docs/notes.md -> ~/.claude/.credentials.json`): the permission rule
|
||||
* matches the literal path ARGUMENT, which is in-tree, but Read/Grep then follow the
|
||||
@@ -324,6 +334,9 @@ export function buildClaudeArgs(opts: {
|
||||
"--append-system-prompt",
|
||||
opts.system,
|
||||
...(opts.jsonSchema ? ["--json-schema", JSON.stringify(opts.jsonSchema)] : []),
|
||||
// @ref LLP 0003#claude-code-cli-containment [implements] — load only the granted read tools; "" loads none
|
||||
"--tools",
|
||||
enabled.join(","),
|
||||
...(allowedTools.length > 0 ? ["--allowedTools", ...allowedTools] : []),
|
||||
"--disallowedTools",
|
||||
...deniedReadTools,
|
||||
|
||||
+46
-8
@@ -20,10 +20,10 @@ import type { ResearchMcpRuntime } from "./research.js";
|
||||
export const CLAUDE_CODE_ENGINE = "claude-code" as const;
|
||||
|
||||
/** Fixed so repo config cannot redirect the Meta credential. */
|
||||
// @ref LLP 0003#muse-spark-via-meta-model-api [implements] — fixed endpoint and model allowlist
|
||||
// @ref LLP 0003#muse-spark-via-meta-model-api [implements] — fixed endpoint, provider routing, and known-model metadata
|
||||
export const META_MODEL_API_BASE_URL = "https://api.meta.ai/v1";
|
||||
|
||||
/** Supported public Muse models. Unknown ids remain visible to preflight. */
|
||||
/** Known public Muse models. Other explicit `meta/...` ids use neutral metadata. */
|
||||
export const META_MUSE_MODELS: Record<string, Record<string, unknown>> = {
|
||||
"muse-spark-1.2": {
|
||||
name: "Muse Spark 1.2",
|
||||
@@ -55,6 +55,36 @@ export const META_MUSE_MODELS: Record<string, Record<string, unknown>> = {
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
"muse-spark-1.3": {
|
||||
name: "Muse Spark 1.3",
|
||||
reasoning: true,
|
||||
limit: { context: 1_048_576, output: 1_048_576 },
|
||||
modalities: {
|
||||
input: ["text", "image", "pdf", "video"],
|
||||
output: ["text"],
|
||||
},
|
||||
options: {
|
||||
store: false,
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
"muse-spark-1.3-contributor": {
|
||||
name: "Muse Spark 1.3 Contributor",
|
||||
reasoning: true,
|
||||
limit: { context: 1_048_576, output: 1_048_576 },
|
||||
modalities: {
|
||||
input: ["text", "image", "pdf", "video"],
|
||||
output: ["text"],
|
||||
},
|
||||
options: {
|
||||
store: false,
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -265,7 +295,19 @@ export function buildOpencodeConfig(
|
||||
entry.tokenEnv &&
|
||||
!entry.upstream
|
||||
) {
|
||||
// Muse needs the Responses adapter; openai-compatible uses Chat Completions.
|
||||
// Meta's Model API uses the Responses adapter; openai-compatible uses Chat
|
||||
// Completions. Register every explicitly referenced `meta/…` model so a new
|
||||
// Meta family/name does not require an ECR release merely to select the right
|
||||
// provider. Known Muse ids get their published capabilities; unknown ids stay
|
||||
// neutral and are validated by Meta on their first request.
|
||||
const models: Record<string, unknown> = {};
|
||||
for (const id of referencedModels) {
|
||||
const slash = id.indexOf("/");
|
||||
if (slash > 0 && id.slice(0, slash) === "meta" && id.length > slash + 1) {
|
||||
const model = id.slice(slash + 1);
|
||||
models[model] = META_MUSE_MODELS[model] ?? {};
|
||||
}
|
||||
}
|
||||
provider.meta = {
|
||||
npm: "@ai-sdk/openai",
|
||||
name: "Meta Model API",
|
||||
@@ -273,11 +315,7 @@ export function buildOpencodeConfig(
|
||||
baseURL: META_MODEL_API_BASE_URL,
|
||||
apiKey: `{env:${entry.tokenEnv}}`,
|
||||
},
|
||||
models: Object.fromEntries(
|
||||
Object.entries(META_MUSE_MODELS).filter(([model]) =>
|
||||
referencedModels.includes(`meta/${model}`),
|
||||
),
|
||||
),
|
||||
models,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
+19
-6
@@ -385,14 +385,27 @@ function sourceLabel(value: string): string {
|
||||
const SHORT_RATIONALE_CHARS = 160;
|
||||
|
||||
/**
|
||||
* A rationale reduced to one plain line for the short (inlined) form: whitespace
|
||||
* collapsed, every `<` escaped so a truncated HTML block (`<details>`) can never
|
||||
* leave an unclosed tag in the comment, cut at a word boundary with an ellipsis.
|
||||
* This excerpt is the audit trail that survives the PR author collapsing or
|
||||
* resolving the inline thread, so it must always be non-empty when the rationale is.
|
||||
* HTML formatting tags a model writes into a rationale (`<br>`, `<details>`, ...).
|
||||
* A fixed list, not "anything in angle brackets", so a type like `Promise<void>`
|
||||
* in the prose survives.
|
||||
*/
|
||||
const FORMATTING_TAG_RE =
|
||||
/<\/?(?:a|b|blockquote|br|code|del|details|div|em|h[1-6]|hr|i|kbd|li|ol|p|pre|s|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|u|ul)\b[^<>]*>/gi;
|
||||
|
||||
/**
|
||||
* A rationale reduced to one plain line for the short (inlined) form: HTML
|
||||
* formatting tags removed (an escaped `<br>` would show as literal text),
|
||||
* whitespace collapsed, every remaining `<` escaped so a truncated HTML block can
|
||||
* never leave an unclosed tag in the comment, cut at a word boundary with an
|
||||
* ellipsis. This excerpt is the audit trail that survives the PR author collapsing
|
||||
* or resolving the inline thread, so it must always be non-empty when the rationale is.
|
||||
*/
|
||||
export function oneLineRationale(text: string): string {
|
||||
const flat = text.replace(/</g, "<").replace(/\s+/g, " ").trim();
|
||||
const flat = text
|
||||
.replace(FORMATTING_TAG_RE, " ")
|
||||
.replace(/</g, "<")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (flat.length <= SHORT_RATIONALE_CHARS) {
|
||||
return flat;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ alwaysRun: true
|
||||
# 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 Opus pass from hanging.
|
||||
# @ref LLP 0009#config-and-prompt-templates [implements]
|
||||
model: anthropic/claude-opus-5
|
||||
model: anthropic/claude-opus-5-5
|
||||
---
|
||||
|
||||
# Security & secrets
|
||||
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
# 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]12
|
||||
run: npm install -g @anthropic-ai/[email protected]80
|
||||
|
||||
# SECURITY: the base-ref checkout includes every .expo-code-review/ config,
|
||||
# whose auth.tokenEnv names the forwarded model credential. `ecr verify-config`
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
# 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]12
|
||||
run: npm install -g @anthropic-ai/[email protected]80
|
||||
|
||||
# SECURITY: the base-ref checkout above includes every .expo-code-review/
|
||||
# config.jsonc + routing.jsonc, whose auth.tokenEnv names the env var the CLI
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
// 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",
|
||||
"model": "anthropic/claude-opus-5-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: anthropic/claude-opus-5\n---`.
|
||||
// e.g. `---\nmodel: anthropic/claude-opus-5-5\n---`.
|
||||
|
||||
// @ref LLP 0009#config-and-prompt-templates [implements] — suggestions off by default, not a schema limit
|
||||
"policy": {
|
||||
@@ -87,7 +87,7 @@
|
||||
// "auth": { "mode": "api-key", "provider": "openai", "tokenEnv": "OPENAI_API_KEY" }
|
||||
//
|
||||
// Muse Spark via the public Meta Model API Responses endpoint:
|
||||
// set models to `meta/muse-spark-1.2` and configure:
|
||||
// set models to `meta/muse-spark-1.3` and configure:
|
||||
// "auth": { "providers": {
|
||||
// "meta": { "mode": "api-key", "tokenEnv": "META_API_KEY" }
|
||||
// } }
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# 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: anthropic/claude-opus-5
|
||||
model: anthropic/claude-opus-5-5
|
||||
---
|
||||
|
||||
# Coordinator — consolidation & decision
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
// Default model for every agent in this scope. Override per-agent via frontmatter,
|
||||
// or at runtime with REVIEWER_MODEL.
|
||||
"model": "anthropic/claude-sonnet-5",
|
||||
"model": "anthropic/claude-opus-5-5",
|
||||
|
||||
// Agents: every markdown file in agents/ beside this file is one reviewer for this
|
||||
// scope (id = filename). shared.md + coordinator.md are this scope's prompts.
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
# 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]12
|
||||
run: npm install -g @anthropic-ai/[email protected]80
|
||||
|
||||
# SECURITY: the TRUSTED BASE checkout above includes every
|
||||
# .expo-code-review/config.jsonc + routing.jsonc, whose auth.tokenEnv names the
|
||||
|
||||
Reference in new issue
Block a user