Skip to content
Draft
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
17 changes: 17 additions & 0 deletions aiac/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,23 @@ The property that grants on a given scope come from exactly one place — the
scope-focal pass. Door B adds only prohibitions and never grants.
_Avoid_: owner, source of truth.

**Scope-exclusivity**:
Exclusive/restrictive language centred on a **scope** ("Only developers may read
source") — it closes that scope to a grantee role set. Owned by the
**scope-focal pass** (the subject gate): it grants the named roles and denies the
complement of candidate roles. Distinct from **role-exclusivity**.
_Avoid_: treating it as a Door B (role-focal) deny trigger.

**Role-exclusivity**:
Exclusive/restrictive language centred on a user **role** ("Testers may access
**only** issues") — it closes that role to a scope set. Owned by **Door B** (the
user-role-focal pass): it denies the complement of candidate scopes. Conflating
it with **scope-exclusivity** — treating a scope-exclusive statement about a
*different* role as grounds for a Door B deny on the focal role — is the category
error #2511 corrects: that complement belongs to the scope-focal pass, so an
absent Door B prohibition is correct, not a defect.
_Avoid_: role pass exclusivity, subject exclusivity.

**Contradiction**:
An *intra-pass* grant∩deny: one focal's own proposed rule set both grants and
prohibits the same candidate. Detected by the LLM auditor within a single pass,
Expand Down
18 changes: 15 additions & 3 deletions aiac/src/aiac/agent/policy_rules_builder/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,27 @@ def _policy_block(policy_text: str) -> str:
+ _MAPPING_RULES
+ _DENY_RULES
)
# The MISSING-PROHIBITION guardrail below is AUDITOR-ONLY on purpose (#2511): it governs the
# auditor's REJECTION decision (when an absent focal deny is a defect vs. correct), not the grant/deny
# GENERATION the two sides share. The auditor was over-reaching — demanding a focal-role deny from a
# scope-exclusive statement that names a DIFFERENT role, then rejecting the proposer's (correct) empty
# proposal to exhaustion (-> 422). The proposer already applies rule 4 correctly, so the shared rules
# are left UNTOUCHED: putting this "empty is correct, don't reject" reassurance there regressed the
# proposer (it under-emitted legitimate description-driven denies). It belongs here alone.
_AUDITOR_SYSTEM = (
"You audit a proposed set of grants and prohibitions. Approve only if every granted pair is "
"policy-supported — REJECT any grant unsupported by the policy or the descriptions, any grant in "
"a domain the candidate is not shown to act in, and any grant for a candidate the policy never "
"mentions. Every prohibited pair must be a genuine explicit-prohibition or exclusivity deny, the "
"exclusivity flag must be truly asserted by the SCENARIO policy, and for a purely permissive "
"policy both denied lists must be empty. When a candidate is named in BOTH lists (a conflict), "
"adjudicate it: a genuine grant-and-prohibit collision is a contradiction (report it), a mere "
"proposer slip is an ordinary rejection.\n" + _SAFETY + _MAPPING_RULES + _DENY_RULES
"policy both denied lists must be empty. Do NOT reject a proposal merely because it lists NO "
"prohibition on the focal entity: an absent focal prohibition is a defect ONLY when the SCENARIO "
"policy is exclusive/restrictive about the focal entity ITSELF (rule 6) or explicitly prohibits "
"it (rule 5). A restriction that names a DIFFERENT entity — e.g. 'Only <other role> may <verb> "
"<scope>' — is another pass's concern (rule 4); an empty prohibition list is then CORRECT and "
"must be approved, never rejected for a missing deny. When a candidate is named in BOTH lists (a "
"conflict), adjudicate it: a genuine grant-and-prohibit collision is a contradiction (report it), "
"a mere proposer slip is an ordinary rejection.\n" + _SAFETY + _MAPPING_RULES + _DENY_RULES
)


Expand Down
38 changes: 38 additions & 0 deletions aiac/test/agent/policy_rules_builder/test_graph_live_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,41 @@ def test_door_b_exclusivity_complement_denies_only():
("source-read", DENY),
("source-write", DENY),
}


# --------------------------------------------------------------------------- #
# Slice 7 — Door B must NOT raise a spurious 422 on SCOPE-exclusivity (#2511). #
# The policy is scope-exclusive about a DIFFERENT role ("Only developers may #
# read and modify source"); the focal role (rossoctl-admin) is a non-grantee #
# user role the policy never names, and is GRANTED nothing here. The real bug #
# #2511 fixes is the AUDITOR rejecting the proposer's proposal to exhaustion #
# (PolicyRulesBuilderError → 422) on the theory that a focal deny is "missing" — #
# it is not: that source deny is the SUBJECT gate's job (the scope-focal pass), #
# never Door B's. What this slice PINS is therefore the negative property, not #
# an exact set: the call must NOT raise, must emit NO ALLOW (Door B is #
# deny-only), and any deny it does emit must stay within the exclusive scopes #
# ({source-read, source-write}) — never leaking onto `issues`, a scope the focal #
# role has no relationship to. A duplicate DENY on the source scopes is harmless #
# here (rossoctl-admin is granted nothing, so nothing collides); the 422 is the #
# only defect. Contrast slice 6, which pins the genuine role-exclusivity deny. #
# --------------------------------------------------------------------------- #
def test_door_b_no_overreach_on_scope_exclusivity():
admin = _role("r-adm", "rossoctl-admin", "A realm administrator, not a developer.")
issues = _scope("s-iss", "issues", "Access the issue tracker.")
source_read = _scope("s-sr", "source-read", "Read source code from the repository.")
source_write = _scope("s-sw", "source-write", "Write and modify source code in the repository.")

# Scope-exclusive about `developers`, not about the focal role — bare prose, no enumerated
# exclusions, no default-effect scaffolding (the point of the #2504 minimization).
policy = "Only developers may read and modify source."

# Must not raise: a PolicyRulesBuilderError (→ 422) from auditing the proposal to exhaustion is
# exactly the #2511 defect and would fail the call outright.
rules = _role_denies(policy, admin, [issues, source_read, source_write])

effects = _scope_effects(rules)
# No ALLOW — Door B is a DENY-only pass; an ALLOW would be a real defect.
assert all(effect is DENY for _, effect in effects), effects
# Any deny stays within the exclusive scopes; `issues` (unrelated to the focal role) is never
# denied. A duplicate source deny is a harmless echo of the scope-focal pass, not a conflict.
assert {name for name, _ in effects} <= {"source-read", "source-write"}, effects
Loading