From 08e3e4a6fb15cc45d5757da09dcb11cee5f7663b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 27 Oct 2026 12:05:03 -0700 Subject: [PATCH 1/2] Reject a Tier Sharing the Maintainer-Only Model and State the Allowance as Conduct The validator rejects a tool mapping where another tier names the maintainer-only model, compared case-insensitively as the guard compares, since the guard would then refuse every dispatch at that tier. Requirement 10 states that setting the allowance is the maintainer's act and that an agent never doing it is conduct the hook does not enforce. Co-Authored-By: Claude Opus 5.5 --- host-setup/agent-safety/README.md | 9 +++++---- spec/validate.py | 7 +++++++ tests/test_spec_validate.py | 5 +++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/host-setup/agent-safety/README.md b/host-setup/agent-safety/README.md index 51dcdf6f..fe678148 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 f6966016..4da7ec97 100755 --- a/spec/validate.py +++ b/spec/validate.py @@ -740,6 +740,13 @@ 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 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 c2e2bb42..3798c634 100755 --- a/tests/test_spec_validate.py +++ b/tests/test_spec_validate.py @@ -397,6 +397,11 @@ 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_rejects_a_tools_list_instead_of_an_object(self) -> None: self.config["tools"] = [] From 129b8564096c36769df6b4163fffb5d64fa16cc6 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 27 Oct 2026 12:18:04 -0700 Subject: [PATCH 2/2] Report a Non-String maintainerOnly Rather Than Raising on It Co-Authored-By: Claude Opus 5.5 --- spec/validate.py | 8 ++++++-- tests/test_spec_validate.py | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/spec/validate.py b/spec/validate.py index 4da7ec97..8e4f5c7b 100755 --- a/spec/validate.py +++ b/spec/validate.py @@ -740,8 +740,12 @@ 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 reserved in models and any( - t != reserved and m.lower() == models[reserved].lower() for t, m in models.items() + 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" diff --git a/tests/test_spec_validate.py b/tests/test_spec_validate.py index 3798c634..83566dce 100755 --- a/tests/test_spec_validate.py +++ b/tests/test_spec_validate.py @@ -402,6 +402,11 @@ def test_rejects_a_tier_sharing_the_maintainer_only_model(self) -> None: 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"] = []