-
Notifications
You must be signed in to change notification settings - Fork 40
Feat: Unify policy-conflict detection on /apply, retire /policy/check (#2500-#2504) #842
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
anatolykoyfman
wants to merge
14
commits into
rossoctl:main
Choose a base branch
from
s-and-p-team:unified_apply_check
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
f4203be
Fix: Surface unjoinable auditor contradictions instead of dropping them
anatolykoyfman 8f78747
Merge remote-tracking branch 'upstream/main' into policy_conflict_dia…
anatolykoyfman be30ded
Refactor: Retire /policy/check route, re-home diagnostic as library (…
anatolykoyfman e306775
Feat: Add Door B user-role-focal deny-only pass (#2501)
anatolykoyfman ae34bcf
Feat: Add inline structural conflict detection with atomic raise (#2502)
anatolykoyfman abbc4f8
Feat: Surface rich ConflictReport on /apply with unified 422 (#2503)
anatolykoyfman fa6ce09
Feat: Detect cross-service policy conflicts + integration OPA-loop te…
anatolykoyfman fdda9b5
Docs: Add AIAC domain glossary (CONTEXT.md)
anatolykoyfman 52db817
Fix: Address CodeRabbit review on #842 (enrichment fallback, empty-qu…
anatolykoyfman 94c97d9
Fix: Address clawgenti review on #842 (log enrichment failures, doc p…
anatolykoyfman eca35ed
Docs: Correct /apply 422 banner to cover both conflict paths (#842)
anatolykoyfman 5907ea8
Fix: Address clawgenti review on #842 (F2 focal-prefix coupling, F4 f…
anatolykoyfman 9ab1334
Test: xfail the unconfirmed cross-service integration scenario (#842 F5)
anatolykoyfman 6216bae
Fix: Redact store-read exception from cross-service 502 detail (CWE-209)
anatolykoyfman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # AIAC | ||
|
|
||
| The AIAC agent turns natural-language authorization policy into applied | ||
| PolicyRules. It surveys IdP roles and scopes, drives an LLM-backed Policy Rules | ||
| Builder over focal entities, and computes/applies the resulting rules through a | ||
| two-layer policy stack. This glossary fixes the vocabulary for how the builder | ||
| grants, prohibits, and reports collisions. | ||
|
|
||
| ## Language | ||
|
|
||
| **Focal entity**: | ||
| The single role or scope a Policy Rules Builder pass is centred on for one | ||
| `build()` run. Every pass fans candidates against exactly one focal per run. | ||
| _Avoid_: subject, principal, target. | ||
|
|
||
| **Scope-focal pass**: | ||
| The pass centred on a scope, fanning candidate roles over it. It is the sole | ||
| **grant authority** for that scope. | ||
| _Avoid_: scope pass, forward pass. | ||
|
|
||
| **User-role-focal pass** (a.k.a. **Door B**): | ||
| A pass centred on a `kind=User` role, fanning it over the focus service's own | ||
| scopes to emit the **deny** rules that a user's exclusivity ("Testers may access | ||
| **only** issues") implies — prohibitions the scope-focal pass structurally | ||
| cannot express. Contributes denies only; never broadens access. | ||
| _Avoid_: role pass (ambiguous with the agent-role-focal pass), Door B pass. | ||
|
|
||
| **Grant authority**: | ||
| 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. | ||
|
|
||
| **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, | ||
| which fails that pass closed. Modelled by `Contradiction` / raised as | ||
| `PolicyContradictionError`. | ||
| _Avoid_: using "conflict" for this — the two are distinct. | ||
|
|
||
| **Conflict**: | ||
| A *cross-pass* grant∩deny: an `Allow` from one pass and a `Deny` from another on | ||
| the **same `(role, scope)`** pair. Structural (a pure id-level allow∩deny | ||
| set-intersection over the assembled rules), not LLM-audited. Modelled by | ||
| `Conflict` / `ConflictReport`. | ||
| _Avoid_: using "contradiction" for this. | ||
|
|
||
| **Within-batch conflict**: | ||
| A **conflict** whose two rules are produced in one `build()` call — i.e. one | ||
| `/apply` request. This is the Door B case: at the focus service's own-scope | ||
| onboarding, both the scope-focal grant and the Door B deny (and any collision | ||
| between them) are in hand in the same build. In scope. | ||
| _Avoid_: intra-request conflict. | ||
|
|
||
| **Cross-run conflict** (a.k.a. **cross-service conflict**): | ||
| A **conflict** whose two rules are produced in separate onboarding requests and | ||
| collide only in the persisted SPM store. Surfaced at `/apply` by the | ||
| cross-service check, which reads the already-applied rules of the services that | ||
| own the touched scopes and folds them into detection; the pure within-build | ||
| structural pass alone does not see it. | ||
| _Avoid_: cross-request conflict, store conflict. | ||
|
|
||
| **Identify-never-reconcile**: | ||
| The governing principle: a `(role, scope)` carrying both an `Allow` and a `Deny` | ||
| **is** a conflict — surface it, never resolve it. No precedence, no | ||
| "deny wins," no merge. See `docs/adr/0001-identify-never-reconcile.md`. | ||
| _Avoid_: deny-overrides, conflict resolution. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # Identify policy conflicts; never reconcile them | ||
|
|
||
| When the assembled PolicyRules for a service carry both an `Allow` and a `Deny` | ||
| on the same `(role, scope)` pair (a **conflict**), the Policy Rules Builder | ||
| **surfaces** it and refuses to apply — it never picks a winner. We deliberately | ||
| reject deny-overrides, allow-overrides, precedence ordering, and silent merging: | ||
| a conflict means the policy prose is genuinely ambiguous, and resolving it in | ||
| code would bury that ambiguity behind a rule the author never stated. | ||
|
|
||
| ## Status | ||
|
|
||
| accepted | ||
|
|
||
| ## Consequences | ||
|
|
||
| - There is a **single entry point, `/apply`**: no conflict → rules are built and | ||
| applied; conflict → an exception is raised and nothing is applied. A separate | ||
| read-only `/policy/check` is **not** part of this model. | ||
| - Detection is a pure `(role.id, scope.id)` allow∩deny set-intersection over the | ||
| assembled `list[PolicyRule]`, run **inside the build** before any compute/apply | ||
| — so a conflict leaves persisted state untouched (atomic-by-construction). | ||
| - A found conflict is raised as a single `ConflictReport`-carrying exception and | ||
| mapped to HTTP 422 with the structured report as the body. | ||
| - Scope is **within one service's build** (Q13). Cross-service conflicts — rules | ||
| written by different `build()` calls colliding only in the persisted store — | ||
| were originally left as a follow-up gap here; they are now detected (still | ||
| identify-never-reconcile) — see the `#2504` addendum below. | ||
| - The intra-pass `PolicyContradictionError` (the LLM auditor's grant∩deny within | ||
| one pass) is a separate, disjoint mechanism and keeps failing that pass closed; | ||
| it is not merged into the cross-pass detector, only re-shaped to the same 422 | ||
| report body at the boundary (Q15). | ||
|
|
||
| ## Addendum (#2503): verbatim-quoted reports on /apply — reversing handoff-07 Q15/Q16 | ||
|
|
||
| Handoff 07 settled the on-`/apply` conflict report as **quote-less / no-LLM**: | ||
|
|
||
| - **Q15** ("Boundary unification") decided to *"unify the report shape, not the | ||
| payload"* — one new structural exception carrying a `ConflictReport`, and | ||
| mapping `PolicyContradictionError` to that shape *"(shallow, **no LLM**)"* at | ||
| the 422 handler. | ||
| - **Q16** ("`Conflict.focal` for a structural conflict") anchored the structural | ||
| conflict on the **SCOPE** side (`FocalType.SCOPE`) and, together with #2502's | ||
| structural detector, produced each `Conflict` with **empty | ||
| `granting_quotes`/`prohibiting_quotes` and `quotes_verified=False`** — a | ||
| deterministic, LLM-free report. | ||
|
|
||
| **#2503 reverses the quote-less / no-LLM decision for the structural path.** The | ||
| `/apply` conflict report is now the **rich, verbatim-quoted** `ConflictReport`: | ||
| when — and **only when** — the deterministic `detect_conflicts` finds a structural | ||
| conflict, an LLM explain/quote pass (`conflict_enrichment.enrich_report`, reusing | ||
| the re-homed diagnostic `explain` machinery) runs over exactly the pairs the | ||
| detector surfaced, classifying each `kind` (`direct`/`coarse_scope`) and | ||
| extracting **substring-validated** quotes from the candidate policy text. A clean | ||
| apply stays fast and **LLM-free** (the explain seam never fires), so the gating — | ||
| not the report's fidelity — is what preserves handoff 07's performance intent. | ||
|
|
||
| Unchanged from handoff 07: the SCOPE-side focal anchoring (Q16), the identify- | ||
| never-reconcile principle above, and Q15's *shape* unification — both | ||
| `PolicyConflictError` (now enriched) and `PolicyContradictionError` (mapped | ||
| shallow, still **no LLM** at the boundary, `quotes_verified=false`) yield one 422 | ||
| `ConflictReport` body. On any quote-validation failure the conflict is **kept** | ||
| with `quotes_verified=false` and a description fallback — never dropped. | ||
|
|
||
| ## Addendum (#2504): cross-service conflicts are detected (closing the Q13 gap) | ||
|
|
||
| The Q13 consequence above scoped detection to **one service's build** and left | ||
| cross-service conflicts — an `Allow` in the current build colliding with a `Deny` | ||
| another service already persisted (or vice versa) on the same `(role.id, | ||
| scope.id)` — as a follow-up gap. `#2504` closes that gap **without** changing the | ||
| principle: still identify-never-reconcile, still no precedence/merge. | ||
|
|
||
| `ServicePolicyBuilder.build` now widens the detector's input to the **combined** | ||
| state — this build's assembled rules **plus** the already-applied inbound rules of | ||
| the other services that own the touched scopes, read from the Policy Store | ||
| (`applied_rules_for_scopes`). The **same** `#2502` `(role.id, scope.id)` | ||
| allow∩deny intersection then surfaces an overlap that a single build's own rules | ||
| could never reveal. The store read is **read-only** and the raise still happens | ||
| **before** `compute_and_apply`, so the atomic-by-construction guarantee holds: a | ||
| cross-service conflict leaves persisted state untouched. Detection stays | ||
| order-independent (keyed on ids), so tool-first vs agent-first onboarding yields | ||
| the identical outcome, and the result is emitted in the same 422 `ConflictReport` | ||
| shape. (The single-writer basis of "atomic-by-construction" is unchanged; | ||
| transactional safety across *concurrent* applies remains a separate follow-up.) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.