Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f7f5cf1
fix(pce): always deny by default β€” remove the configurable default ef…
oblinder Sep 30, 2026
17267c1
fix(pce): serialize compute_and_apply and decommission with one PCE lock
oblinder Sep 30, 2026
a0bd81c
fix(uc1): quarantine a failed onboarding and keep it out of later runs
oblinder Sep 30, 2026
25ba751
feat(pdp): allow the MCP session messages on the agent outbound
oblinder Sep 30, 2026
f662f4d
fix(k8s): make opa-kind-enable.sh work on current operator and cortex…
oblinder Sep 30, 2026
e8cdce8
docs: describe the PCE lock, quarantine, always-DENY and the MCP session
oblinder Oct 1, 2026
2efe76e
fix(k8s): pass the authbridge plugin build tags in opa-kind-enable.sh
oblinder Oct 1, 2026
5450ef2
fix(pdp): accept a slash-bearing agent id on the by-id policy routes
oblinder Oct 1, 2026
b435aa1
docs(pdp): record that the outbound OPA denies A2A and LLM calls
oblinder Oct 1, 2026
8846b51
test(system): add the UC1 failure-rollback rung (quarantine and lift)
oblinder Oct 1, 2026
9aa0d98
fix(uc1): wait for the tool's MCP endpoint before discovery fails
oblinder Oct 1, 2026
a47e331
docs(examples): regenerate opa-team1-policy.yaml from the generator
oblinder Oct 1, 2026
a2ba263
fix(pce): drop rules for deleted services and never derive a deleted …
oblinder Oct 1, 2026
c52335c
fix(uc1): always quarantine a failed onboarding and apply the review …
oblinder Oct 1, 2026
9159cf8
fix(pce): take only the clientId, and quarantine the rollback-deleted…
oblinder Oct 1, 2026
c7a2f7d
style: ruff-format test_engine.py and use underscore emphasis in CONT…
oblinder Oct 1, 2026
7409457
fix(pce,uc1): apply the PR 227 review fixes
oblinder Oct 1, 2026
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
22 changes: 22 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,25 @@ description-only prohibition yields no durable DENY (see the PRB spec's _digeste
input retires exclusivity handling and Door B_ decision). Enforcement is tracked
in the neutrality-guard follow-up (`rossoctl/aiac`).
_Avoid_: description-driven deny, effect-in-description.

**Quarantine**:
The policy teardown of a failed onboarding, done by the PCE after the UC1
rollback disables the client. Keyed by the clientId, as every PCE operation
is. It deletes the service's SPM, removes its roles
from the other SPMs, replaces a failed agent's CR with a no-rules CR (which
denies every request), and re-derives the affected agents. A disabled client
_is_ a quarantined service: the routing guard drops every rule that touches it,
and the focal resolver gives it no candidate role or scope. Only a successful
re-onboarding lifts it. Distinct from **decommission** (the offboard teardown of
a deleted client, keyed by clientId).
_Avoid_: rollback (the IdP-side teardown that comes before it), block, ban.

**Focus service**:
The service that the current onboarding builds rules for β€” the `focus_service`
the onboarding route and the NATS consumer pass to the PCE, as its clientId
(never its Keycloak UUID). It is the one
disabled service that the routing guard and the focal resolver do not skip,
because a re-onboarding of a quarantined service builds and applies while its
client is still disabled (`reenable_service` runs after the apply).
_Avoid_: target service (a target is the service an agent calls), focal entity
(the per-pass subject inside one build).
293 changes: 54 additions & 239 deletions docs/examples/opa-team1-policy.yaml

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/specs/components/aiac-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ The Controller is a FastAPI routes layer (`controller/routes.py`). Its responsib
- Parse the trigger type and entity ID from the request path.
- Dispatch to the Service Onboarding Orchestrator (UC1) or directly to the Policy Update / Role Update sub-agents (UC2, UC3).
- Receive the `list[PolicyRule]` returned by the Orchestrator or sub-agent (already merged by the sub-agent).
- Call `compute_and_apply(merged_rules)` from `aiac.policy.computation` (PCE) once.
- Call `compute_and_apply(merged_rules)` from `aiac.policy.computation` (PCE) once. For UC1, the onboarding route and the NATS consumer also pass `override` and `focus_service` (the clientId of the service being onboarded, which `onboard_service` returns β€” not the UUID in the path), so the PCE routing guard keeps the rules of a disabled (quarantined) service that re-onboards.
- Return a bare HTTP status code to the caller; write summary and debug info to the log.

No per-use-case business logic, retry handling, or state assembly lives in the Controller. PRB calls are owned by the producing sub-agents; the Controller's shared step is the single PCE call.
Expand Down Expand Up @@ -165,7 +165,7 @@ Every sub-agent (UC1 Provision + Service Policy Builder, UC2 Build + Rebuild, UC

A `/health` reply needs a **free event loop** β€” the process being "up" is not enough. The event path therefore offloads its slow synchronous handler to a threadpool so the loop stays answerable during onboarding (see [NATS Consumer β†’ Failure isolation](#failure-isolation)). As a complementary defence, the k8s Deployment tunes the **liveness** probe tolerant β€” an explicit `timeoutSeconds` and a raised `failureThreshold` (so several consecutive slow probes, not one, must fail before a restart) β€” so a brief loop-busy window cannot kill the pod. The concrete probe values are authoritative in `k8s/agent-deployment.yaml`; this spec fixes only the intent.

The `/apply/offboard/{service_id}` path uses the `{service_id:path}` converter (slash-bearing SPIFFE-URI clientIds) and is keyed on the **clientId (SPM key)**, not the Keycloak UUID that `/apply/service/{service_id}` carries — an offboarded client is gone from `get_services()`, so UUID→clientId resolution is impossible.
The `/apply/offboard/{service_id}` path uses the `{service_id:path}` converter (slash-bearing SPIFFE-URI clientIds) and is keyed on the **clientId (SPM key)**, not the Keycloak UUID that `/apply/service/{service_id}` carries — an offboarded client is gone from `get_services()`, so UUID→clientId resolution is impossible. The PCE takes only the clientId; the onboarding resolves it from the UUID in the Orchestrator.

The `/apply/*` endpoints return bare HTTP status codes: `200 OK` on success (no response body), and the status codes from the Error Handling table on upstream failure. Success responses carry no body; upstream failures and PRB exceptions are raised as FastAPI `HTTPException`s, so error responses carry a sanitized JSON error body (`{"detail": <safe summary>}`; see [Error Handling β†’ Sanitized body vs. full log](#sanitized-body-vs-full-log)) alongside the status code. Summary, applied-rule details, and debug information are written to the service log. Validation failures surface as an error status and log entry; detailed reporting is specified in [policy-rules-builder.md](aiac-agent/policy-rules-builder.md). A genuine grant/prohibit conflict surfaces on `/apply` as a `422` with a `ConflictReport` body (verbatim policy quotes; see [Error Handling](#error-handling)). There is no separate pre-commit `/policy/check` route β€” it is retired (see [PRB design decision: identify conflicts, never reconcile](aiac-agent/policy-rules-builder.md#design-decision-identify-conflicts-never-reconcile) / #2503), and the conflict diagnostic is folded into `/apply`.

Expand Down Expand Up @@ -265,7 +265,7 @@ src/aiac/
β”œβ”€β”€ shared/ ← project-level shared: run_upstream (upstream.py) β€” transport retry primitive
└── agent/
β”œβ”€β”€ controller/
β”œβ”€β”€ shared/ ← flatten_role (roles.py); focal_entities.py (resolve_focal_entities β€” D13, shared by live build() + diagnostic); error_logging.py (log_by_type β€” per-persona named-logger router)
β”œβ”€β”€ shared/ ← flatten_role (roles.py); focal_entities.py (resolve_focal_entities β€” D13, shared by live build() + diagnostic; skips the roles and scopes of every disabled service, except the focus service); error_logging.py (log_by_type β€” per-persona named-logger router)
β”œβ”€β”€ uc/
β”‚ β”œβ”€β”€ onboarding/
β”‚ β”‚ β”œβ”€β”€ orchestrator.py ← sequences provision β†’ policy_builder, returns list[PolicyRule]
Expand Down
6 changes: 6 additions & 0 deletions docs/specs/components/aiac-agent/policy-rules-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,12 @@ and is **not yet enforced** here.
| UC3 β€” Role Update | Role sub-agent | `build_role_rules(role, all_scopes)` β€” one call |
| Conflict diagnostic (folded into `/apply`, [identify conflicts, never reconcile](#design-decision-identify-conflicts-never-reconcile) / #2503) | Apply path | A parallel diagnostic assembly reusing propose / precheck / audit (record-not-raise + a terminal `explain` node); returns a `422` `ConflictReport` |

> **Disabled services are not candidates.** The UC1 caller and the conflict diagnostic get their
> candidates from one shared resolver, `resolve_focal_entities` (`agent/shared/focal_entities.py`).
> It skips the roles and scopes of every disabled service (`Service.enabled == False`, a quarantined
> failed onboarding), except the focus service. So the PRB never judges a pair with a quarantined
> service. See [`uc1-service-onboarding.md` β†’ Service Policy Builder](uc1-service-onboarding.md#sub-agent-service-policy-builder).

> **Door B removed (#2540).** UC1 previously also ran a user-role-focal **deny-only** pass
> (`build_role_denies` over each `RoleKind.USER` candidate role, alongside the scope-focal pass) to
> derive user-role exclusivity denies. Under digested input that pass is redundant and has been
Expand Down
Loading
Loading