Repository navigation
feat(policy): target-side access control and shared roles (D29–D33) - #232
Conversation
Audit every spec, testing and evaluation doc against the code on origin/main (384d011) and fix each claim that does not agree with it: the SPM store, one CR per agent, the quarantine, the PCE lock, the always-DENY Rego, the real routes, library functions, env vars, images and test markers. Remove duplicated paragraphs. Add a one-line "Status: not built yet" mark where a doc describes a feature that is not built (RAG, Guardrails, UC2/UC3 stubs, AIAC_AC_MODEL, rebuild). Where the code looks wrong and the doc looks right, the doc is not changed. docs/specs/digested-policy.md is not changed, because the digester loads it verbatim as its LLM system prompt. This commit describes the base only; it adds no new feature. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Describe the end state of handoff 12 (both phases) as the spec of the
system:
- the enforcement side: under target side (the default) each callee,
agent or tool, checks the access to itself in its own inbound OPA,
from its own CR; under agent side the agent's outbound decides, as
today. One global switch, AIAC_ENFORCEMENT_SIDE (D16, D29);
- the policy-model hierarchy with the enforcement_side tag and the
shared inbound projection (D18-D18b), and the writer routes
POST/PUT /policy and DELETE /policy/services/{id} (D18c);
- one CR per managed service; the D20 combiner denies a pod that has
no client CR, so the quarantine and the decommission delete the CR;
- the target-side tool inbound (D26), the agent-level agent inbound
(D26a), D27, and the outbound pass-through (D24);
- the PCE policy-model stage and the managed set (D21, D23), the
resync at Controller start (D28, D28a), the read-only route (D18);
- the start check and the onboarding precondition checks with the
permanent 409 (D30);
- the known limits, the glossary terms in CONTEXT.md, and the Kind
runbook and CLAUDE.md system-test text (C4).
The review checkpoint added these decisions:
- tool discovery under D20/D26: a self-discovery rule on the tool
inbound (the four MCP session methods for the tool's own clientId,
never tools/call) and a bootstrap CR that the PCE writes for an
enabled focus tool before Provision; discovery waits on a 403;
- a failed D30 check gives 409 and is permanent, but it is not a
rollback error (no rollback, no client disable, no quarantine);
- the Controller has a startupProbe; the resync PUTs only live
services; the AgentCard HTTP sync and the UC-2b store clear are
written as a known limit and an open issue.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Each callee, agent or tool, now checks the access to itself in its
own inbound OPA, from its own AuthorizationPolicy CR.
- policy model (D18a, D18b): the PolicyModel base with the
enforcement_side tag, TargetSidePolicyModel, AnyPolicyModel (a body
must carry its tag), and project_inbound, the shared split of one
SPM's edges into the user gate and the calling-agent gate; _derive
uses it with no change in behavior;
- libraries (D18c, C3): apply_policy / replace_policy /
delete_service_cr / delete_policy; the store lists every SPM when
GET /policy/services has no role (list_service_policies);
- PCE (D21, D23, D20, D28, D18): the policy-model stage deploys the
changed live SPMs; a zero-rule focus SPM is stored; rule P4 is gone;
quarantine and decommission delete the CR; resync() PUTs every live
stored SPM and quarantines disabled ones; policy_model_for();
bootstrap() writes the focus tool's first CR before Provision;
- writer (D18c, D24, D26, D26a, D27): POST/PUT /policy, DELETE
/policy/services/{id}; one CR per managed service with both request
packages; the tool inbound (user and calling-agent gates, the MCP
session rule, the self-discovery rule); the agent-level agent
inbound; a pass-through outbound;
- agent (D30, D18, D28): the precondition checks #1, #2 and #6 run
first and give a permanent 409 with no rollback; the tool bootstrap;
discovery waits on a 403 (180 s); the start sequence runs check #4
and the resync before the NATS consumer; GET
/policy/services/{id}.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
…art (P0) - k8s/aiac-combiner-default.yaml: the global combiner without the two `client_ok if not data.authbridge.client.<dir>.request` lines, so a pod that has no client CR is denied (D20); the response packages keep the stock content. test_combiner.py checks it with opa eval. - opa-kind-enable.sh applies it in Step 1 in place of the stock one (no fail-open window on a re-run), checks it, and restarts the agent and the tool pods; opa-kind-restore.sh puts back the stock combiner. - agent-deployment.yaml: the Controller gets `get` on the authbridge-runtime-config ConfigMaps (check #2) and on the `default` AuthorizationPolicy (check #4), AIAC_BUNDLE_SERVICE_NAMESPACE, and a startupProbe that covers the start sequence. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Every onboarded service, the tool included, has its own CR; the tool's inbound decides the tool calls and the agent's outbound is a pass-through. A service with no CR is denied (D20); the quarantine deletes the CR; rung 6 checks that a Controller restart (the resync) leaves the CRs unchanged and writes a missing CR again. The lane skips cleanly when the cluster still has the stock combiner. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
…ollow target side (P0) - demo/use-cases/uc1-onboarding: the per-intent tool check evaluates the tool's own inbound package when the agent's outbound is a pass-through (the agent-side path is kept), and the outbound input now carries mcp.method. - k8s/opa-kind-driver.sh follows the updated runbook (D20 baseline, no hand-applied tool pass-through, the /mcp probe). - test/unit/pdp/policy/generate_rego.py builds a TargetSidePolicyModel. - docs/examples/opa-team1-policy.yaml is the generated target-side CR pair of github-agent and github-tool. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
… P0b) One global switch, AIAC_ENFORCEMENT_SIDE (target-side, the default, or agent-side), selects where every callee's access is checked (D16, D29). - policy model (D18a): AgentSidePolicyModel(agents, pass_through); AnyPolicyModel is the discriminated union, so a body that mixes the sides cannot exist; - enforcement_side() reads the switch; an unknown value raises, and the Controller start stops before check #4 and the resync; - PCE (D23, D20, D28, D18): every entry point dispatches on the side. Agent side restores today's affected-agent set and deploys the derived APMs plus the pass-through of a focus tool; quarantine and decommission re-derive the targeters; the resync PUTs the APMs of the live agents and a pass-through for each live tool; - writer (D24): an agent-side body writes today's agent CR (inbound and outbound tool checks) and a pass-through CR for each tool; a body that mixes the sides, or an agent that is also a pass-through, is 422; - agent (D30): under agent side the checks cover agents only, and #2 also needs opa in the outbound pipeline; - aiac-agent-config sets AIAC_ENFORCEMENT_SIDE: target-side; - the eval pipeline pins agent side, because it scores the agent outbound Rego. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The rung switches AIAC_ENFORCEMENT_SIDE to the other side, restarts the Controller, and checks that the resync moves every CR (no mixed state) and that the demo verdicts hold with the deny at the enforcement point of the new side. It always restores the start value. The unused print_rego_dir helper is removed from the launcher. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Before a system test's teardown or pre-run cleanup deletes anything, the UC-1 harness now writes every AIAC-managed AuthorizationPolicy CR (label app.kubernetes.io/managed-by=aiac-pdp-policy-writer, all namespaces) and the global combiner (rossoctl-system/default) to the pytest host as YAML. After a failed run, the CR behind a wrong allow/deny is then still there to read. - capture_aiac_crs(phase) in test/system/uc1_onboard.py: read-only (kubectl get), best-effort (never raises, so it does not mask the test failure or stop the teardown), managedFields stripped. - Layout: <root>/<UTC time>__<test module>__<phase>/ with index.yaml and one <namespace>__<name>.yaml per CR. Root: AIAC_CR_CAPTURE_DIR, default test/system/artifacts/cr-captures/ (gitignored). - Hooks: pre-run-slate and teardown in onboarded_stack and pristine_stack, pre-env-restore (controller_env), pre-side-restore (controller_enforcement_side), and pre-cr-delete in rung 6. - 23 offline unit tests with kubectl stubbed. - docs/testing/uc1-onboarding-pipeline.md: the per-rung flow names the capture step. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Under target side, the tool's inbound OPA checks the user gate on the exchanged token. Keycloak's standard token exchange builds that token for the agent client and applies only the agent's client scopes, so the username-to-sub mapper on the rossoctl login client does not reach it: the tool sees sub = the user's Keycloak ID while AIAC keys users by username, and every agent -> tool call is denied (UC-1 rung 2). docs/analysis/user-subject-across-token-exchange.md records the live evidence, the AuthBridge, operator and Keycloak 26.5.2 facts, and three options for one subject on every leg, scoped to AIAC-managed agents and tools (client.type): - A: sub = user ID everywhere; AIAC keys users by user ID. - B: sub = username everywhere; recommended placement B-AIAC, where onboarding links an aiac-username-sub scope to each managed client. - C: AuthBridge takes the subject from preferred_username. Assessment: if usernames are unique, cannot change and are never reused, B-AIAC now and C later; otherwise A. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
docs/analysis/keycloak-access-control-analysis.md predates the OPA-as-PDP design as built. Its still-valid content is covered by the PRD (OPA as PDP, AuthBridge as PEP), the OPA Kind runbook (routes, the exchange scope, the OPA input) and the subject analysis; several of its statements are now wrong (the exchanged token keeps the same sub, the outbound OPA runs before the exchange, OPA gets the full token, client_secret auth). The one unique part, the Keycloak gates on the token exchange, moves to docs/analysis/user-subject-across-token-exchange.md §1.5, rewritten for Keycloak 26.5.2 (V2) failure modes, with the doubt about the realm-default audience scopes marked To verify. The Keycloak-native RBAC model it also described is not used; git history keeps it. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The subject analysis had live evidence for the target side only. The agent side has the same cause but fails later: the agent-side outbound check takes its subject from delegation.origin, the sub of the token that came into the agent, so one hop (login token, username) works, and the second agent of a chain fails in both its inbound and its outbound, because it receives an exchanged token with sub = user ID. - New §1.7 Enforcement sides: where each user check gets its subject, per side, for one and two hops (inferred from the code; not run live). - Option C: on the agent side the delegation origin of the token-exchange plugin must use the same claim; the jwt-validation option alone fixes only the inbound checks. - §8: B-AIAC fixes both sides with one setup and needs no identity change when the enforcement side switches; A is valid on both sides. - §9: two open questions to check the agent-side one-hop and two-hop results live; open questions renumbered. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
B-AIAC no longer changes the shared login client. rossoctl keeps its own
username-to-sub mapper, which reaches only tokens issued for rossoctl
(the login token); aiac-username-sub, linked by AIAC at onboarding to
each managed client, reaches the exchanged tokens, because the standard
token exchange applies only the requesting agent's scopes. Both have
the same mapping, so the subject is the username on every leg, and the
rossoctl guide, examples, runbook prerequisite and other users of the
client keep working.
The scope must not carry the aiac.managed marker: a marked scope linked
to many clients violates Assumption 2 (GET /services/{id}/scopes gives
409, so the catalog read fails), and it would become an own scope of
every linked service.
Two remaining risks are added: the two sources of the rule must stay
equal (the harness checks both), and the rossoctl mapper stays a manual
prerequisite.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Under target side every agent -> tool call was denied: the exchanged
token had sub = the Keycloak user ID, while the CRs key users by
username. The Keycloak standard token exchange applies only the scopes
of the requester (the agent client), so the login client's own
username-to-sub mapper never reached the exchanged token.
- IdP service: POST /services/{id}/subject-scope ensures the shared
client scope aiac-username-sub (a username -> sub mapper, no
aiac.managed marker) and links it as a default scope of the client.
It is idempotent, safe for concurrent onboardings, and moves an
optional link to a default link.
- IdP library: Configuration.link_subject_scope(service).
- Provision links the scope for agents and tools before
set_service_type. The scope is never in the created-manifest, so a
rollback never deletes it. The login client rossoctl does not change.
- System harness: require_subject_scope after each workload converges;
rung 2 checks the scope and decodes exchanged tokens; rung 5 checks
that the rollback keeps the scope. Unit tests cover each part.
- Docs: D31 in the PRD, the aiac.managed exception, the IdP, library,
UC-1, writer, PCE and policy-model specs, the runbook, the testing
docs and the analysis status.
Live: rung 2 (agent then tool, target side) 23 passed; the tool inbound
now sees subject dev-user / test-user.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Rung 2 passed with D31 deployed (23 tests, the exchanged token has sub = username, and the tool inbound sees dev-user / test-user); rungs 1, 3 and 7 passed too. Rung 7 closes question 7: the agent side works at one hop on the live cluster. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Keep CLAUDE.md short. Move the test-authoring ladder into docs/testing/testing-strategy.md, and move the container and pod hardening baseline into a path-scoped rule, .claude/rules/container-hardening.md. CLAUDE.md keeps short pointers, including one to docs/agents/test.md for the lane details. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Remove docs/agents/ from .gitignore, so that a fresh clone gets the files that CLAUDE.md points to. Add docs/agents/deploy.md (deploy parameters for the deploy skill) and docs/agents/test.md (test lanes for the test skill). Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The docs said that a request that falls back to V1 does not get the aiac-username-sub mapper. That is wrong for D31 as built: V1 uses the target client's scopes, and AIAC links the scope to each managed agent and tool, so an onboarded target still gets sub = the username. The AuthBridge requests go to V2 in any case, and a wrong subject can only cause a deny (D20, D25). So turning V1 off is optional hardening for the platform, not something AIAC depends on. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The ensure-step of POST /services/{id}/subject-scope checked the mapper
of aiac-username-sub by name only. A mapper with that name but a wrong
type or config (for example claim.name=preferred_username) stayed, so
the exchanged token kept sub = the user ID while onboarding reported
success. The system harness checks the type and the mapping, so the two
checks disagreed.
The ensure-step now converges AIAC's own mapper: a wrong config is
updated in place (PUT, with the mapper id), a wrong type is deleted and
added again (a concurrent 404 or 409 is success), and a correct mapper
gets no write. Only the keys AIAC sets are compared. A mapper with
another name is left as it is.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
After D31, B.4 is allowed only when github-agent links aiac-username-sub as a default scope. Without the link the driver polled B.4 for POLL_SECS and then failed with DENIED_TOOL_INBOUND. A read-only precheck now reads the client's default (and optional) scopes and stops at once with the cause and the fix when the link is missing or optional only. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Rung 5 recorded the subject-scope state only for the failed agent. Phase 4 now records it for the failed tool too, and test_rollback_keeps_the_subject_scope runs for both. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Fix a garbled verify_subject_mapper sentence in the policy-pipeline doc, say that require_subject_scope checks the mapper type and mapping and that rossoctl does not link the scope, and note in the analysis which commit its file:line references are for. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The listener ran inside AdminEventBuilder.send(), before Keycloak committed the admin change. So a CLIENT_CREATED event could reach the Controller before the client was visible, the first IdP read got 404, and the onboarding waited for the NATS redelivery (600 s) while D20 denied the new service (handoff 20). onEvent(AdminEvent) now only queues the subject. One after-completion transaction for each provider instance, enlisted at the first matching event, publishes the queue after the main commit. A rollback or a failed commit publishes nothing. With no active transaction, the event is published at once. A late event after the transaction is finished is dropped with a warning. A publish failure is logged and does not fail the admin request. The factory passes the session, and the shared NATS connection is resolved at commit time. The tests use a mocked transaction manager, and Keycloak's own DefaultKeycloakTransactionManager (keycloak-services, test scope only, not shaded) to pin the commit order. They pass against 26.7.3 and 26.5.2. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The IdP service changed every KeycloakError to 502, also "Could not
find client" for a client that Keycloak had not committed yet. So the
Controller could not tell a new client from an outage.
GET /services/{id}, its /roles, /scopes and /discovery-token reads, and
GET /roles/{name}/composites now answer 404 for a Keycloak 404. Other
Keycloak errors stay 502. The write routes do not change.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Configuration._check raised a plain RuntimeError with no status, so is_transient was always False and no HTTP-status failure of the IdP library was retried, against the docstrings. The unit tests hid it with UPSTREAM_MAX_RETRIES=1. _check now raises IdPHTTPError, a RuntimeError subclass with .status and .response (picklable). run_upstream then retries a 5xx up to UPSTREAM_MAX_RETRIES attempts with backoff, and raises a 4xx at once. _status_code no longer lets a mock status hide the real one. New tests run with UPSTREAM_MAX_RETRIES > 1 and no real sleeps. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The SPI event can come before the Keycloak client is visible. Then the first get_service of onboard_service gets 404 and the onboarding failed at once. onboard_service now reads the service again on a 404, up to ONBOARD_CLIENT_WAIT_ATTEMPTS reads with ONBOARD_CLIENT_WAIT_BACKOFF between them (default 15 x 2.0 s, about 30 s). After the budget it raises ServiceNotVisibleError, an HTTPException(502). Any other error is a 502 at once, as before. Nothing is provisioned while it waits. This is defense in depth for the SPI fix. The wait helper _WaitConfig is now public (WaitConfig), and a knob that is not a finite number falls back to the default. A cross-layer test drives the real IdP library through the wait. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
A ServiceNotVisibleError was left unacked, so NATS redelivered it only after ACK_WAIT (600 s). Below MAX_DELIVER, the consumer now naks it with AIAC_NOT_VISIBLE_NAK_DELAY_SECONDS (default 30 s). Each nak uses one of the MAX_DELIVER deliveries; at MAX_DELIVER the message goes to the DLQ as before. A failed nak is logged and the message stays unacked. Other errors, ACK_WAIT and MAX_DELIVER do not change. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
When the race hit a rung, the gate failed with a wrong cause ("OPA had
not loaded the agent's bundle").
The agent gate, the tool gate and the rung-5 gates now read a bounded
tail of the Controller log. If it shows "Could not find client" or
ServiceNotVisibleError, the failure message says that the onboarding
event probably came before the Keycloak commit, and what to check. The
hint helper never raises. The default gates stay; the testing doc marks
the workaround removal as to verify live.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Record D33 in the PRD (key decisions, the UC-1a flow, sections 6, 7.6, 7.11 and the section 9 test rows): the SPI publishes after the commit, the IdP service keeps a Keycloak 404, the library keeps the status and retries a 5xx, onboard_service waits a bounded time on the first read, and the consumer naks a service that is not visible yet with a delay. ACK_WAIT is not lowered. Update event-broker.md (at-most-once SPI publish, the delayed nak, the missing EnforcementPreconditionError) and aiac-agent.md (the new knobs, the retry wording, the ack contract). Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The sub-read 404 poll of the first IdP read, the 403 wait of MCP discovery under both sides, the kept shared role of a quarantined holder, and the full agent-side re-render of a role-members event. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
- Run the Controller Deployment with the Recreate strategy: the PCE lock and the per-service lock are in one process, so two pods must not write CRs at the same time. - Re-enable the client by its UUID (set_service_enabled_by_id), with no Service read under the PCE lock. - Under agent side, a tool bootstrap writes its pass-through CR with no IdP read. - Read AIAC_PDP_HTTP_TIMEOUT at call time, and fall back to the default for a bad value, so it cannot stop the Controller at import. - The writer parses the CR identity of an entry one time. - Start check #4 checks every entry of a repeated path, and k8s/opa-kind-enable.sh uses the same client_ok allowlist. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Remove the source-tree tour, the config and Dockerfile layout, the redundant pointer to the path-scoped hardening rule, and the issue migration history. Fix the CONTEXT.md location (repo root, not aiac/). Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
- Add the PCE drop_bootstrap: it deletes the CR of a service that has no stored SPM. The Orchestrator calls it after a failure that runs no quarantine (a 502 from Provision, a build error that is not a rollback error, a failed compute_and_apply), so an agent-side allow-all bootstrap CR does not stay for a tool that is not managed. - Provision gets the pod coordinates and type that the precondition checks read, so classify_service makes no second IdP read and no second pod wait. - Move env_num to aiac.shared.env and use it for the PDP timeout. - The decommission primes the SPM cache from its list read, so the agent-side derive makes no by-role store query. - opa-kind-enable.sh reports a failed read of the combiner CR. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The installed engineering skills (mattpocock-skills 1.3) name the glossary GLOSSARY.md and record decisions under docs/adr/. This repo uses CONTEXT.md and design-decision sections in the specs. State the mapping in docs/agents/domain.md and CLAUDE.md, so that domain-modeling does not create a second glossary. Remove the derivable file-structure section and a repeated sentence from domain.md. Link the neutrality-guard follow-up in CONTEXT.md to #217. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
issue-tracker.md: add the 'PRs as a request surface: no' flag that /triage reads, and mark the old Project #11 as closed. test.md: warn that a git worktree has no .env, so the live lanes skip; describe all four integration tests, not only the first; remove the retired exclusivity denies from the llm-lane text. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
…et-side branch - Check #2 under agent side also needs mcp-parser in the outbound pipeline: the agent outbound package reads input.mcp.method and input.mcp.params.name (code, tests, PRD, UC-1 spec, runbook). - The Orchestrator has one try around Provision and the build: a rollback error quarantines, any other error drops the bootstrap CR. - The combiner skip gate of the system tests uses the rules of start check #4 (new pure combiner_problems in controller/start.py). - env_num gets an exclusive option for a strictly positive value. - PRD names drop_bootstrap in the PCE entry points, the Orchestrator calls and the _pce_lock list, and after a Provision failure. - Use the CONTEXT.md term "pass-through CR" in place of "allow-all". Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The order of a scope or role list, and of map keys, has no meaning in the rendered Rego, and the renderer keeps the order the IdP returns. test_failed_tool_agent_cr_by_side failed under target side only because agent_scopes came back in a different order after the tool's quarantine. Add uc1.rego_canonical / cr_canonical: each top-level data binding is read as JSON and written again with every list and map key sorted; all other lines stay as they are. Use it in each exact CR compare: the failure-rollback agent CR check, the side-switch "restores the start CRs" check, and the resync _changed_paths and repaired-CR checks. Unit tests pin the helper against the real rego renderers. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
…et-side branch - The Orchestrator drops the bootstrap CR after a failed apply only for a bootstrapped tool (_onboard returns the bootstrapped flag). - created_roles/created_scopes are set before the try, so a rollback error from Provision still runs the rollback and the quarantine. - New unit tests: drop_bootstrap holds the PCE lock; the agent-side APM has the same rule order on the read-model, resync and rerender_role paths; a failed apply of an agent drops nothing. - rego_canonical removes only a comma at line end, so a comma in a string value stays; tests for that and for a non-JSON binding replace a tautological assertion. - Specs name drop_bootstrap as a caller of delete_service_cr, the PRD UC-1 flow names all three failures that drop the bootstrap CR, and the testing doc says the CR compare ignores list and key order. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
… UC1 specs _onboard now returns _Onboarded(rules, client_id, bootstrapped), and onboard_and_apply gives override=False at the compute_and_apply call. The UC1 specs say that onboard_and_apply calls the PCE itself and the Controller gets nothing back, name the new return shape, and say that a failed apply drops the bootstrap CR only for a tool that this run bootstrapped. The test map names the drop_bootstrap tests. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
…et-side branch onboard_and_apply passes override=False as a keyword to compute_and_apply. The docs, docstrings and comments now say that UC1 calls compute_and_apply from onboard_and_apply and returns None, and that _onboard returns an _Onboarded (rules, client_id, bootstrapped). Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
A live switch of the enforcement side (a ConfigMap patch and a Controller restart) is not supported. The side is set at deploy time, and a side change is a clean redeploy of AIAC. - Delete test/system/test_uc1_onboard_side_switch.py (rung 7) and the harness helpers only it used (controller_enforcement_side and its restore error). - Replace the runbook section "Switch the enforcement side" with "Set the enforcement side": delete every AIAC CR and the aiac-system namespace (with the Policy Model Store), deploy again, set the side, onboard again. - Point the ac-side deploy parameter at that procedure, and remove every live-switch statement from the PRD, the component specs, the test docs, the demo, the manifest comments and the source comments. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The analysis cites rung 7 (the side switch) as the evidence that the agent side works. Rung 7 is deleted, because a live side switch is not supported. Say so, and say how to check the agent side again: run the system lane after a clean redeploy of AIAC with agent-side. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
…ide branch - policy-pipeline.md: remove the live side switch from the agent-side run (a side change is a clean redeploy) and fix the broken UC-1 ladder sentence. - orchestrator.py: check #2 under agent side needs mcp-parser and opa outbound; _Onboarded.bootstrapped controls only the failed-apply drop; _onboard names the build-failure drop. - deploy.md: say that the Trap patch and runbook step 4 are not a side change. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
…at a test Rung 7 (the side-switch suite) is deleted. Remove the four rung-7 references from the UC1 onboarding pipeline spec and the harness docstring. Apply ruff format to the provision-users unit test. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
The clean redeploy of "Set the enforcement side" failed at enable.sh: the teardown deletes aiac-system/keycloak-admin-secret with the namespace, and enable.sh needs team1/openai-secret. Add the LLM key Secret as a precondition and a step that makes the namespace and the Keycloak admin Secret again. Renumber the steps and the step reference in docs/agents/deploy.md. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
86878cc to
c3d1235
Compare
c3d1235 to
f62874b
Compare
omerboehm
left a comment
There was a problem hiding this comment.
Thanks for this. It's a big piece of work, and the review rounds and the system-test table in the description help a lot. My comments are below. The inline ones carry the details.
Blocking question
- D31 writes to the
rossoctllogin client, which the rossoctl chart owns. The PRD marks the rossoctl team's agreement as open (PRD.md:536), but the code links the scope already. An earlier version of the spec (33b0bef) said "AIAC never changes a login client". Can we get that agreement before merge, or put the link behind a flag?
Scope and size
- 184 files and 94 commits make this hard to review well. About 2.5k added and 1.1k deleted lines are not part of D29–D33: the
CLAUDE.mdrewrite,.claude/rules/,docs/agents/*,docs/evaluation/*, theeval/*changes, the launcher fixes, thedemo/rewrite, and the deletion ofdocs/analysis/keycloak-access-control-analysis.md. Could these go into their own PRs? The demo and launcher changes that target side needs can stay. - The PRD's decision numbers (D24, D29–D33) are all new in this PR, and several were written or changed after the code. One example: D29 first said a side change is a ConfigMap patch plus a Controller restart (4e83c02). After the side-switch suite was deleted (446d9a0), it says a live switch is not supported. If that was a deliberate scope cut, please record it as a known limit or a follow-up issue.
Test coverage
- The description's own caveat says the system runs used images built before 26e1b60–03487b8, so 9
src/files are not covered. Those commits include thedrop_bootstrappaths and the preconditions. Could you re-run the UC-1 suite on rebuilt images under both enforcement sides? - No live test covers these:
- I could not find a CI workflow that installs
opa. The Rego behaviour tests skip without it, so the OPA 1.21 fixes (object.get, the emptyowned_toolsguard) are only checked as strings in CI. Can CI pin and install OPA 1.21?
|
|
||
| # The start of a rule of ``client_ok``: its head, also a ``default`` one. A line that is only | ||
| # ``client_ok`` is a reference in the body of another rule. | ||
| _CLIENT_OK_HEAD = re.compile(r"(default )?client_ok\b.") |
There was a problem hiding this comment.
client_ok\b. skips a line that is only client_ok. What if a rule head is written as client_ok with if not data... on the next line? If OPA accepts that form, a fail-open rule gets past the allowlist, at least while a safe rule is also present. Could you add a test that runs this check against the output of opa parse, or parse the policy with opa parse here instead of a regex?
There was a problem hiding this comment.
You are right. opa parse accepts client_ok on one line and if not ... on the next, and the old regex skipped that line. In start.py, _client_ok_rules now ignores comments and string text and counts brackets. Each line outside every bracket that names client_ok is a rule start, including a bare client_ok. Unbalanced brackets fail closed. The same rule (in awk) is in k8s/opa-kind-enable.sh. test_start.py has new tests for split heads, plus TestTheCheckAgreesWithOpaParse: it runs opa parse on each fixture and checks that "no problem" means every client_ok rule in the AST is a safe rule (it skips without opa). I kept the text check because the Controller image has no opa binary.
Fixed in c8be228.
| except _InvariantViolation as e: | ||
| return JSONResponse(status_code=409, content={"error": str(e)}) | ||
| except KeycloakError as e: | ||
| return JSONResponse(status_code=502, content={"error": str(e)}) |
There was a problem hiding this comment.
An unknown service_id makes Keycloak answer 404, but this returns 502. The read routes in this PR keep the 404 through _read_error, and the library retries a 5xx, so a bad UUID is retried as if Keycloak were down. Could this use _read_error, or at least pass a 404 through?
There was a problem hiding this comment.
link_subject_scope in keycloak/main.py now returns _read_error(e). A Keycloak 404 (an unknown service_id) stays 404, so the library does not retry it as an outage. Other errors stay 502. There is a new test, test_returns_404_for_an_unknown_service, and the old 404-to-502 link test now uses a 500. idp-configuration-service.md lists the new 404.
Fixed in c8be228.
| # enabled-with-no-policy. The re-enable is an IdP call, so it takes the subject's UUID. | ||
| if focus is not None: | ||
| reenable_service(ServiceUuid(msg.subject.removeprefix(_SERVICE_PREFIX))) | ||
| await _process(msg.subject, redelivered=msg.metadata.num_delivered > 1) |
There was a problem hiding this comment.
num_delivered > 1 also turns off the client wait on a redelivery after an unrelated failure, for example an LLM timeout followed by the ACK_WAIT redelivery. That redelivery reads the IdP once and naks at once, using up a MAX_DELIVER attempt. The impact is low, but should the decision depend on why the last delivery failed (the not-yet-visible nak path), not only on the delivery count?
There was a problem hiding this comment.
The decision now depends on the cause, not on the delivery count. AiacEventConsumer keeps the stream sequence of each message that it nak'd for ServiceNotVisibleError (_not_visible_seqs). Only the next delivery of such a message runs with wait_for_client=False. A redelivery after another failure (for example an LLM timeout and the ACK_WAIT redelivery) waits again. The set is in memory: after a restart a redelivery waits, which is the safe side. Tests in test_consumer.py and the text in D33 (e), aiac-agent.md and uc1-service-onboarding.md are updated.
Fixed in c8be228.
| # deleted UUID (a manual POST, a phantom event) also waits the whole budget before its 502. The | ||
| # consumer gives ``wait_for_client=False`` on a NATS redelivery: the nak delay already gave the | ||
| # client time to become visible, so a redelivery reads once and does not block the consumer again. | ||
| CLIENT_WAIT = WaitConfig("ONBOARD_CLIENT_WAIT_ATTEMPTS", "ONBOARD_CLIENT_WAIT_BACKOFF", 15, 2.0) |
There was a problem hiding this comment.
The comment above explains this well: the wait of about 30 s runs inside _service_lock, and the consumer handles one message at a time. So an unknown or deleted UUID stalls every onboarding for 30 s. Please add this as a known limit in the PRD under D33, so operators know about it.
There was a problem hiding this comment.
I added a PRD section, "Known limits (the onboarding event, D33)". It says that a UUID the IdP does not have (unknown or deleted, a phantom event, a wrong realm) blocks every other onboarding for the whole client wait, about 30 s, because the wait runs in _service_lock and the consumer handles one message at a time. A manual POST blocks only its own request. The section also says how to set a smaller budget.
Fixed in c8be228.
| super().__init__(502, detail) | ||
|
|
||
|
|
||
| # Event-before-commit race tolerance for the first IdP read (handoff 20). The Keycloak SPI listener runs |
There was a problem hiding this comment.
nit: "(handoff 20)" points nowhere, because docs/handoffs/ is not in the repo. The same reference is in consumer.py:95. Could these point to D33 in the PRD instead?
There was a problem hiding this comment.
orchestrator.py and consumer.py now point to "D33 in docs/specs/PRD.md", not "handoff 20". The same comment in three unit test files is fixed too. The system-harness hint text in test/system/uc1_onboard.py still says "handoff 20 / D33", because a unit test asserts that text.
Fixed in c8be228.
| - **Policy Computation Engine is a library, not a service.** `aiac.policy.computation` runs in-process within the AIAC Agent pod. It requires no Kubernetes deployment, no container image, and no ClusterIP Service. The Controller calls `compute_and_apply(rules, override)` directly (UC1: the Orchestrator's `onboard_and_apply`). | ||
| - **One CR per managed service + one SQLite store, distinct owners, distinct purposes.** The Policy Model Store owns a SQLite `service_policies` table (backed by a 1 Gi RWO PVC). The table holds one `ServicePolicyModel` (SPM) per service. The SPMs are the source of truth for policy state, served from an in-memory cache. Each managed service, agent or tool, has one `AuthorizationPolicy` CR, owned by the PDP Policy Writer. The CR holds the two Rego request packages that the writer renders from the service's entry in the policy model of the current side. The CR name and namespace come from the service's SPIFFE ID (`identity_ref`: the namespace and the ServiceAccount). The two services have no dependency on each other; both are driven by the PCE via their respective libraries. | ||
| - **One global switch selects the enforcement side (D16).** Under target side, each callee (agent or tool) checks the access to itself in its own inbound OPA, from its own CR. Under agent side (the legacy method), each agent's CR checks the agent's calls to tools on its outbound, and each tool's CR is a pass-through. The two sides never exist together. See [§3 Enforcement side](#enforcement-side). | ||
| - **The switch is `AIAC_ENFORCEMENT_SIDE`, and the default is `target-side` (D29).** The accepted values are `target-side` and `agent-side`. The switch is in the `aiac-agent-config` ConfigMap. The Controller reads it at start, and an unknown value stops the Controller. The writer reads the side from the policy-model tag, not from an env var. The side is set at deploy time. A live switch (a ConfigMap patch and a Controller restart) is not supported: a side change is a clean redeploy of AIAC, which deletes every AIAC CR and the Policy Model Store. `AIAC_AC_MODEL` is a different setting: it names a modeling paradigm, not the place of the check. |
There was a problem hiding this comment.
The first version of D29 (4e83c02) promised a live switch: a ConfigMap patch and a Controller restart, after which the resync writes every CR in the new side. This line now says a live switch is not supported. If the live switch is dropped for good, please say why here. If it is deferred, please open a follow-up issue.
There was a problem hiding this comment.
We dropped the live switch for good, and D29 now says why. The two sides put the check in different CRs, so a CR set that mixes the sides can check nothing: for example, a pass-through outbound on the agent and a pass-through inbound on the tool let every tool call through. A clean redeploy deletes every AIAC CR and the Policy Model Store, so no mixed state can exist, and then each service is onboarded again. D29 links to the runbook section "Set the enforcement side".
Fixed in 1bab23d.
| # the realm KEYCLOAK_REALM. Without KEYCLOAK_REALM it skips the startup link; | ||
| # each onboarding makes the link too. | ||
| KEYCLOAK_REALM: "rossoctl" | ||
| PLATFORM_SOURCE_CLIENTS: "rossoctl" |
There was a problem hiding this comment.
PLATFORM_SOURCE_CLIENTS is also set in aiac-pdp-config. Two copies of the value can drift apart. Can one of them be the single source? Separately: the same setting is called _LOGIN_CLIENTS_ENV in keycloak/main.py and PLATFORM_SOURCE_CLIENTS in opa/main.py and rego.py. I accept the reason for keeping two parses, but the names could still match.
There was a problem hiding this comment.
In the real topology there is already one source: both containers of the Interface Pod read PLATFORM_SOURCE_CLIENTS from aiac-pdp-config. The second copy was in the dev-only standalone manifest, which must not reuse that ConfigMap (the manifest comment explains the clobber). I removed the key from aiac-pdp-config-standalone, so that Pod uses the code default (rossoctl), and the comment points to the one value. In keycloak/main.py, _LOGIN_CLIENTS_ENV / _DEFAULT_LOGIN_CLIENTS / _login_clients are now _PLATFORM_SOURCE_CLIENTS_ENV / _DEFAULT_PLATFORM_CLIENTS / _platform_clients, to match the writer.
Fixed in c8be228.
| current holders, so the edges have the same order.""" | ||
| former: set[str] = set() | ||
| for old, new in zip( | ||
| stored.inbound_allow_rules + stored.inbound_deny_rules, |
There was a problem hiding this comment.
nit: m.inbound_allow_rules + m.inbound_deny_rules repeats at :340, :406, :1174, :1233 and :1305. The for attr in ("inbound_allow_rules", "inbound_deny_rules") loop with getattr/setattr repeats at :205 and :278. One all_edges() method on ServicePolicyModel would remove the repetition.
There was a problem hiding this comment.
ServicePolicyModel now has all_edges() (the Allow edges, then the Deny edges) and prune_edges(prune), which prunes each list on its own. The six inbound_allow_rules + inbound_deny_rules places in engine.py and the model-store route use all_edges(). _purge_role and _reconcile use prune_edges, so the getattr/setattr loops are gone.
Fixed in c8be228.
| else: | ||
| apply_policy(AgentSidePolicyModel(agents=[], pass_through=[service_id])) | ||
|
|
||
|
|
||
| def _loggable(value: str) -> str: |
There was a problem hiding this comment.
nit: _loggable is also defined in orchestrator.py:223. Could it move to aiac.shared, next to env.py?
There was a problem hiding this comment.
The two _loggable functions are now one loggable() in aiac/shared/logging_config.py. engine.py and orchestrator.py import it. It accepts any object and removes CR/LF.
Fixed in c8be228.
| @@ -70,12 +70,15 @@ async def lifespan(app: FastAPI): | |||
|
|
|||
|
|
|||
| @app.get("/policy/services", response_model=None) | |||
| def list_service_policies_by_role(role: str) -> list[ServicePolicyModel]: | |||
| # Return every cached SPM referencing the given role id across BOTH inbound rule lists | |||
| def list_service_policies_by_role(role: str | None = None) -> list[ServicePolicyModel]: | |||
There was a problem hiding this comment.
nit: when role is None, this now returns every SPM, which the name list_service_policies_by_role doesn't suggest. Could it be renamed, or split into two routes?
There was a problem hiding this comment.
I renamed the handler to list_service_policies, and policy-model-store.md changed with it. I did not split it into two routes: both would be GET /policy/services, and FastAPI would match only one. ?role= is an optional filter on the one collection route, and a comment now says this.
Fixed in c8be228.
- start check #4: find client_ok rule heads outside every bracket (a head can span lines), ignore strings and comments, fail closed on unbalanced brackets; same rule in opa-kind-enable.sh; test against opa parse. - IdP config: POST /services/{id}/subject-scope keeps a Keycloak 404. - consumer: skip the client wait only on the redelivery after a not-visible nak, not on every redelivery. - PRD: known limit for the D33 client wait; D33 refs replace handoff 20. - ServicePolicyModel.all_edges()/prune_edges(); shared loggable(); list_service_policies route name; PLATFORM_SOURCE_CLIENTS naming and one value in k8s. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
…h reason - D31: the link of aiac-username-sub to the platform login clients (PLATFORM_SOURCE_CLIENTS, default rossoctl) is decided, not open. - D29: a live side switch is not planned; a CR set that mixes the sides can check nothing, and a clean redeploy leaves no mixed state. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Oleg Blinder <oblinder@gmail.com>
|
Thanks for the summary review, @omerboehm. The inline items are fixed in c8be228 and 1bab23d.
Inline threads: 11 replied (all fixed). Fix commits
Checks ( |
Summary
This PR adds target-side access control to AIAC. A deploy-time switch selects the enforcement side:
target-side(default, D29): the inbound OPA of each tool makes the decision. The outbound request package of each service is a pass-through, so the callee makes the decision (D24).agent-side: the agent has inbound and outbound Rego rules.The switch is
AIAC_ENFORCEMENT_SIDEin theaiac-agent-configConfigMap (/deploy ac-side=agent|target, seedocs/agents/deploy.md). A live switch is not supported. To change the side, do a clean redeploy (k8s/opa-kind-runbook.md→ Set the enforcement side).The branch also adds these decisions:
D30, precondition checks. At start, the Controller stops if the global combiner does not deny a missing client CR (check Chore(deps): Bump python from 3.12-slim to 3.14-slim in /src/aiac/idp/service/configuration/keycloak #4). At each onboarding, the Orchestrator checks the sidecar (Chore(deps): Bump python from 3.12-slim to 3.14-slim in /src/aiac/pdp/service/policy/opa #1), the namespace pipeline (Chore(deps): Bump python from 3.12-slim to 3.14-slim in /src/aiac/policy/model_store/service #2) and the probes (Chore(deps-dev): Bump org.apache.maven.plugins:maven-surefire-plugin from 3.5.2 to 3.6.0 in /keycloak-spi #6). Under agent side, Chore(deps): Bump python from 3.12-slim to 3.14-slim in /src/aiac/policy/model_store/service #2 needs
mcp-parserandopain the outbound pipeline.D31, the subject is the username on every leg. The PCE links the subject client scope to the login clients.
D32, shared roles and scopes across services. The PCE resolves the role holders at render time. The Keycloak SPI publishes
aiac.apply.role-members.*, and the event bus renders the CRs again. The outbound subject maps are keyed by target.D33, the onboarding event comes after the Keycloak commit. The agent waits a bounded time for a new client. The consumer naks a service that is not visible yet, with a delay.
UC-1 onboarding:
drop_bootstrap) when Provision, the build orcompute_and_applyfails.onboard_and_applycallscompute_and_apply(rules, override=False, focus_service=client_id)itself.OPA 1.21 fixes. The Rego reads every map with
object.get. There is no session rule over an emptyowned_tools.Tests:
Runbook: the clean redeploy of the enforcement side (
k8s/opa-kind-runbook.md→ Set the enforcement side) failed atenable.sh, because the teardown deletesaiac-system/keycloak-admin-secret. The procedure now names the LLM key Secret (team1/openai-secret) as a precondition, and it has a step that makes the namespace and the Keycloak admin Secret again.The specs are updated with the code:
docs/specs/PRD.md(D24, D29–D33), the component specs underdocs/specs/components/,CONTEXT.mdand the testing docs underdocs/testing/.Feature Acceptance tier: Tier 2. The change is large and goes across components: the agent, the PCE, the PDP writer, the IdP service, the Keycloak SPI, the event bus and the k8s manifests. The design spec is
docs/specs/PRD.md(decisions D29–D33).Related issue(s)
None. The design is in
docs/specs/PRD.md. A follow-up for the neutrality guard inCONTEXT.mdis tracked in #217.(Optional) Testing Instructions
.venv/bin/pytest(2059 passed, 406 deselected).docs/agents/test.md. Deploy withk8s/opa-kind-runbook.md. Use the Set the enforcement side procedure to run the suites underagent-side.System tests
The UC-1 onboarding system suite ran on Kind under each enforcement side. Between the runs, AIAC got a clean redeploy (
k8s/opa-kind-runbook.md→ Set the enforcement side).Command:
.venv/bin/pytest -m system -k uc1_onboard -v -rs. It collects 79 tests in 5 rungs: agent only, agent then tool, tool then agent, failure rollback and resync.target-sideagent-sidetest/system/disabled/(cross_service_conflict×1,policy_agnostic×8), which is disabled on purpose. No live test skipped.test_crs_match_live_sideandtest_deny_comes_from_enforcement_pointpassed under the two sides. They check the CR shapes and the place of the deny for the live side.target-side.enable.shneeds. Commit f62874b adds them to the runbook.src/(the Orchestrator, the preconditions, the Controller start, the consumer, the PCE andenv.py). The test code and the k8s manifests were current. Thus these runs do not cover the newersrc/changes. To cover them, rebuild the images (demo/use-cases/onboarding/enable.sh --stack-only --rebuild) and run the suite again under each side.Review
The
/prskill ran 7 review rounds (the Standards and Spec axes againstorigin/mainand the PRD) and 1 dev-guide round. Four earlier review rounds on the branch were already addressed before this run.compute_and_applycaller textMain fixes:
mcp-parser.drop_bootstrapis gated on abootstrappedflag and has a_pce_locktest.created_rolesandcreated_scopescan no longer be unbound in the rollback handler._onboardreturns an_OnboardedNamedTuple, andoverride=Falseis a keyword argument.drop_bootstrapcases and the removal of the live side switch (rung 7).Declined, with reasons:
PLATFORM_SOURCE_CLIENTSparse: the Keycloak image holds onlymain.pyand cannot importaiac.shared.EnforcementSideswitches inengine.py: this refactor is large and has risk, and it changes no behaviour.catalogandholders: the change has little value for its cost.combiner_problemsto its own module: the import cost is small.built: the name is clear.The round-7 hard finding was a text change that the reviewer gave exactly. It was checked with a grep and was not reviewed again.
All commits carry a DCO
Signed-off-byand the repo'sAssisted-Bytrailer. The commit messages were rewritten to add them. The trees did not change.Checks
checks.shreproduces the CI jobs that run onpull_request:Assisted-By: Claude Code