« Phase 09 · Warmup · Track Overview
Lab 01 — The Control Plane
The problem
There are four hundred agents in production. An examiner asks four questions:
- Which of them can move money?
- Who owns the one that made this decision, and when was it last evaluated?
- Which policy allowed it — and can you show me that version of the policy?
- If you suspend one right now, how long until it stops acting?
A platform without a control plane answers the first with a spreadsheet, the second with a Slack archaeology exercise, the third with "the rules are in the prompt", and the fourth with a shrug. Those four answers are the difference between a platform and a collection of teams with API keys.
You build the layer that answers all four in milliseconds — and the one property that is harder than it sounds: it must keep answering when it is itself unreachable.
What you build
| # | Component | What it does |
|---|---|---|
| 1 | Subject, Resource, Environment, Request | the four decision inputs — a blended principal, and posture in the environment |
| 2 | Rule, _action_matches | ABAC matching with a wildcard, where empty means "any" |
| 3 | PolicyEngine | default-deny, deny-overrides — the two combining rules a bank cannot negotiate |
| 4 | PolicyBundle | versioned, HMAC-signed, structurally validated |
| 5 | BundleDistributor, Posture | atomic activation, staleness alarm, hard stop — and fail-static |
| 6 | AgentRegistry, AgentState | the KYA database and its lifecycle, with a mandatory human owner |
| 7 | ToolRegistry, ControlPlane.discover | authorization-aware capability discovery |
| 8 | PostureFinding, .posture_checks, .authorize | KYA at request time — categorical vs graduated — then policy, then the record |
| 9 | ContinuousAuthorizer, Lease | decision TTLs, mid-task re-evaluation, a kill switch that beats the TTL |
| 10 | EvaluationPipeline | golden sets and safety suites — wired into authorization, not into a dashboard |
| 11 | Tracer, lineage | agent- and tool-granular spans carrying identity, decision and version |
Key concepts
| Concept | Where | Why it matters |
|---|---|---|
| Default-deny | PolicyEngine.evaluate | a policy set that fails open when someone forgets a case is not a control |
| Deny-overrides | PolicyEngine.evaluate | makes the rule set order-independent, and therefore reviewable |
| Every matching rule is recorded | Decision.matched_rules | "what else fired?" is the first question in a policy incident |
| A decision is an artifact | Decision | a boolean cannot be shown to an examiner; a policy version can |
| Signed bundles | PolicyBundle.sign/verify | the mechanism that makes atomic activation trustworthy |
| Allow-everything is a bug | PolicyBundle.validate | an ALLOW with every facet empty is never intended, and is silent |
| Rollback resistance | BundleDistributor.offer | a replayed older bundle is a policy downgrade attack |
| Fail-static | Posture | the third option: not fail-open (a hole), not fail-shut (a self-inflicted outage) |
| The hard stop | BundleDistributor.engine | a bundle hours old in a bank is worse than an outage — say the number |
| Rejection ≠ refresh | offer on a bad bundle | a rejected push must not reset the staleness clock, or staleness never fires |
| Every agent has a human owner | AgentRegistry.register | the standard audit finding, and the reorg problem underneath it |
| A pinned model version | AgentRecord.model_version | an unpinned model changes under you, silently, between evaluations |
| KYA is a runtime property | posture_checks | onboarding checks describe the agent that was approved, not the one running |
| Categorical vs graduated | PostureFinding.blocks_reads | a control that downs the fleet on a late eval job is a control operators disable |
| Discovery is authorization | ControlPlane.discover | a tool a model can see is a tool it will eventually try to call |
| Posture degrades discovery | discover | offering a tool the next call would refuse is worse than not offering it |
| Leases and revocation latency | Lease.ttl_ticks | the TTL is the revocation SLA, and it is a number you will be asked for |
| A new bundle invalidates leases | ContinuousAuthorizer | otherwise a policy push takes effect one TTL from now |
| High-impact is never leased | high_impact=True | the actions worth caching are exactly the ones not worth caching |
| The kill switch has two halves | revoke_agent | dropping leases makes it fast; the revoked set keeps it fast |
| Safety failures are disqualifying | EvaluationPipeline.gate | an aggregate that averages away a safety failure is a gate that does not gate |
| Evaluation feeds authorization | posture_checks ← record_evaluation | the single wire that turns quality from a report into a control |
| Lineage is a query | Tracer.lineage | only answerable because every span carries identity, policy and model version |
Files
| File | Role |
|---|---|
| lab.py | your implementation |
| solution.py | reference; python solution.py runs an eight-part worked session |
| test_lab.py | 119 tests |
| requirements.txt | pytest |
Run
pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v
python solution.py
Success criteria
-
All 119 tests green against your
lab.py. - No matching rule denies, and the decision still names a policy version.
- Any matching DENY beats every ALLOW, and reversing the rule order changes nothing.
- The decision lists every rule that matched, sorted — not only the winner.
- An unsigned, tampered, structurally invalid or older bundle leaves the previous one live.
- An unconditional allow-everything rule is rejected; an unconditional deny-everything is not.
- A rejected push raises an alarm and does not reset the staleness clock.
- A stale bundle keeps serving until the hard stop; past it, the evaluator refuses.
- An agent with no human owner, or no pinned model version, cannot be registered.
-
draft → activeis refused; retirement is terminal. - Discovery hides a tool for five distinct reasons, and returns nothing under a categorical posture failure — but degrades to reads only under a graduated one.
- A stale evaluation blocks a high-impact action and not a read; several failures are all reported.
- A repeated read inside the TTL is served from a lease; a high-impact action never is.
- Activating a new bundle invalidates every live lease.
- A suspension alone is not seen until the lease expires — and the kill switch beats it.
-
A safety failure blocks promotion even with
min_score=0.0. - Span durations are never negative, and span ids are identical across two runs.
How this maps to the real stack
| This lab | The real thing | What we simplified |
|---|---|---|
Rule + Condition callables | OPA/Rego, AWS Cedar, or Entra CAE policies | no policy language, no parser, no partial evaluation |
PolicyEngine | an OPA sidecar, Cedar embedded, or a PDP service | no data documents, no bundle-scoped data, no query API |
PolicyBundle + HMAC | OPA bundles signed with Cosign/Notary, served from OCI | symmetric signing; the digest covers structure, not condition source |
BundleDistributor | OPA's bundle plugin with polling, signing and status reporting | no HTTP, no ETags, no persistent disk cache across restarts |
AgentRegistry | an internal service over Postgres, plus Entra app registrations | no approval workflow, no attestation, no discovery of shadow agents |
ControlPlane.discover | an MCP server filtering tools/list per principal (Phase 02) | no protocol; the filtering is the point |
ContinuousAuthorizer | Entra Continuous Access Evaluation, or an in-house lease cache | no push channel; revocation here is in-process |
EvaluationPipeline | Azure AI Foundry evaluations, Promptfoo, DeepEval, an internal harness | no LLM judge, no statistical significance, no drift detection |
Tracer | OpenTelemetry with GenAI semantic conventions (Phase 14) | no context propagation, no sampling, no exporter |
Honest limits. The bundle digest covers rule structure, not condition source — because conditions here are Python callables, and a callable cannot be hashed meaningfully. That is exactly why production policy lives in Rego or Cedar text: the text is what you sign, review, diff and attest. The kill switch is in-process, so it says nothing about the genuinely hard part — propagating a revocation to fifty PEP instances across three regions faster than their lease TTL. There is no policy test framework, and a policy set without unit tests is a policy set that will be changed by someone who does not know what rule seven does. And the anomaly score arrives as a number with no provenance; in production, deciding what feeds it is a larger design than everything in this file.
Extensions
- Swap the callables for Rego. Run a real OPA sidecar, express the same rule set in Rego, and sign the bundle with Cosign. Then hash the source and watch the digest become meaningful.
- Policy unit tests. Give every rule a fixture set — one request it must allow, one it must deny. Then break a rule and confirm the suite catches it before the bundle ships.
- A push-based revocation channel. Replace the in-process kill switch with a fan-out to N PEP instances. Measure the p99 propagation, then decide honestly whether your TTL can be raised.
- Decision-log streaming. Ship every decision to an append-only store with a hash chain (Phase 10), and answer "show me every denial for agent A last Tuesday" without a grep.
- Break-glass. Add an emergency-override path that is itself a policy decision, requires two approvers, expires in fifteen minutes, and pages someone. Emergency access nobody reviews is permanent access with a story attached.
- Shadow evaluation. Run a candidate bundle alongside the active one, record where they disagree, and ship only when the disagreements are all intended. This is how you change policy in a bank without an incident.
- Multi-region staleness. Two distributors, one source, a partition. Which region hard-stops first, and is that the behaviour you want?
Interview / resume bullets
- "Built the platform's control plane: a policy engine with default-deny and deny-overrides evaluating ABAC over subject, action, resource and environment, driven by signed versioned bundles with atomic activation — so every agent decision carries the policy version that produced it."
- "Designed for fail-static: when the control plane is unreachable, the data plane keeps enforcing the last known-good bundle, alarms on staleness, and hard-stops at a stated threshold — which avoided making control-plane availability a multiplier on the platform's."
- "Made capability discovery an authorization decision rather than a lookup, so an agent is never shown a tool it cannot use — removing a whole class of prompt-injection target."
- "Implemented continuous authorization with decision leases and a kill switch that beats the lease TTL, so suspending an agent stops in-flight work in under a second instead of at the next refresh."
- "Wired the evaluation pipeline into the authorization path: an agent whose safety suite is stale or failing cannot act, which turned model quality from a dashboard into a control."