Repository navigation
fix(agent-os): reject supervisors registered above the deterministic trust root - #3510
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
🤖 AI Agent: security-scanner — View details
No security issues found. |
🤖 AI Agent: test-generator — View details
Test coverage looks good. No gaps identified. |
🤖 AI Agent: docs-sync-checker — Docs Sync
Docs Sync
|
🤖 AI Agent: code-reviewer — View details
TL;DR: 0 blockers, 0 warnings. Fix correctly addresses critical security gaps in trust root validation.
No action items required. Clean change. |
🤖 AI Agent: contributor-guide — View details
Welcome, and thank you for your detailed contribution! You did a great job thoroughly testing the changes to ensure compatibility and correctness. Before merging, please verify that all new violation messages are consistent with existing error phrasing for clarity. Refer to CONTRIBUTING.md for guidance. |
🤖 AI Agent: breaking-change-detector — API Compatibility
API Compatibility
These changes introduce stricter validation rules, which may break existing code that relied on the previously lenient behavior. |
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
|
🔴 Contributor Check: HIGH
Automated check by AGT Contributor Check. |
There was a problem hiding this comment.
Pull request overview
TL;DR: 1 blocker, 1 warning. Fix #1 and this ships.
This PR tightens agent-os supervisor hierarchy invariants by rejecting supervisors registered “above” the deterministic trust root (negative levels) and by hardening TrustRoot.validate_supervisor against type-confusion inputs; it also adds regression tests to lock the behavior in.
Changes:
- Enforce
levelas a realint(excludingbool) and rejectlevel < 0inTrustRoot.validate_supervisor. - Emit a
validate_hierarchy()violation for supervisors registered with negative levels. - Add tests covering negative, non-integer, and boolean levels plus authority-chain implications.
| # | Sev | Issue | Where |
|---|---|---|---|
| 1 | Block | validate_hierarchy() can raise TypeError on non-int level inputs (contract is “return violations list”) |
agent_os/supervisor.py::validate_hierarchy |
| 2 | Warn | Docstring grammar typo (“reachable around”) | tests/test_trust_root.py |
#1: Treat non-integer (and bool) supervisor levels as violations and filter them out of the gap-scan max_level computation so validation fails closed without crashing.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
agent-governance-python/agent-os/src/agent_os/trust_root.py |
Tightens supervisor level validation (type + non-negative) before applying the level-0 determinism rule. |
agent-governance-python/agent-os/src/agent_os/supervisor.py |
Adds negative-level detection to hierarchy validation (but needs type-safety to avoid runtime errors). |
agent-governance-python/agent-os/tests/test_trust_root.py |
Adds regression tests for negative/non-integer/bool levels and hierarchy violations. |
…trust root The level-0 determinism rule was enforced by a single exact comparison, `if level == 0 and is_agent`, in both `TrustRoot.validate_supervisor` and `SupervisorHierarchy.validate_hierarchy`. Anything not equal to 0 passed, including every negative level -- which places an LLM agent *above* the deterministic root, the one position the rule exists to protect. `validate_hierarchy` had no check that could see a negative level at all: the determinism loop only inspects supervisors whose level is exactly 0, and the gap scan walks `range(1, max_level + 1)`, which never covers negatives. An agent registered at level -1 therefore produced an empty violation list -- the documented "valid" signal -- while ranking ahead of the trust root as final authority in `get_authority_chain`. A non-integer level slipped through the same way. `"0" == 0` is False in Python, so a string level -- exactly what a config loader that skips coercion produces -- skipped the determinism check rather than failing it. `validate_supervisor` now requires `level` to be a real `int` (`bool` is excluded explicitly, being an `int` subclass whose `False == 0`) and rejects negative levels before the determinism check. `validate_hierarchy` emits a violation for every negative level, checked first so the message names the supervisor that is there rather than the root that is missing. Previously accepted configurations are unchanged: level 0 deterministic, and agent-based supervisors at levels 1..N. Signed-off-by: LHMQ878 <LHMQ878@users.noreply.github.com>
ded7ac4 to
a1ba0eb
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
agent-governance-python/agent-os/tests/test_trust_root.py:69
- The docstring claims that any non-
intlevel "skipped the determinism check", but that wasn’t true for all values in the parametrization (e.g.,0.0 == 0andFalse == 0, so those cases did not bypass the oldlevel == 0check). Consider rewording to describe the actual failure mode (values not equal to 0, plusboolsemantics) so the test rationale stays accurate.
"""A level that is not a real ``int`` skipped the determinism check.
``"0" == 0`` is False in Python, so a string level -- exactly what a config
loader that skips coercion produces -- was never compared against the root
at all rather than failing the comparison. ``bool`` is covered here too: it
|
Rebased onto
I took main's side wholesale — its The source fix itself is unchanged — A verification caveat I should state plainly: I could not run That is pre-existing and not caused by this branch — To not leave the logic unverified, I exercised every assertion in the new tests against the rebased source directly, stubbing only the runtime (which none of these code paths call): 24 checks, all passing. Happy to reshape anything if the flat style is not what you wanted here — main had only just landed it, so I followed it rather than assume. |
|
Reviewed. The escalation reproduces on main ( One correction for the PR description: it says previously accepted configurations are unchanged, but |
|
You're right, and thanks for testing it rather than reading it — that claim in my description was wrong. Corrected above, and pinned in Confirmed the case you named, and it is broader than But a float never reached a working hierarchyThis is what decided me against loosening the check to So a config that got a float past That reasoning is now a test rather than a comment, since the next reader will have exactly your doubt: @pytest.mark.parametrize('level', [0.0, 1.0, 2.5])
def test_float_level_never_reached_a_working_hierarchy(level: float) -> None:
hierarchy = SupervisorHierarchy(trust_root=_root())
hierarchy.register_supervisor('root', level=level, is_agent=False)
with pytest.raises(TypeError, match='float'):
hierarchy.validate_hierarchy()If you would rather a YAML Verification caveat, same as I noted on #3508: Glad the |
… fires
Review feedback: the tests here covered validate_hierarchy but never called
validate_supervisor, so nothing pinned the relationship between the two layers.
register_supervisor sits between them and calls neither, which is precisely why
a level rejected at declaration time could still be governed by the hierarchy.
This adds one test over the same parametrize list the existing
validate_supervisor test uses ('0', '1', 0.0, 1.5, [0], True, False) and asserts
both answers for each value: validate_supervisor returns False, and after
register_supervisor the hierarchy reports a non-integer violation naming that
supervisor. A higher integer level is registered alongside so the value is not
the maximum, which is the shape where the gap scan never reaches it.
Mutation-checked both directions. Reverting this PR's check in
validate_hierarchy fails all 7 cases; reverting microsoft#3510's check in
validate_supervisor fails 5 of 7 (True and False still fail the determinism rule
by other means). Either layer regressing alone is now caught.
Signed-off-by: LHMQ878 <LHMQ878@users.noreply.github.com>
Fixes #3509
What was wrong
The level-0 determinism rule was enforced by a single exact comparison,
if level == 0 and is_agent, in bothTrustRoot.validate_supervisorandSupervisorHierarchy.validate_hierarchy. Anything not equal to0passed — including every negative level, which places an LLM agent above the deterministic trust root, and every non-integer level, because"0" == 0isFalsein Python.validate_hierarchyhad no check that could see a negative level at all. The determinism loop only inspects supervisors whose level is exactly0, and the gap scan walksrange(1, max_level + 1), which never covers negatives. So:An empty violation list is the API's "valid" answer (
docs/api-reference.md), while the agent occupies the slot the docstring reserves for the trust root as final authority. That contradicts the invariantdocs/owasp-agentic-top10-mapping.mdstates under ASI10:What changed
trust_root.py—validate_supervisorlevelmust be a realint.boolis excluded explicitly: it is anintsubclass andFalse == 0, so a bool level would otherwise be read as the root level.level < 0is rejected before the determinism check, regardless ofis_agent. There is no configuration under which a supervisor above the root is meaningful, so even a deterministic one there is a misconfiguration — this is not a case where an opt-out applies.supervisor.py—validate_hierarchyLevel 0 (root) has no registered supervisor, which points at the wrong thing.Behaviour table (
is_agent=Trueunless noted):level-1/-100TrueFalse-1,is_agent=FalseTrueFalse"0"/"1"/1.5/[0]TrueFalseTrueTrueFalse0FalseFalse0,is_agent=FalseTrueTrue1..NTrueTrueOne previously accepted configuration does change:
level=0.0withis_agent=Falsewas accepted byvalidate_supervisoron main and is now rejected, sinceisinstance(level, int)excludes floats. It cost nothing, because no float level could be consumed downstream anyway -- the gap scan computesrange(1, max_level + 1), which raisesTypeErroron a float. Pinned intest_float_level_never_reached_a_working_hierarchy. Otherwise unchanged: level 0 deterministic, and agent-based supervisors at levels 1..N.Compatibility
validate_supervisorbecomes stricter, so a caller that today passeslevelas a string and relies onTruewill start gettingFalse. That is the defect being fixed — a governance check returning "acceptable" for the input it exists to reject — and there are no in-repo callers passing a non-intlevel.register_supervisoris annotatedlevel: intalready, so the hierarchy side is a pure addition of a missing violation.Verification
tests/test_trust_root.py: 44 passed. With the two source files stashed, the same file is 15 failed / 29 passed — every new assertion fails onmainand passes with the fix.tests/suite (-p no:randomly --continue-on-collection-errors): 4488 passed / 257 failed, against 4464 / 257 on base — exactly +24, the new test count, with no new failures. (The 257 pre-existing failures and 3 collection errors are unrelated: modules importingagentmesh/agent_sre.)ruff checkandruff format --checkon the three touched files: identical to base (base already reportsI001+F401in the test file and would reformattrust_root.py; this branch reports the same and nothing more).cspellon all 147 added lines: clean.Tests added
TestValidateSupervisorLevelInvariant(4 methods / 16 cases) — negative levels rejected regardless ofis_agent; non-integer levels rejected;boollevels rejected with the reason stated; and a table asserting the four previously valid combinations still behave the same.TestNegativeLevelIsAViolation(5 methods / 6 cases) — an agent above the root is reported; a deterministic supervisor above the root is also reported; the authority-chain consequence is pinned so the reason the violation matters is testable, not just asserted in a comment; a negative level is reported even with no level 0 present, where the gap scan is empty; and a valid 0/1 hierarchy still returns[].Checklist
ruff check/ruff format --checkstate unchanged relative to base