Skip to content

feat(policy): target-side access control and shared roles (D29–D33) - #232

Merged
oblinder merged 96 commits into
mainfrom
target-side-ac-pr
Oct 11, 2026
Merged

oblinder merged 96 commits into
mainfrom
target-side-ac-pr

Conversation

@oblinder

@oblinder oblinder commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

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_SIDE in the aiac-agent-config ConfigMap (/deploy ac-side=agent|target, see docs/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:

The specs are updated with the code: docs/specs/PRD.md (D24, D29–D33), the component specs under docs/specs/components/, CONTEXT.md and the testing docs under docs/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 in CONTEXT.md is tracked in #217.

(Optional) Testing Instructions

  • Offline unit lane: .venv/bin/pytest (2059 passed, 406 deselected).
  • Integration lane, system lane on Kind, and eval: see docs/agents/test.md. Deploy with k8s/opa-kind-runbook.md. Use the Set the enforcement side procedure to run the suites under agent-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.

Side Passed Failed Skipped Time
target-side 70 0 9 29m 04s
agent-side 70 0 9 23m 47s
  • The 9 skips are the same under the two sides. All of them are in test/system/disabled/ (cross_service_conflict ×1, policy_agnostic ×8), which is disabled on purpose. No live test skipped.
  • test_crs_match_live_side and test_deny_comes_from_enforcement_point passed under the two sides. They check the CR shapes and the place of the deny for the live side.
  • After each redeploy, the ConfigMap, the Controller pod env and the Controller start log showed the new side, and start check Chore(deps): Bump python from 3.12-slim to 3.14-slim in /src/aiac/idp/service/configuration/keycloak #4 passed. After the runs, AIAC was deployed again with target-side.
  • The first redeploy showed that the runbook procedure did not make the two Secrets that enable.sh needs. Commit f62874b adds them to the runbook.
  • Caveat: the AIAC images in these runs were built before commits 26e1b60–03487b8. Those commits change 9 files under src/ (the Orchestrator, the preconditions, the Controller start, the consumer, the PCE and env.py). The test code and the k8s manifests were current. Thus these runs do not cover the newer src/ 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 /pr skill ran 7 review rounds (the Standards and Spec axes against origin/main and the PRD) and 1 dev-guide round. Four earlier review rounds on the branch were already addressed before this run.

Round Fixed Declined
1 7 4
2 8 1
3 4 0
4 11, and a repo-wide search for stale UC1 compute_and_apply caller text 1
5 (the findings were carried into round 6) 0
6 5 0
7 1 2

Main fixes:

Declined, with reasons:

  • The duplicated PLATFORM_SOURCE_CLIENTS parse: the Keycloak image holds only main.py and cannot import aiac.shared.
  • The repeated EnforcementSide switches in engine.py: this refactor is large and has risk, and it changes no behaviour.
  • The import location of the kube seam: the change is only cosmetic.
  • The data clump of catalog and holders: the change has little value for its cost.
  • Moving combiner_problems to its own module: the import cost is small.
  • The variable name built: the name is clear.
  • Two more wording items in round 7: the reviewer rated them not worth a change.

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-by and the repo's Assisted-By trailer. The commit messages were rewritten to add them. The trees did not change.

Checks

checks.sh reproduces the CI jobs that run on pull_request:

PASS branch
PASS commits-ahead  (94)
PASS rebased
PASS clean-tree
PASS signoff
PASS ruff-check
PASS ruff-format
PASS pre-commit
PASS python-tests
PASS keycloak-spi
PASS ai-assisted-by-trailer
PASS markdown-lint
PASS link-check
SKIP link-check-full  (advisory in CI (fail: false); external URLs)
SKIP dependency-review  (needs GitHub Dependency Review API; continue-on-error in CI)
PASS shellcheck
PASS yamllint
PASS bandit
PASS hadolint
SKIP trivy-fs  (informational in CI (exit-code 0); trivy not installed locally)
SKIP codeql  (needs GitHub CodeQL (Advanced Security) runner; not reproducible locally)
PASS action-pinning
SKIP pr-title  (checked on GitHub by pr-verifier.yml)
SKIP spellcheck  (spellcheck_action.yml job is disabled (if: false))
SKIP project-automation  (project.yml is pull_request_target and needs org secrets)
RESULT: ALL GATES PASS

Assisted-By: Claude Code

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>

@omerboehm omerboehm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 rossoctl login 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.md rewrite, .claude/rules/, docs/agents/*, docs/evaluation/*, the eval/* changes, the launcher fixes, the demo/ rewrite, and the deletion of docs/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 the drop_bootstrap paths and the preconditions. Could you re-run the UC-1 suite on rebuilt images under both enforcement sides?
  • No live test covers these:
    • a failed precondition (#1, #2 or #6 answering 409);
    • the D32 path from a Keycloak role mapping through aiac.apply.role-members.* and the re-render to a changed CR;
    • the drop_bootstrap path.
  • 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 empty owned_tools guard) are only checked as strings in CI. Can CI pin and install OPA 1.21?

Comment thread src/aiac/agent/controller/start.py Outdated

# 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.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/aiac/agent/eventbus/consumer.py Outdated
# 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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/specs/PRD.md Outdated
- **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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread k8s/idp-configuration-keycloak-pod.yaml Outdated
# 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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/aiac/policy/computation/engine.py Outdated
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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/aiac/policy/computation/engine.py Outdated
else:
apply_policy(AgentSidePolicyModel(agents=[], pass_through=[service_id]))


def _loggable(value: str) -> str:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: _loggable is also defined in orchestrator.py:223. Could it move to aiac.shared, next to env.py?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@oblinder

Copy link
Copy Markdown
Contributor Author

Thanks for the summary review, @omerboehm. The inline items are fixed in c8be228 and 1bab23d.

  • Blocking question (D31): fixed — 1bab23d. The link to the platform login clients is a design decision now, and the PRD says so (see the thread on PRD.md:536).
  • Scope and size: declined. We keep everything in this PR. The extra changes (CLAUDE.md, .claude/rules, docs/agents, docs/evaluation, eval/*, launcher, demo, the analysis doc) stay here.
  • D29: fixed — 1bab23d. The live switch is dropped for good, and D29 gives the reason.
  • Test coverage and CI: answered. We did the live UC-1 re-run after a fresh AIAC redeploy, and all live tests pass under both enforcement sides. OPA is upgraded to 1.21.

Inline threads: 11 replied (all fixed).

Fix commits

Checks (checks.sh --base origin/main)

PASS branch / commits-ahead (96) / rebased / clean-tree / signoff
PASS ruff-check / ruff-format / pre-commit / uv-lock-frozen / python-tests / keycloak-spi
PASS markdown-lint (0 issues on tracked files) / link-check / shellcheck / yamllint / bandit / hadolint
SKIP trivy-fs, action-pinning (informational in CI); dependency-review, codeql (need GHAS); spellcheck (disabled in CI); pr-title, dco (checked on GitHub)

@oblinder
oblinder requested a review from omerboehm October 11, 2026 16:37
@oblinder
oblinder merged commit 6456cfa into main Oct 11, 2026
18 checks passed
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Rossoctl Issue Prioritization Oct 11, 2026
@oblinder
oblinder deleted the target-side-ac-pr branch October 11, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants