Skip to content
Merged
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
5 changes: 4 additions & 1 deletion aiac/docs/specs/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ Nine components across five Kubernetes Pods plus a Python library layer, all imp
| 5 | **Policy and Domain Knowledge RAG** | ChromaDB vector store holding the access control policy and domain knowledge in persistent, queryable form, populated via a co-located RAG Ingest Service. |
| 6 | **Policy Guardrails Agent** | Verification gate co-located with ChromaDB and the RAG Ingest Service in the RAG Pod. Every document is checked before the RAG Ingest Service writes it to ChromaDB. Reachable only on the RAG Pod's loopback network — not exposed on the RAG Pod's ClusterIP Service. One service, two API families (`policy`, `domain-knowledge`); the `policy` family runs LLM-backed hygiene + corpus-contradiction checks (defined), `domain-knowledge` specced later. |
| 7 | **Event Broker** | NATS JetStream pod that decouples event producers (Keycloak SPI listener, RAG Ingest Service) from the AIAC Agent. Provides durable, at-least-once delivery with automatic replay on Agent pod restart. Competing consumer model ensures each event is processed exactly once. |
| 8 | **AIAC Agent** | LangGraph-based AI agent triggered by Event Broker subscriptions (`aiac.apply.>` subjects) and directly by the operator (`rebuild` only). Retrieves the current policy from the RAG store, interprets it against live PDP state, and applies the required policy changes immediately. |
| 8 | **AIAC Agent** | LangGraph-based AI agent triggered by Event Broker subscriptions (`aiac.apply.>` subjects) and directly by the operator (`rebuild` only). Retrieves the current policy from the RAG store, interprets it against live PDP state, and applies the required policy changes immediately. Also exposes a **read-only pre-commit policy conflict diagnostic** (`POST /policy/check`) that surveys a candidate policy for grant/prohibit contradictions without mutating policy state. |
| 9 | **Python library** | Python API library provides typed access to IdP and policy services via `aiac.idp.configuration`, `aiac.policy.model`, `aiac.policy.model_store.library`, `aiac.pdp.policy.library`, and `aiac.policy.computation` modules backed by generic Pydantic models. |

### High-level architecture
Expand Down Expand Up @@ -312,6 +312,7 @@ All inter-pod traffic is Kubernetes ClusterIP. External access is exclusively vi
- **Guardrails verification is a synchronous, per-document, pre-flight, fail-closed gate.** The RAG Ingest Service calls the Policy Guardrails Agent once per document before making any ChromaDB mutation; any rejection fails the whole request with nothing written, and an unreachable or erroring agent is treated the same as a rejection unless verification is explicitly disabled via `AIAC_GUARDRAILS_ENABLED`.
- **The Policy Guardrails Agent has no Event Broker involvement.** It neither publishes nor consumes NATS subjects; the RAG Ingest Service's existing `aiac.apply.policy.build` publish is unchanged.
- **AIAC Agent is stateless.** Changes are applied immediately on trigger — no pending session or human confirmation step.
- **Pre-commit conflict diagnostic is a separate read-only path.** `POST /policy/check` surveys a candidate policy for grant/prohibit contradictions but **never mutates policy state**, returns a `ConflictReport` rather than applying anything, and returns **`200` (not `422`) on a found conflict** — a found conflict is a successful diagnosis, not a failure. It is deliberately **distinct** from the live `/apply` contradiction contract (which raises `PolicyContradictionError` → `422` and aborts on the first genuine conflict), which stays byte-for-byte unchanged. Full spec: [components/aiac-agent/policy-conflict-check.md](components/aiac-agent/policy-conflict-check.md).
- **Event Broker decouples all automated triggers from the Agent.** The Keycloak SPI listener and RAG Ingest Service publish to NATS subjects; the Agent subscribes as a durable competing consumer. This removes all direct dependencies between trigger sources and the Agent.
- **`rebuild` bypasses the Event Broker.** It is an operator-only command issued directly via HTTP (`kubectl port-forward`). It is never published to NATS and has no NATS listener.
- **NATS consumer is a thin adapter.** It receives events from the Event Broker and calls the same internal handler functions used by the debug HTTP endpoints. No business logic lives in the consumer.
Expand Down Expand Up @@ -421,6 +422,8 @@ FastAPI + LangGraph service (`0.0.0.0:7070`). Receives automated triggers via th

All sub-agent `StateGraph` instances are logically separated modules running within a single pod and process. Sub-UC agents produce `list[PolicyRule]` and call `compute_and_apply(rules)` — they do not call `aiac.policy.model_store.library` or `aiac.pdp.policy.library` directly. The **Policy Update** sub-agents compute a minimal rule delta between the current ChromaDB policy and live OPA state. The **Rebuild** variant additionally clears the Policy Model Store and all OPA policy rules before recomputing. The **Role Update** orchestrator computes rules for all services affected by the role change. The **Service Onboarding** orchestrator classifies the new service via the pod's `rossoctl.io/type` label (for agents reads the `AgentCard` CR; for tools calls `tools/list` on the MCP endpoint discovered via K8s Service label lookup), then computes rules and calls `compute_and_apply`. Stateless; changes are applied immediately. Integrated retry with differentiated error codes per upstream.

The Agent additionally exposes a **read-only pre-commit policy conflict diagnostic** at `POST /policy/check`: given candidate policy text plus a target service id, it surveys that service's focal entities and returns a `ConflictReport` (all grant/prohibit contradictions at once, with verbatim quotes) — it never mutates policy state, never calls the PCE, and does **not** `422` on a found conflict (a found conflict is a successful `200` diagnosis), distinct from the live `/apply` contradiction path.

**Full spec:** [components/aiac-agent.md](components/aiac-agent.md)

---
Expand Down
13 changes: 10 additions & 3 deletions aiac/docs/specs/components/aiac-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,9 @@ Each use case (and the UC1 Orchestrator) is specified in a dedicated sub-PRD:
| Policy Update | [aiac-agent/uc2-policy-update.md](aiac-agent/uc2-policy-update.md) | `aiac.apply.policy.build`, `POST /apply/policy/build`, `POST /apply/policy/rebuild` | |
| Role Update | [aiac-agent/uc3-role-update.md](aiac-agent/uc3-role-update.md) | `aiac.apply.role.{id}`, `POST /apply/role/{id}` | |
| Service Offboarding | (see PCE `decommission`) | `POST /apply/offboard/{service_id}` (`aiac.apply.offboard.{id}` — NATS wiring is a follow-up) | Thin sub-agent; calls the PCE's `decommission(service_id)` **directly** (whole-service teardown, not a rule fold — bypasses the PRB and `compute_and_apply`). Keyed by **clientId, not UUID** (an offboarded client is gone from `get_services()`). |
| Policy Conflict Check (pre-commit diagnostic) | [aiac-agent/policy-conflict-check.md](aiac-agent/policy-conflict-check.md) | `POST /policy/check` (HTTP only — not routed through the Event Broker) | **Read-only** diagnostic: candidate `policy_text` + service id → `ConflictReport`. Reuses the PRB propose/precheck/audit machinery in a **separate assembly** (record-not-raise + `explain`). Does **not** call the PCE / `compute_and_apply`; never mutates policy; returns `200` on a found conflict. |

> **Note:** Each producing sub-agent (UC1–UC3) calls the **shared Policy Rules Builder** directly, merges the results, and returns `list[PolicyRule]` to the Controller. The Controller calls `compute_and_apply(merged_rules)` from `aiac.policy.computation` (PCE) once. Policy rule application is fully specified in [policy-computation-engine.md](policy-computation-engine.md). The Policy Rules Builder is specified in [aiac-agent/policy-rules-builder.md](aiac-agent/policy-rules-builder.md). **UC4 (Service Offboarding) is the exception:** it produces no rules — its handler resolves the clientId and calls the PCE's authoritative `decommission(service_id)` (specified in [policy-computation-engine.md → Decommission](policy-computation-engine.md#decommission-service-offboard)) to tear down the service's entire policy footprint.
> **Note:** Each producing sub-agent (UC1–UC3) calls the **shared Policy Rules Builder** directly, merges the results, and returns `list[PolicyRule]` to the Controller. The Controller calls `compute_and_apply(merged_rules)` from `aiac.policy.computation` (PCE) once. Policy rule application is fully specified in [policy-computation-engine.md](policy-computation-engine.md). The Policy Rules Builder is specified in [aiac-agent/policy-rules-builder.md](aiac-agent/policy-rules-builder.md). **UC4 (Service Offboarding) is the exception:** it produces no rules — its handler resolves the clientId and calls the PCE's authoritative `decommission(service_id)` (specified in [policy-computation-engine.md → Decommission](policy-computation-engine.md#decommission-service-offboard)) to tear down the service's entire policy footprint. **Policy Conflict Check is a further exception:** it is a **read-only** agent capability (not a UC number — the informal "UC4" is offboarding's), producing neither rules nor any PCE call. It surveys a candidate policy and returns a `ConflictReport`; the caller decides what to do with it. Full spec: [aiac-agent/policy-conflict-check.md](aiac-agent/policy-conflict-check.md).

### IdP access — library, not service

Expand All @@ -151,13 +152,16 @@ Every sub-agent (UC1 Provision + Service Policy Builder, UC2 Build + Rebuild, UC
| POST | `/apply/role/{role_id}` | Role Update | Role |
| POST | `/apply/service/{service_id}` | Service Onboarding | Provision |
| POST | `/apply/offboard/{service_id}` | Service Offboarding | Offboard (calls PCE `decommission` directly) |
| POST | `/policy/check` | Policy Conflict Check (diagnostic) | `check_policy_conflicts` — read-only; returns a `ConflictReport` JSON body |

`GET /health` is a bare liveness/readiness probe: the Controller is stateless (no local state, no connection held at rest), so it answers `200 {"status": "ok"}` whenever the process is serving, dispatching to no handler and touching no upstream. Upstream reachability (IdP, PCE, NATS) is validated per-request by the handlers. The k8s Deployment wires both the readiness and liveness probes to it.

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/*` 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 are raised as FastAPI `HTTPException`s, so error responses carry FastAPI's default JSON error body (`{"detail": ...}`) 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).

`POST /policy/check` is the **first endpoint that returns a JSON success body** (a `ConflictReport`), unlike the bare-status-code `/apply/*` routes. It is the read-only pre-commit conflict diagnostic: a **found conflict is a successful `200` diagnosis, NOT `422`** — only a pre-survey failure (IdP unreachable, unknown service, missing `policy_text`) is non-2xx. This is the opposite of the live `/apply` contradiction path, which raises `PolicyContradictionError` → `422`. Full spec: [aiac-agent/policy-conflict-check.md](aiac-agent/policy-conflict-check.md).

---

## Configuration
Expand Down Expand Up @@ -214,7 +218,7 @@ aiac/src/aiac/
├── shared/ ← project-level shared: run_upstream (upstream.py) — transport retry primitive
└── agent/
├── controller/
├── shared/ ← flatten_role (roles.py)
├── shared/ ← flatten_role (roles.py); focal_entities.py (resolve_focal_entities — D13, shared by live build() + diagnostic)
├── uc/
│ ├── onboarding/
│ │ ├── orchestrator.py ← sequences provision → policy_builder, returns list[PolicyRule]
Expand All @@ -223,8 +227,11 @@ aiac/src/aiac/
│ ├── policy_update/
│ │ ├── build/ ← calls PRB, returns list[PolicyRule]; TBD internals
│ │ └── rebuild/ ← delegates to Build; TBD internals
│ └── role_update/ ← calls PRB with (role, all_scopes), returns list[PolicyRule]
│ ├── role_update/ ← calls PRB with (role, all_scopes), returns list[PolicyRule]
│ └── policy_check/ ← read-only diagnostic: check_policy_conflicts(policy_text, service_id) → ConflictReport
└── policy_rules_builder/ ← shared; called by Service Policy Builder, Build, and Role sub-agent
├── diagnostic.py ← parallel diagnostic assembly (START-seeds-text, _audit_diagnostic record-not-raise, terminal _explain)
└── diagnostic_models.py ← ConflictReport + conflict/unevaluated row models
```

Docker build command (run from repo root):
Expand Down
Loading
Loading