Let the board choose agent models, per launch intent

BOARD_AGENT_MODEL picks one model for every headless launch;
BOARD_AGENT_MODEL_WORK / _ACT_PR / _REVIEW beat it per intent (review
covers PR reviews and relevance checks). Empty — the default — means
the variable never reaches the adapter and the vendor's own resolution
applies, exactly today's behaviour; a stray AGENT_MODEL in the board's
own environment is stripped rather than inherited silently.

Core carries the name as an opaque string: config resolves intent →
model, _launch passes it as AGENT_MODEL, and each adapter renders it
natively — claude appends --model, opencode sets the generated
config's model key ("provider/model-id", per its docs). The resolved
model (or its absence) is recorded on the agent record, so the
Sessions and Focus views state what a run actually rode instead of
leaving it to whoever's machine the board happens to run on.

Stub-binary tests pin the byte-identical-when-unset guarantee, the
per-intent resolution, the launch-env seam and the record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
istos
2026-07-30 07:37:39 +02:00
co-authored by Claude Fable 5
parent d591bd843e
commit 9fed08d174
9 changed files with 339 additions and 20 deletions
+13
View File
@@ -15,6 +15,19 @@ BOARD_AGENT_ADAPTER=claude
BOARD_CLAUDE_BIN=claude
BOARD_OPENCODE_BIN=opencode
# The model headless agents run on — an opaque vendor-native name the
# adapter passes through untranslated (claude: a model name or alias for
# --model; opencode: the "provider/model-id" config key). Empty = inherit
# the vendor's own default, i.e. whatever the CLI on this machine would
# pick anyway. The per-intent settings beat the general one for their
# intent only; _REVIEW covers both PR reviews and relevance checks. Work
# agents write code; reviews just read and judge — they can ride a
# cheaper, faster model.
BOARD_AGENT_MODEL=
BOARD_AGENT_MODEL_WORK=
BOARD_AGENT_MODEL_ACT_PR=
BOARD_AGENT_MODEL_REVIEW=
# Command prefixes headless agents may run in their worktree — the
# project's test/check commands, comma-separated, in plain neutral form
# (each adapter renders them into its vendor's permission rules; the
+14 -3
View File
@@ -15,9 +15,9 @@ An adapter is a directory with two executables:
- env in: `AGENT_PROMPT` (the full prompt), `AGENT_MODE` (the launch
intent, below), `AGENT_COMMANDS` (the project's allowed command
prefixes, below), `AGENT_CWD`, and the `BOARD_*` passthrough
(`BOARD_AGENT_ID`, `BOARD_TASK`, `BOARD_PORT`) which your event bridge
must forward with every event.
prefixes, below), `AGENT_MODEL` (optional, below), `AGENT_CWD`, and
the `BOARD_*` passthrough (`BOARD_AGENT_ID`, `BOARD_TASK`,
`BOARD_PORT`) which your event bridge must forward with every event.
- stdout is captured by the board as the job log. The prompts instruct the
agent to end with marker lines (`NOT READY:`, `RELEVANCE REVIEW:`,
`PR REVIEW:`, `ADDRESSED:`) — the board parses them from this output, so
@@ -57,6 +57,17 @@ prefix-pattern based, so the translation is mechanical:
"allow"}}` in a generated config, wildcard rules, last match wins
(`opencode/permission_config.py`)
### The model (`AGENT_MODEL`) — optional
Absent = the vendor's own default: launch without any model argument and
let your CLI resolve it however it normally would. When set, it is an
opaque vendor-native model name — a claude alias, an opencode
`provider/model-id` — that core never validates or interprets; pass it
through untranslated (claude → `--model "$AGENT_MODEL"`, opencode → the
generated config's `model` key). Never send your vendor an empty value:
the board only sets the variable when a model is actually configured
(`BOARD_AGENT_MODEL` and its per-intent overrides in `local/.env`).
### `wire` — wire live-session visibility into the host project
Called by `install.py` with the project root as argv[1] (plus `--dry-run`).
+11
View File
@@ -7,6 +7,9 @@
# (see core/adapters/README.md)
# AGENT_COMMANDS comma-separated neutral command prefixes the
# project lets agents run (tests/checks)
# AGENT_MODEL optional; a claude model name/alias passed
# through as --model. Absent = the CLI's own
# resolution (user settings), untouched.
# AGENT_CWD working directory (already set as cwd by the board)
# BOARD_* passthrough for the event bridge
# stdout: captured by the board as the job log; the closing report's
@@ -25,12 +28,20 @@ BIN="${BOARD_CLAUDE_BIN:-claude}"
MODE="${AGENT_MODE:-work}"
SETTINGS="$(python3 "$HERE/hook_settings.py" "$MODE")"
# The ${arr[@]+...} expansion keeps set -u happy on bash 3.2 when unset.
MODEL_ARGS=()
if [ -n "${AGENT_MODEL:-}" ]; then
MODEL_ARGS=(--model "$AGENT_MODEL")
fi
if [ "$MODE" = "review" ]; then
exec "$BIN" -p "$AGENT_PROMPT" --settings "$SETTINGS" \
${MODEL_ARGS[@]+"${MODEL_ARGS[@]}"} \
--permission-mode default \
--disallowedTools Edit Write NotebookEdit
else
# work and act-pr both mutate the worktree; the allowlist differs.
exec "$BIN" -p "$AGENT_PROMPT" --settings "$SETTINGS" \
${MODEL_ARGS[@]+"${MODEL_ARGS[@]}"} \
--permission-mode acceptEdits
fi
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""Print the opencode config JSON for one headless launch: the permission
rules for the launch's intent.
rules for the launch's intent, plus the model when the board configured one.
Usage: permission_config.py [work|act-pr|review]
@@ -21,6 +21,10 @@ and never a blanket allow: the worktree is isolated, the shell is not.
The project's test/check commands arrive in AGENT_COMMANDS as comma-
separated neutral command prefixes (set BOARD_AGENT_COMMANDS in
local/.env); here each becomes "<prefix>" and "<prefix> *" allow rules.
AGENT_MODEL, when set, becomes the config's top-level "model" key —
opencode's "provider/model-id" form (opencode.ai/docs/config), passed
through untranslated. Absent = no key, opencode's own resolution applies.
"""
import json
import os
@@ -56,17 +60,21 @@ def bash_rules(mode: str, commands: list[str]) -> dict:
return rules
def build_config(mode: str, commands: list[str]) -> dict:
return {
def build_config(mode: str, commands: list[str], model: str = "") -> dict:
config = {
"$schema": "https://opencode.ai/config.json",
"permission": {
"edit": "deny" if mode == "review" else "allow",
"bash": bash_rules(mode, commands),
},
}
if model:
config["model"] = model
return config
if __name__ == "__main__":
mode = sys.argv[1] if len(sys.argv) > 1 else "work"
commands = split_commands(os.environ.get("AGENT_COMMANDS", ""))
print(json.dumps(build_config(mode, commands)))
model = os.environ.get("AGENT_MODEL", "").strip()
print(json.dumps(build_config(mode, commands, model)))
+3
View File
@@ -7,6 +7,9 @@
# (see core/adapters/README.md)
# AGENT_COMMANDS comma-separated neutral command prefixes the
# project lets agents run (tests/checks)
# AGENT_MODEL optional; opencode's "provider/model-id" form,
# set as the generated config's model key.
# Absent = opencode's own resolution, untouched.
# AGENT_CWD working directory (already set as cwd by the board)
# BOARD_* passthrough for the event bridge
# stdout: captured by the board as the job log; `opencode run` prints
+24 -12
View File
@@ -81,6 +81,9 @@ def _agent_public(record: dict) -> dict:
("id", "task", "branch", "worktree", "status", "rc", "started", "session")}
public["mode"] = record.get("mode", "work")
public["name"] = record.get("name")
# The model the launch was actually given; None = inherited the
# vendor's own default. Honesty for the Sessions/Focus views.
public["model"] = record.get("model")
return public
@@ -111,9 +114,11 @@ def _launch(mode: str, prompt: str, cwd: Path, agent_id: str, filename: str, log
The adapter contract: `run` gets AGENT_PROMPT, AGENT_MODE (the intent:
work = mutate and commit, act-pr = work + push, review = read-only +
post PR verdicts) and AGENT_COMMANDS (the project's runnable command
prefixes) plus the BOARD_* passthrough for its event bridge; its
stdout is the job log; exit 0 = completed.
post PR verdicts), AGENT_COMMANDS (the project's runnable command
prefixes) and AGENT_MODEL (the configured model, when there is one)
plus the BOARD_* passthrough for its event bridge; its stdout is the
job log; exit 0 = completed. Returns (proc, log_file, model) with
model = '' when the launch inherits the vendor default.
"""
adapter = config.adapter_dir()
if adapter is None:
@@ -130,6 +135,13 @@ def _launch(mode: str, prompt: str, cwd: Path, agent_id: str, filename: str, log
"BOARD_TASK": filename,
"BOARD_PORT": str(state.serve_port),
})
model = config.agent_model(mode)
if model:
env["AGENT_MODEL"] = model
else:
# Inherit = the variable is simply absent. Popping also stops a
# stray AGENT_MODEL in the board's own environment leaking through.
env.pop("AGENT_MODEL", None)
log_file = log_path.open("wb")
try:
proc = subprocess.Popen(
@@ -138,7 +150,7 @@ def _launch(mode: str, prompt: str, cwd: Path, agent_id: str, filename: str, log
except OSError as exc:
log_file.close()
raise ValueError(f"could not launch adapter {adapter}: {exc}")
return proc, log_file
return proc, log_file, model
def start_agent(filename: str, stage: str) -> dict:
@@ -185,7 +197,7 @@ def start_agent(filename: str, stage: str) -> dict:
prompt = config.prompt("work.md").format(
branch=branch, filename=filename, body=task["body"])
proc, log_file = _launch("work", prompt, worktree, agent_id, filename, log_path)
proc, log_file, model = _launch("work", prompt, worktree, agent_id, filename, log_path)
name = _pick_name(stem)
record = {
@@ -193,7 +205,7 @@ def start_agent(filename: str, stage: str) -> dict:
"worktree": str(worktree), "base": base, "status": "running",
"rc": None, "started": time.time(), "session": None,
"log": str(log_path), "proc": proc, "origin": stage, "mode": "work",
"name": name,
"name": name, "model": model or None,
}
with state.LOCK:
state.AGENTS[agent_id] = record
@@ -219,14 +231,14 @@ def start_review(filename: str, stage: str) -> dict:
prompt = config.prompt("review.md").format(
stage=stage, filename=filename, body=task["body"])
proc, log_file = _launch("review", prompt, config.REPO, agent_id, filename, log_path)
proc, log_file, model = _launch("review", prompt, config.REPO, agent_id, filename, log_path)
name = _pick_name(filename)
record = {
"id": agent_id, "task": filename, "branch": None, "worktree": None,
"base": None, "status": "running", "rc": None, "started": time.time(),
"session": None, "log": str(log_path), "proc": proc,
"origin": stage, "mode": "review", "name": name,
"origin": stage, "mode": "review", "name": name, "model": model or None,
}
with state.LOCK:
state.AGENTS[agent_id] = record
@@ -352,13 +364,13 @@ def start_pr_review(filename: str, stage: str) -> dict:
prompt = config.prompt("review-pr.md").format(
filename=filename, pr=task["pr"], branch=branch, body=task["body"])
proc, log_file = _launch("review", prompt, config.REPO, agent_id, filename, log_path)
proc, log_file, model = _launch("review", prompt, config.REPO, agent_id, filename, log_path)
record = {
"id": agent_id, "task": filename, "branch": branch, "worktree": None,
"base": None, "status": "running", "rc": None, "started": time.time(),
"session": None, "log": str(log_path), "proc": proc,
"origin": stage, "mode": "review", "name": name,
"origin": stage, "mode": "review", "name": name, "model": model or None,
}
with state.LOCK:
state.AGENTS[agent_id] = record
@@ -403,14 +415,14 @@ def start_pr_fix(filename: str, stage: str) -> dict:
prompt = config.prompt("act-pr.md").format(
filename=filename, branch=branch, pr=task["pr"], body=task["body"])
# act-pr is the one intent allowed to push: the PR must update.
proc, log_file = _launch("act-pr", prompt, worktree, agent_id, filename, log_path)
proc, log_file, model = _launch("act-pr", prompt, worktree, agent_id, filename, log_path)
record = {
"id": agent_id, "task": filename, "branch": branch,
"worktree": str(worktree), "base": None, "status": "running",
"rc": None, "started": time.time(), "session": None,
"log": str(log_path), "proc": proc, "origin": stage, "mode": "work",
"name": name,
"name": name, "model": model or None,
}
with state.LOCK:
state.AGENTS[agent_id] = record
+5 -1
View File
@@ -1300,12 +1300,15 @@ function renderFlight() {
const stopBtn = agent && agent.status === 'running'
? `<button id="stopagent" class="stopbtn" data-aid="${esc(agent.id)}">Hold</button>` : '';
const branch = agent && agent.branch ? ` · <span class="mono">${esc(agent.branch)}</span>` : '';
// Honesty about what the run actually rode: the configured model, or
// the vendor default it inherited. Interactive sessions say nothing.
const model = agent ? ` · <span class="mono">${agent.model ? esc(agent.model) : 'model inherited'}</span>` : '';
$('#fsession').innerHTML =
`<div><div class="s-title">${esc((meta.label || sid).split(' · ')[0])}` +
`<span class="sid">${esc(sid.slice(0, 8))}</span></div>` +
`<div class="s-line">${meta.task ? 'on ' + esc(meta.task) + ' · ' : ''}` +
`started ${fmtShort(meta.started)} · ${meta.count || 0} events · ` +
`${files.size} files edited · ${checks} check runs${branch}</div></div>` +
`${files.size} files edited · ${checks} check runs${branch}${model}</div></div>` +
stopBtn + spark(events, meta);
const stop = $('#stopagent');
if (stop) stop.addEventListener('click', () => stopAgent(stop.dataset.aid));
@@ -1490,6 +1493,7 @@ function renderFocus() {
refBits.push(task.number ? '#' + esc(task.number) : esc(task.file));
refBits.push(`<span class="acc">${esc((meta.label || '').split(' · ')[0])}</span>`);
if (agent && agent.branch) refBits.push('worktree ' + esc(agent.branch));
if (agent) refBits.push(agent.model ? esc(agent.model) : 'model inherited');
refBits.push(esc(task.stage) + '/' + esc(task.file));
} else {
refBits.push(esc(sid.slice(0, 8)), 'no task attached');
+18
View File
@@ -95,6 +95,24 @@ ADAPTER = setting("BOARD_AGENT_ADAPTER", "claude")
# the adapter's own knowledge; this list is the project's half.
AGENT_COMMANDS = setting("BOARD_AGENT_COMMANDS", "python3 -m unittest")
# Model per launch intent — an opaque vendor-native name core passes to the
# adapter untranslated (what names mean anything is vendor knowledge). Empty
# = inherit the vendor's own default, exactly today's behaviour. A per-intent
# setting beats the general one; review covers PR reviews and relevance
# checks (they share the review intent).
AGENT_MODEL = setting("BOARD_AGENT_MODEL", "")
AGENT_MODELS = {
"work": setting("BOARD_AGENT_MODEL_WORK", ""),
"act-pr": setting("BOARD_AGENT_MODEL_ACT_PR", ""),
"review": setting("BOARD_AGENT_MODEL_REVIEW", ""),
}
def agent_model(mode: str) -> str:
"""The model one launch intent rides — '' means inherit."""
return AGENT_MODELS.get(mode, "") or AGENT_MODEL
# GitHub plumbing: the gh CLI (stub-able for tests) and the git remote PRs
# go to. Empty remote = auto-detect the first remote; no remote = no PRs.
GH_BIN = setting("BOARD_GH_BIN", "gh")
+239
View File
@@ -0,0 +1,239 @@
"""Choosing agent models per launch intent (task 12): the BOARD_AGENT_MODEL
settings resolve intent model in config, travel to the adapter as
AGENT_MODEL (absent when empty never an empty flag value), and each
adapter renders the opaque name natively. With nothing set, launches are
byte-identical to the inherit-everything behaviour.
The `run` scripts are exercised end-to-end against stub binaries
(BOARD_CLAUDE_BIN / BOARD_OPENCODE_BIN), the same seam a live board uses.
Run with: python3 -m unittest discover -s tests
"""
import importlib.util
import json
import os
import stat
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
CORE = REPO / "manager" / "core"
CLAUDE = CORE / "adapters" / "claude"
OPENCODE = CORE / "adapters" / "opencode"
sys.path.insert(0, str(CORE))
import agents # noqa: E402
import config # noqa: E402
def _load(name, path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
permission_config = _load("permission_config", OPENCODE / "permission_config.py")
# Neutralize any local/.env or shell leakage: process env beats .env, and
# an empty value is exactly "nothing configured".
UNSET = {"BOARD_AGENT_MODEL": "", "BOARD_AGENT_MODEL_WORK": "",
"BOARD_AGENT_MODEL_ACT_PR": "", "BOARD_AGENT_MODEL_REVIEW": ""}
def _resolve(settings: dict) -> dict:
"""config.agent_model per intent, in a fresh interpreter so the given
settings are what config reads at import."""
env = dict(os.environ)
env.update(UNSET)
env.update(settings)
out = subprocess.check_output(
[sys.executable, "-c",
"import sys; sys.path.insert(0, sys.argv[1]); import config, json; "
"print(json.dumps({m: config.agent_model(m) "
"for m in ('work', 'act-pr', 'review')}))",
str(CORE)],
env=env, text=True)
return json.loads(out)
class ModelResolution(unittest.TestCase):
def test_nothing_set_means_inherit_for_every_intent(self):
self.assertEqual(_resolve({}),
{"work": "", "act-pr": "", "review": ""})
def test_the_general_setting_covers_all_intents(self):
self.assertEqual(_resolve({"BOARD_AGENT_MODEL": "vendor-x"}),
{"work": "vendor-x", "act-pr": "vendor-x",
"review": "vendor-x"})
def test_a_per_intent_setting_beats_the_general_one_for_that_intent_only(self):
resolved = _resolve({"BOARD_AGENT_MODEL": "big",
"BOARD_AGENT_MODEL_REVIEW": "cheap"})
self.assertEqual(resolved,
{"work": "big", "act-pr": "big", "review": "cheap"})
def test_per_intent_alone_leaves_the_others_inheriting(self):
resolved = _resolve({"BOARD_AGENT_MODEL_ACT_PR": "pusher"})
self.assertEqual(resolved,
{"work": "", "act-pr": "pusher", "review": ""})
class LaunchEnv(unittest.TestCase):
"""_launch's half of the contract: AGENT_MODEL set iff a model is
configured absent means absent, even against a leaky environment."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
tmp = Path(self._tmp.name)
self.capture = tmp / "env.json"
adapter = tmp / "stub-adapter"
adapter.mkdir()
run = adapter / "run"
run.write_text("#!/usr/bin/env python3\n"
"import json, os\n"
f"open({str(self.capture)!r}, 'w').write(json.dumps(dict(os.environ)))\n")
run.chmod(run.stat().st_mode | stat.S_IXUSR)
self._saved = (config.AGENT_MODEL, config.AGENT_MODELS,
config.adapter_dir, config.child_env)
config.adapter_dir = lambda: adapter
# A stray AGENT_MODEL inherited by the board process must not leak.
config.child_env = lambda: {"PATH": os.environ.get("PATH", ""),
"AGENT_MODEL": "stray-from-the-shell"}
def tearDown(self):
(config.AGENT_MODEL, config.AGENT_MODELS,
config.adapter_dir, config.child_env) = self._saved
self._tmp.cleanup()
def _launch_env(self, mode: str) -> tuple[dict, str]:
log = Path(self._tmp.name) / "job.log"
proc, log_file, model = agents._launch(
mode, "do the task", Path(self._tmp.name), "id-1", "t.md", log)
proc.wait()
log_file.close()
return json.loads(self.capture.read_text()), model
def test_no_model_configured_means_no_variable_at_all(self):
config.AGENT_MODEL = ""
config.AGENT_MODELS = {"work": "", "act-pr": "", "review": ""}
env, model = self._launch_env("work")
self.assertNotIn("AGENT_MODEL", env)
self.assertEqual(model, "")
def test_the_resolved_model_arrives_as_agent_model(self):
config.AGENT_MODEL = "big"
config.AGENT_MODELS = {"work": "", "act-pr": "", "review": "cheap"}
env, model = self._launch_env("review")
self.assertEqual(env["AGENT_MODEL"], "cheap")
self.assertEqual(model, "cheap")
env, model = self._launch_env("work")
self.assertEqual(env["AGENT_MODEL"], "big")
self.assertEqual(model, "big")
class AgentRecord(unittest.TestCase):
def test_public_record_carries_the_model_none_means_inherited(self):
base = {"id": "a", "task": "t.md", "branch": None, "worktree": None,
"status": "running", "rc": None, "started": 0.0,
"session": None, "mode": "review", "name": "Wren"}
self.assertIsNone(agents._agent_public(base)["model"])
self.assertEqual(
agents._agent_public({**base, "model": "cheap"})["model"], "cheap")
def _write_stub(directory: Path, name: str, script: str) -> Path:
stub = directory / name
stub.write_text(script, encoding="utf-8")
stub.chmod(stub.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
return stub
class ClaudeRunModel(unittest.TestCase):
"""AGENT_MODEL reaches the claude launch as --model; absent (or empty,
which must never happen but costs nothing to survive) = no flag."""
def _run(self, model: str | None) -> list[str]:
with tempfile.TemporaryDirectory() as tmp:
capture = Path(tmp) / "args.json"
stub = _write_stub(Path(tmp), "claude-stub",
"#!/usr/bin/env python3\n"
"import json, sys\n"
f"open({str(capture)!r}, 'w').write(json.dumps(sys.argv[1:]))\n")
wrapper = _write_stub(Path(tmp), "bin",
f"#!/usr/bin/env bash\nexec python3 {stub} \"$@\"\n")
env = dict(os.environ)
env.pop("AGENT_MODEL", None)
env.update({"BOARD_CLAUDE_BIN": str(wrapper),
"AGENT_PROMPT": "do the task", "AGENT_MODE": "work",
"AGENT_COMMANDS": "python3 -m unittest"})
if model is not None:
env["AGENT_MODEL"] = model
result = subprocess.run(["bash", str(CLAUDE / "run")], env=env,
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
return json.loads(capture.read_text())
def test_unset_launches_byte_identical_to_today(self):
args = self._run(None)
self.assertNotIn("--model", args)
self.assertEqual(args, self._run("")) # empty behaves like absent
def test_set_appends_model_and_changes_nothing_else(self):
args = self._run("claude-model-x")
i = args.index("--model")
self.assertEqual(args[i + 1], "claude-model-x")
self.assertEqual(args[:i] + args[i + 2:], self._run(None))
class OpencodeRunModel(unittest.TestCase):
"""AGENT_MODEL reaches the opencode launch as the generated config's
model key; absent = no key, config byte-identical to today."""
def _run(self, model: str | None) -> dict:
with tempfile.TemporaryDirectory() as tmp:
capture = Path(tmp) / "capture.json"
stub = _write_stub(Path(tmp), "opencode-stub",
"#!/usr/bin/env python3\n"
"import json, os\n"
f"open({str(capture)!r}, 'w').write("
"json.dumps(json.load(open(os.environ['OPENCODE_CONFIG']))))\n")
wrapper = _write_stub(Path(tmp), "bin",
f"#!/usr/bin/env bash\nexec python3 {stub} \"$@\"\n")
env = dict(os.environ)
env.pop("AGENT_MODEL", None)
env.update({"BOARD_OPENCODE_BIN": str(wrapper),
"AGENT_PROMPT": "do the task", "AGENT_MODE": "review",
"AGENT_COMMANDS": "python3 -m unittest"})
if model is not None:
env["AGENT_MODEL"] = model
result = subprocess.run(["bash", str(OPENCODE / "run")], env=env,
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
return json.loads(capture.read_text())
def test_unset_launches_byte_identical_to_today(self):
cfg = self._run(None)
self.assertNotIn("model", cfg)
self.assertEqual(cfg, permission_config.build_config(
"review", ["python3 -m unittest"]))
def test_set_lands_as_the_config_model_key_untranslated(self):
cfg = self._run("anthropic/model-x")
self.assertEqual(cfg["model"], "anthropic/model-x")
del cfg["model"]
self.assertEqual(cfg, self._run(None))
def test_build_config_only_grows_the_key_when_given_a_model(self):
commands = ["python3 -m unittest"]
self.assertNotIn("model", permission_config.build_config("work", commands))
self.assertEqual(
permission_config.build_config("work", commands, "p/m")["model"], "p/m")
if __name__ == "__main__":
unittest.main()