Repository navigation
fix: use evaluated aggregation restrictions in context decision obligations - #3523
Conversation
…obligations Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
|
Welcome to the Agent Governance Toolkit! Thanks for your first pull request. |
🤖 AI Agent: test-generator — `agent-governance-python/agent-os/src/agent_os/policies/context_accumulation.py`
|
🤖 AI Agent: contributor-guide — View details
Welcome, and thank you for your contribution! 🎉 Great job updating the code to improve context aggregation logic—your changes are clear and focused. Before merging, please ensure:
Refer to CONTRIBUTING.md for guidance. |
🤖 AI Agent: code-reviewer — View details
TL;DR: 0 blockers, 0 warnings. Clean change; fixes aggregation logic and adds comprehensive tests.
No action items. Clean change. |
🤖 AI Agent: docs-sync-checker — Docs Sync
Docs Sync
|
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: MEDIUM
Automated check by AGT Contributor Check. |
There was a problem hiding this comment.
Pull request overview
Updates the Python CAG next-action decision path so host obligations reflect aggregation-evaluated restrictions (including rule-derived restrictions), aligning with the accumulated-context governance model in agent_os.policies.
TL;DR: 1 blockers, 0 warnings. Fix #1 and this ships.
| # | Sev | Issue | Where |
|---|---|---|---|
| 1 | Block | Gating still checks env.restrictions, so rule-added restrictions can be missed; floor gating can emit empty obligations |
decide_next() / context_accumulation.py |
Changes:
- Use evaluated aggregation restrictions (
agg.restrictions) when emittingconstrainobligations.
The one-line fix had no regression test. The existing floor test uses a single label, so the rule never fires and agg.restrictions stays empty -- which is why it passed either way and the bug went unnoticed. Adds the case that actually discriminates: labels satisfying the rule with nothing yet accumulated onto the envelope, where the old code returned CONSTRAIN with an empty obligation set. Plus a case pinning that reading obligations off the aggregation result never drops an existing envelope restriction. Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com>
🤖 AI Agent: security-scanner — View details
No security issues found. |
|
Two corrections to this PR and a test it was missing. RetitledThe old title was Now The test that was missingThe body previously said local test infra was unavailable, which was true — The reason the bug survived is worth stating: The discriminating case is an envelope whose labels do satisfy the rule but where no The first is the real defect: the caller is told On safety of the changeI wanted to be sure swapping Not touched
|
…ion set Addresses the review on microsoft#3523. The first commit changed only the obligations to read agg.restrictions and left the gate reading env.restrictions, which was inconsistent and left a hole: a rule that adds a gating token without lifting sensitivity to the floor put the token in agg but not yet in env, so neither trigger fired and the action was allowed. Both now read the evaluated set. Because evaluate_aggregation seeds from env.restrictions the result is a superset, so this can only gate more, never less. Separately, a floor-triggered CONSTRAIN on an envelope with no recorded restrictions returned an empty obligation set -- a no-op for any host that enforces through obligations. The action's gating token is the constraint being applied in that case, so it is now named. Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com>
|
Thanks — that's exactly the pointer I needed. Added in I followed the Three things it records that aren't obvious from the diff: The containment argument, stated once. Defect 1 needs one ordinary rule, not a contrived one. A rule that adds a gating token without lifting sensitivity to the floor satisfies neither trigger — the token is in Which tests fail on base. I reverted only
|
|
One note on verifying it: the The other 10 checks are green on the new head. |
- Geography default fail-open: require explicit geography match when policy.required_geography is set; absent geography no longer bypasses. - Empty all_labels rejected: AggregationRule.__post_init__ raises ValueError when all_labels is empty, preventing silent backstop defeat. The restriction-gating gap (decide_next reading env.restrictions) is covered by microsoft#3523 and intentionally left out of this PR. Each fix ships with a regression test that would fail without the change, plus a security audit doc under docs/security/audits/. Signed-off-by: Jainil Gosalia <jainil.gosalia@gmail.com>
- Geography default fail-open: require explicit geography match when policy.required_geography is set; absent geography no longer bypasses. - Empty all_labels rejected: AggregationRule.__post_init__ raises ValueError when all_labels is empty, preventing silent backstop defeat. The restriction-gating gap (decide_next reading env.restrictions) is covered by #3523 and intentionally left out of this PR. Each fix ships with a regression test that would fail without the change, plus a security audit doc under docs/security/audits/. Signed-off-by: Jainil Gosalia <jainil.gosalia@gmail.com>
Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com>
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
Ran this one both ways, because the value of a fail-closed fix is entirely in whether its tests fail
without it.
PR source + PR tests -> 10 passed
main source + PR tests -> 4 failed, 6 passed
The four that fail against main are test_obligations_keep_envelope_restrictions_and_add_new_ones,
test_rule_added_restriction_gates_below_the_floor,
test_floor_gated_decision_always_names_an_obligation, and one more. So these are genuine regression
tests, not tests written to describe the new code.
The superset argument holds and it is the load-bearing claim. I checked evaluate_aggregation in
context_aggregation.py:
restrictions: set[str] = set(env.restrictions)
for rule in ruleset.rules:
if rule.all_labels <= env.labels:
restrictions |= set(rule.adds_restrictions)It seeds from env.restrictions and only ever unions, so agg.restrictions ⊇ env.restrictions
unconditionally. Your comment's "it can only ever gate more, never less" is exactly right, and that
is what makes reading the evaluated set safe rather than merely different. Without that property this
change could silently drop an obligation, which is why it is worth having stated in the code.
The bug is real and it is the nastier of the two shapes. A rule that adds a gating restriction
without lifting sensitivity to the floor produced neither trigger: the token was in agg but not yet
in env, and the floor never fired. That is a rule that does exactly what a policy author wrote and
has no effect, which is worse than a rule that errors.
The second fix is the one I would highlight. When the floor fires on an envelope with no recorded
restrictions, the old code emitted CONSTRAIN with an empty ObligationSet. For any host that
enforces through obligations that is a constrain instruction with nothing to satisfy, which either
no-ops or blocks with no remedy depending on how the host reads it. Adding the gating token so the
decision always names at least one obligation closes an ambiguity that a caller could not have
resolved. Your test comment says it well: "the caller is told to constrain the action without being
told what to satisfy".
Two mechanical things before this can merge.
It is CONFLICTING and 55 commits behind main.
Spell-check changed files is genuine, one word, in your own added line:
:34:63 - Unknown word (unioning)
That is from the test comment "before unioning rule restrictions". A # cspell:ignore unioning, or
rewording to "before merging in rule restrictions", clears it. Worth knowing it is real, because
several other PRs in this queue show the same check red purely from a stale-base over-scan and this
is not one of them.
Nothing blocking on the fix.
|
MohammadHaroonAbuomar liamcrumm flagging this one. It closes two fail-opens in context-accumulation I ran the tests both ways: 10 pass with the fix, 4 fail against |
Resolve conflict in the sibling fail-open-closure audit frontmatter by taking upstream main. Rephrase "unioning" in the context-accumulation test comment so spell-check passes. Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com> Co-authored-by: hari <harikp2002@gmail.com>
Resolve conflict in the sibling fail-open-closure audit frontmatter by taking upstream main. Rephrase "unioning" in the context-accumulation test comment so spell-check passes. Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com> Co-authored-by: hari <harikp2002@gmail.com>
01cf5bc to
5a10d30
Compare
|
Merged upstream Please re-review when you have a chance. |
|
Re-triggering CI against current main (the earlier run predates the typing-extensions pin fix in #3928). Reopening now. |
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
Approved after group integration review (tests, gates and adversarial pass on the combined change).
c4103c0
into
microsoft:main
…ations (microsoft#3523) * fix: use evaluated aggregation restrictions in constraining decision obligations Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com> * test: cover obligations for restrictions triggered during decide_next The one-line fix had no regression test. The existing floor test uses a single label, so the rule never fires and agg.restrictions stays empty -- which is why it passed either way and the bug went unnoticed. Adds the case that actually discriminates: labels satisfying the rule with nothing yet accumulated onto the envelope, where the old code returned CONSTRAIN with an empty obligation set. Plus a case pinning that reading obligations off the aggregation result never drops an existing envelope restriction. Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com> * fix: gate on evaluated restrictions and never return an empty obligation set Addresses the review on microsoft#3523. The first commit changed only the obligations to read agg.restrictions and left the gate reading env.restrictions, which was inconsistent and left a hole: a rule that adds a gating token without lifting sensitivity to the floor put the token in agg but not yet in env, so neither trigger fired and the action was allowed. Both now read the evaluated set. Because evaluate_aggregation seeds from env.restrictions the result is a superset, so this can only gate more, never less. Separately, a floor-triggered CONSTRAIN on an envelope with no recorded restrictions returned an empty obligation set -- a no-op for any host that enforces through obligations. The action's gating token is the constraint being applied in that case, so it is now named. Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com> * docs(security): add the audit record for the context-accumulation gate fix The security-audit-required gate flags any change under agent_os/policies/. Records the two fail-open defects, the monotone direction of both fixes, and which regression tests fail on base. Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com> --------- Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com> Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com>
…ations (microsoft#3523) * fix: use evaluated aggregation restrictions in constraining decision obligations Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com> * test: cover obligations for restrictions triggered during decide_next The one-line fix had no regression test. The existing floor test uses a single label, so the rule never fires and agg.restrictions stays empty -- which is why it passed either way and the bug went unnoticed. Adds the case that actually discriminates: labels satisfying the rule with nothing yet accumulated onto the envelope, where the old code returned CONSTRAIN with an empty obligation set. Plus a case pinning that reading obligations off the aggregation result never drops an existing envelope restriction. Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com> * fix: gate on evaluated restrictions and never return an empty obligation set Addresses the review on microsoft#3523. The first commit changed only the obligations to read agg.restrictions and left the gate reading env.restrictions, which was inconsistent and left a hole: a rule that adds a gating token without lifting sensitivity to the floor put the token in agg but not yet in env, so neither trigger fired and the action was allowed. Both now read the evaluated set. Because evaluate_aggregation seeds from env.restrictions the result is a superset, so this can only gate more, never less. Separately, a floor-triggered CONSTRAIN on an envelope with no recorded restrictions returned an empty obligation set -- a no-op for any host that enforces through obligations. The action's gating token is the constraint being applied in that case, so it is now named. Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com> * docs(security): add the audit record for the context-accumulation gate fix The security-audit-required gate flags any change under agent_os/policies/. Records the two fail-open defects, the monotone direction of both fixes, and which regression tests fail on base. Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com> --------- Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com> Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com> Signed-off-by: yuvrajsingh2428 <offcyuvi2428@gmail.com>
A one-line correctness fix in the Python accumulated-context policy, plus the regression tests it was missing.
The bug
decide_next()built its obligations fromenv.restrictions— the restrictions already folded onto the envelope — rather thanagg.restrictions, the freshly evaluated set. A rule that fires during this very call has its restriction inaggbut not yet inenv, so the caller was told toCONSTRAINwhile being handed an empty obligation set: gated, with nothing to satisfy.A second, related hole came out of review (thanks Copilot — this was a good catch). The gate itself still read
env.restrictions:So a rule that adds a gating token without lifting sensitivity to the
RESTRICTEDfloor triggered neither condition — token present inagg, absent fromenv, sensitivity below the floor — and the action was allowed. Both the gate and the obligations now read the evaluated set.This is safe in the fail-closed direction:
evaluate_aggregationseedsrestrictionsfromenv.restrictionsbefore unioning matching rules (context_aggregation.py:72), soagg.restrictionsis always a superset. It can gate more, never less, and an existing envelope restriction can never be dropped.Third: a floor-triggered
CONSTRAINon an envelope with no recorded restrictions still returned an empty obligation set, which is a no-op for any host that enforces through obligations. The action's own gating token is the constraint being applied there, so it is now named.Why the existing tests missed it
test_floor_triggers_flow_action_without_explicit_restrictionuses{"pii"}alone, sopii_financial_restrictednever fires,agg.restrictionsstays empty, andenv.restrictionsvsagg.restrictionsis indistinguishable. It passes either way.Tests
Five added, each verified to fail with its own fix reverted and pass with it applied:
test_floor_gated_decision_reports_newly_triggered_restrictionsassert set() == {'no_external_export'}test_obligations_keep_envelope_restrictions_and_add_new_onesassert {'no_print'} == {'no_external_export', 'no_print'}test_rule_added_restriction_gates_below_the_floorALLOWwhereCONSTRAINis requiredtest_floor_gated_decision_always_names_an_obligationassert set() == {'no_external_export'}test_unrelated_action_is_not_gated_by_someone_elses_restriction10 passed.ruff format --checkclean on both files.Not touched
ruffreportsUP035(typing.Iterable) andUP042(str, Enum) incontext_accumulation.pyunder this package's own config. Both predate this branch. Left alone rather than widening a bug fix; happy to send separately.This change was prepared with AI assistance under human direction.