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
3 changes: 2 additions & 1 deletion host-setup/agent-safety/claude/model-tier-guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
`model-tiers.json` marks maintainer-only, today `highest`, so `fable`. Everything else passes.

The mapping is read from `model-tiers.json` beside this file, which the installer copies from the
hub's spec/model-tiers.json. A model release is then an edit to that file alone.
hub's spec/model-tiers.json. A model release is then an edit to the hub's spec/model-tiers.json and
to its AGENTS.md tier table, never to this hook. The hub's spec gate holds the two equal.

The maintainer allows the tier for a session by launching it with FLEET_ALLOW_HIGHEST=1 in the
environment. A hook runs as its own process, so an agent cannot set it for itself, and the deny
Expand Down
5 changes: 5 additions & 0 deletions spec/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,11 @@ def model_tier_errors(root):
f"spec/model-tiers.json: tool {tool!r} must map exactly the declared tiers"
)
continue
if not all(isinstance(m, str) and m for m in models.values()):
errors.append(
f"spec/model-tiers.json: tool {tool!r} must map each tier to a non-empty model name"
)
continue
rows.append(f"| {entry['name']} | " + " | ".join(f"`{models[t]}`" for t in tiers) + " |")
agents_path = root / "AGENTS.md"
if not agents_path.is_file():
Expand Down
28 changes: 15 additions & 13 deletions tests/test_spec_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,21 +351,17 @@ class ModelTierCase(unittest.TestCase):
def setUp(self) -> None:
self.root = Path(self.enterContext(tempfile.TemporaryDirectory()))
(self.root / "spec").mkdir()
self.models: dict[str, object] = {
"highest": "fable",
"frontier": "opus",
"standard": "sonnet",
"bulk": "haiku",
}
self.config: dict[str, object] = {
"tiers": ["highest", "frontier", "standard", "bulk"],
"maintainerOnly": "highest",
"defaultTier": "frontier",
"tools": {
"claude-code": {
"name": "Claude Code",
"models": {
"highest": "fable",
"frontier": "opus",
"standard": "sonnet",
"bulk": "haiku",
},
}
},
"tools": {"claude-code": {"name": "Claude Code", "models": self.models}},
}

def errors(self, table: str = TABLE) -> list[str]:
Expand All @@ -390,11 +386,17 @@ def test_rejects_the_maintainer_only_tier_as_the_default(self) -> None:
self.assertTrue(any("defaultTier" in e for e in self.errors()))

def test_rejects_a_tool_missing_a_tier(self) -> None:
tool = self.config["tools"]["claude-code"] # type: ignore[index]
del tool["models"]["bulk"]
del self.models["bulk"]

self.assertTrue(any("exactly the declared tiers" in e for e in self.errors()))

def test_rejects_a_model_that_is_not_a_non_empty_string(self) -> None:
for value in (None, 7, ""):
with self.subTest(value=value):
self.models["highest"] = value

self.assertTrue(any("non-empty model name" in e for e in self.errors()))

def test_rejects_a_tools_list_instead_of_an_object(self) -> None:
self.config["tools"] = []

Expand Down
Loading