diff --git a/host-setup/agent-safety/README.md b/host-setup/agent-safety/README.md index 51dcdf6fe..fe6781482 100644 --- a/host-setup/agent-safety/README.md +++ b/host-setup/agent-safety/README.md @@ -111,10 +111,11 @@ Requirements 1 to 7 judge a command's text, read the way a simple lexer reads it instruction set carried no tier table. It was taken as "the frontier model", and the work ran on the most capable and most expensive model nobody had asked for. Read the tier mapping from the hub's `spec/model-tiers.json`, deployed beside the hook. Refuse a dispatch naming the - maintainer-only tier's model unless the session was launched with that tier allowed. An agent - never sets the allowance, for its own session or for one it launches. A mapping the hook cannot - read refuses every dispatch naming a model, since a guard that cannot tell what to block must - not go quiet. A session launched with the allowance blocks no model, so it needs no mapping. + maintainer-only tier's model unless the session was launched with that tier allowed. Setting the + allowance is the maintainer's act. An agent never does it, for its own session or for one it + launches, and that is conduct the hook does not enforce. A mapping the hook cannot read refuses + every dispatch naming a model, since a guard that cannot tell what to block must not go quiet. A + session launched with the allowance blocks no model, so it needs no mapping. This one guards the maintainer's choice of model and his quota rather than the credentials or the machine. It sits in the host layer all the same, because a dispatch happens inside the diff --git a/spec/validate.py b/spec/validate.py index f6966016b..8e4f5c7ba 100755 --- a/spec/validate.py +++ b/spec/validate.py @@ -740,6 +740,17 @@ def model_tier_errors(root): f"spec/model-tiers.json: tool {tool!r} must map each tier to a non-empty model name" ) continue + if ( + isinstance(reserved, str) + and reserved in models + and any( + t != reserved and m.lower() == models[reserved].lower() for t, m in models.items() + ) + ): + errors.append( + f"spec/model-tiers.json: tool {tool!r} must give maintainerOnly a model no other tier shares" + ) + 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(): diff --git a/tests/test_spec_validate.py b/tests/test_spec_validate.py index c2e2bb42c..83566dcee 100755 --- a/tests/test_spec_validate.py +++ b/tests/test_spec_validate.py @@ -397,6 +397,16 @@ def test_rejects_a_model_that_is_not_a_non_empty_string(self) -> None: self.assertTrue(any("non-empty model name" in e for e in self.errors())) + def test_rejects_a_tier_sharing_the_maintainer_only_model(self) -> None: + self.models["frontier"] = "Fable" + + self.assertTrue(any("no other tier shares" in e for e in self.errors())) + + def test_reports_a_maintainer_only_value_that_is_not_a_string(self) -> None: + self.config["maintainerOnly"] = [] + + self.assertTrue(any("maintainerOnly" in e for e in self.errors())) + def test_rejects_a_tools_list_instead_of_an_object(self) -> None: self.config["tools"] = []