Skip to main content

ADR-040: The Closed-Loop Control Decision — Authorized, Applied, Verified

Status

Accepted as target state — 2026-07-30. Records the decisions authorized after review of the six workstream demo scripts.

This is the single shared ADR for the closed loop. Per-workstream documents extend it and must not redefine these words. Amend this ADR rather than opening a parallel one — several ADRs describing one decision is the exact failure it exists to prevent.

Numbering note. ADR-034 through ADR-039 are claimed by an earlier open PR (the GitHub deployment gate). Three parallel drafts of this decision were independently written as adr-034-closed-loop-control-decision.md; they are superseded by this document and their substantive content is absorbed here. Any surviving reference to "ADR-034" for the closed-loop vocabulary means this document — ADR-034 was never created and never will be.

Read this first. This ADR describes a target state. Where a property is already shipped it says so with a code citation; where it is not, it says not shipped in the same sentence. No paragraph should be read as current behavior unless it is marked as one. The Target versus shipped table is the authority on what may be demonstrated as working today.


Context

The platform can already assemble a cross-system authority path, evaluate it deterministically, and produce a finding with a breakage cone. What it cannot do is finish the sentence. Work stops at a proposal, and the proposal's fate is recorded as a work-tracker status typed by a human.

Three words carry most of the product's argument, and each meant different things in different documents:

  • approved — sometimes "a human clicked accept on a remediation", sometimes "the business sanctioned this agent to do this job". Different facts, different lifetimes.
  • applied — sometimes "we generated a control artifact", sometimes "a control is in force at the provider".
  • verified — sometimes "somebody said it was done", sometimes "the provider refused the repeated action".

That ambiguity was the blocker, not missing engineering. Six workstreams could not describe one product because they were not describing the same lifecycle, and a buyer's technical reviewer comparing two of our documents would find the contradiction before we did.

The loop, in its settled form

understand authority → compare with approval → decide → apply → prove

Surfaced to users as a status rail:

Observed → Decided → Authorized → Applied → Verified

Every workstream feeds this one loop rather than defining a decision model of its own — but read that with the scoping note directly below it, not as "only the source varies":

WorkstreamWhat it contributes to the same loop
A — AirbnbKubernetes/EKS → AWS as the authority source
B — VertexA customer-published proprietary system as the authority source
C — KochAgent coverage across registry, cloud and vendor-hosted workflows
D — ExperienceThe one control-case view every workstream renders through
E — EnforcementThe apply-and-prove half, proven end to end in our own AWS organization
F — FoundationsThe installation that makes any of it safe, current and repeatable
G — EnterpriseThe same loop across isolated business units

What "the same loop" does and does not mean. Once the loop closes once — in E1 — A, B, C and G reuse the control case and the evidence contracts defined here, instead of each inventing a decision model. That is the reason to do E first.

It does not mean they become the same implementation. Each provider and each proprietary system still needs its own source adapter, its own control artifact, and its own verification ceiling — and the ceiling differs materially (see §4). An earlier phrasing — "A, B, C and G become different sources of the same decision loop" — was directionally right and too broad for engineering: it reads as though only the source varies. Read the workstream table below as what each contributes, not as what each gets for free.

Correction to a previously recorded position

Internal review material stated that "the approved job does not exist anywhere." That was too absolute, and it was repeated by more than one session. ApprovedBaseline is a real, validated, accepted structure. The accurate statement is that approved intent exists as supplied report input and not as a durable, reusable, owner-confirmed definition of a workload's business job.

This is therefore a promotion and hardening exercise, not greenfield design.

A related over-generalization is corrected here too: an employee's jobTitle is not an input to approved agent authority. It describes a human's position, not the authority a business sanctioned for a workload. Do not use it for this purpose.

Verified in code before writing this ADR

ClaimVerified at
An approved-baseline shape exists — allowed_authority, reachable_systems, constraints, expiry, owner, drift_triggersrc/reports/pre-deploy-reachability/types.ts:148; its own comment reads "§9 — proposed approval baseline. SUPPLIED, not computed."
The report API accepts and validates it — a malformed or foreign baseline is a 400 up front, not a worker failuresrc/api/routes/reports.ts:77
The decision surface distinguishes "no baseline supplied" from "drift outside the supplied baseline"src/services/decision-surface-service.ts:725 — emits unknown:baseline_not_supplied, stating "this is missing input, not proof of no drift"
…but the baseline is matched on resource namessame file — the coverage row carries caveat("name_matched_baseline")
completed → verified requires exactly one field: verified_bysrc/api/routes/mitigation-actions.ts:93 (TRANSITION_REQUIRED_FIELDS)
…and verified is terminal, so an unevidenced assertion is permanentsrc/domain/mitigation-actions/types.ts (TERMINAL_STATUSES)
Evidence packs have no control, approver or provider-receipt sectionsrc/domain/evidence-packs/types.ts:234 — nine required and two optional sections, all derived from the graph
Execution evidence carries an outcome…src/domain/evidence/types.ts:12EvidenceOutcome = "success" | "failure" | "unknown"
…and nothing reads it: a grep for outcome across src/evaluator/ returns zero linesverified 2026-07-30
dormant-authority.ts returns "recently active" on the most recent evidence timestamp, with no outcome checksrc/evaluator/rules/dormant-authority.ts:59
unproven-execution.ts returns early on any evidence record — and has no recency window at allsrc/evaluator/rules/unproven-execution.ts:39-50getExecutionEvidence(entity._id, 1) with no cutoff
Stale findings auto-resolve on a later syncsrc/evaluator/index.ts:620 (resolveStaleFindings), documented at 02-processing-pipeline §2.3 step 5

Decision

1. One definition each. These are the only meanings.

TermDefinitionIt is not
ObservedSecurityV0 has read, from a named scan run, configuration and/or execution evidence establishing that an authority path exists.Not "live". Not "exercised" — a path can be observed and never used.
DecidedSecurityV0 has produced one deterministic recommendation — the narrowest control that removes the path — with its impact analysis.Not an authorization. A decision permits nothing to happen. "We generated a control" is Decided, not Applied.
AuthorizedA named person authorized this control, at a recorded time, against a recorded context, with an expiry where one applies.Not risk acceptance (that is an attestation). Not a role or a team — a person. Not the workload's approved scope, which is what the business sanctioned the workload to do — a different object with a different lifetime, defined in §2.
AppliedA control is in force at the provider, and we have read it back from that provider with a source-stated time.Not "we sent it". Not "we generated an artifact". "A ticket was closed" is a work-tracker state, not Applied.
VerifiedEvidence produced by something other than SecurityV0 shows the control has the intended effect.Not a status a human can assert. A person typing their name is not an artifact. Not "the finding disappeared".

The two approvals — a collision in this ADR, named and resolved

This document previously used one word for two different facts, which is the exact failure it was written to prevent. Found by adversarial review, 2026-08-01.

Approved scopeApproved (the rail step)
What it isThe business sanctioned this workload to do these thingsA named person authorized this control
ObjectThe Approved Job (§2)One control definition
ActorA workload owner, at installationAn approver, per decision
LifetimeVersioned; expires; supersedesOne decision; expires; spent on use
If absentunknown — missing inputThe control is not authorized and must not be applied
Consequence of confusing themAn auditor cannot tell whether "approved" means the agent was authorized to hold the permission or the remediation was authorized to remove it

Both meanings are legitimate and both stay. What is forbidden is the bare word. In any document or screen where both could be meant — which is every control case, because the comparison block and the rail sit on the same page — write "approved scope" or "the control was authorized". Never an unqualified "approved".

Open, and it needs a ruling because it changes a word on every screen. The stronger fix is to rename the rail step to Authorized, giving Observed → Decided → Authorized → Applied → Verified, and leaving "approved" to mean the workload's sanctioned scope and nothing else. The ripple is small and now is the cheapest it will ever be: five rail statements in this PR (00-overview, 01-data-model, 07-ui-reporting, and twice here) and two files on the Kubernetes/AWS branch. Not applied unilaterally — it is the user-facing word on every screen.

Applied requires two things to be true, and the second is the one usually skipped:

  1. The change was made at the control point.
  2. A read-back has observed it there, and the observation carries a source-stated time.

2. Approved Job — promote ApprovedBaseline into a durable object

Definition. Approved names the authority a business has sanctioned for a specific identity to hold, confirmed by a named owner, with an expiry.

It is never inferred. The platform does not compute what should have been approved. Where a customer has no system of record for it, the value is customer-asserted and displayed with its provenanceasserted by NAME, DATE — and never implied to be more.

Its first version is created by the installation manifest, confirmed by a named owner. A verbal answer in a meeting is an input to that confirmation; it is never itself canonical. This is the front half of the work and it belongs to installation, not to a policy-authoring feature.

PropertyTodayTarget
OriginSupplied in a report requestCreated by the installation manifest, versioned
ScopeOne report typeTenant-level, referenced by any decision
MatchingResource namesStable entity identity
LifecycleNoneDraft → confirmed by named owner → active → superseded / expired
OwnerA string fieldA named confirmer recorded with the confirmation

Retained unchanged: allowed_authority, reachable_systems, constraints, expiry, drift_trigger — and, importantly, the existing honesty behavior.

Binding rule — the part that must not be inherited from the current shape. The Approved Job binds to stable entity identity, not resource names. Name matching silently loses its subject when a resource is renamed, and it does so without an error — an approval that quietly stops covering the thing it approved is worse than no approval, because it reads as green. Where only a name is available, the row is unknown, not covered. Any hand-authored baseline in circulation will need re-expressing.

Absence is a first-class state. "No Approved Job supplied" must remain distinguishable from "reach outside the supplied Approved Job." That distinction already works (decision-surface-service.ts:725) and must survive the promotion. Absence of an approval is unknown, never clean.

"What was approved?" stays a required column on every decision surface. Where no Approved Job exists the column shows unknown and says why. It is never removed, never blank, and never filled in by a presenter.

Explicitly not this: an approved job is not a person's jobTitle.

Explicitly not this either: the Approved Job must not grow into a general policy-authoring product. It describes the sanctioned job for the identities under an installation's proof contract, and nothing wider.

Front half owned by workstream F.

3. Control case — one object joining decision, approval, control and receipts

Today these live in three places that do not reference each other: the finding, the mitigation action, and the evidence pack. There is nowhere to put a policy document or a denial receipt.

The control case is the join:

finding             (what changed, why it matters)
+ approved job (what was sanctioned)
+ decision (narrowest control, breakage cone, unknown coverage)
+ approval (named human, timestamp, context, expiry)
+ control artifact(the exact policy document)
+ receipts (provider accepted · read-back · denied replay · reconciliation)

Why this is not more sections on an evidence pack. Evidence packs are sealed and immutableintegrity_hash, sealed_at. A control case is mutable over time: approval arrives after the decision, receipts after application, reconciliation after a later scan. The control case is therefore a separate object that references sealed evidence packs, each rung sealing its own artifact as it completes. Making packs mutable to accommodate this would destroy the property that makes them worth sending to an auditor. (An earlier draft proposed adding a control-case section to the pack; see Alternatives considered.)

The rail is reusable machinery, but it is not the mitigation lifecycle

proposed → assigned → in_progress → completed → verified (src/domain/mitigation-actions/types.ts) is a work tracker: who owes what, by when. Observed → Decided → Authorized → Applied → Verified is an evidence-backed control decision: what is true, who sanctioned it, and what proves it. The state names coincide; the meanings do not.

Control-case railReusable machineryGap
Observedfinding active
Decidedproposed
Approvedassigned / in_progress + status_history[].changed_bySemantics only
AppliedcompletedNo provider-confirmation receipt
VerifiedverifiedRequires only verified_by — a name, not a receipt

Reuse the lifecycle, the enforced transitions and the named-actor history. Do not claim the control rail already exists wholesale — the verified_by-only transition is the proof that it does not. The evidence binding is the actual work.

Reuse the machinery; do not relabel the tracker. Renaming MitigationAction to "Control Case" would import a work-tracker's semantics — including a verified reachable with a name — into an evidence-backed record. The control case is a new persisted object. It may reuse the transition-validation and status-history mechanisms; it does not inherit the tracker's states.

The rail is a buyer view. The persistence model is an append-only event history.

Observed → Decided → Authorized → Applied → Verified is what a customer is shown. It is not the storage model, and building it as a five-value status column guarantees the first rejected approval or failed application has nowhere to live.

The durable record is an append-only event history bound to one stable control-case ID. Each event carries its actor, its timestamp, and its required artifacts — an event that cannot produce its artifacts cannot be appended. The rail state a customer sees is derived from that history, never stored as the source of truth.

These events are legal and must all be representable:

EventRequired artifactsEffect on the derived rail
case_openedfinding reference, scan run ID, Approved Job version or unknownObserved (the rail's initial state)
decision_recordedcontrol definition, impact analysis, unknown-coverage list→ Decided
authorization_grantednamed approver, timestamp, context hash, expiry if any→ Authorized
authorization_rejectednamed actor, reason→ Decided (returns to the decider)
authorization_withdrawnnamed actor, reason, prior approval reference→ Decided
authorization_expiredprior approval reference, expiry that elapsed→ Decided; the control is no longer authorized
approved_job_expired / approved_job_supersededprior and successor Approved Job versionsapproval invalidated; case returns for re-decision
application_attemptedtarget control point, control artifact hashno rail advance on its own
application_failedprovider error, control artifact hash→ Decided, with the failure visible
readback_confirmedprovider-stated active version, source-stated time→ Applied
readback_mismatchexpected vs observed version, source-stated time→ Applied is revoked, not merely annotated
verification_succeededreplay result, provider denial receipt, control-case ID, policy hash→ Verified
verification_failedreplay result showing the action still succeeds→ Applied, flagged; the control does not work
verification_unavailablethe stated ceiling for that control pointchange_confirmed_effect_not_observable (terminal, honest)
reconciledscan run ID, finding resolution referencecorroboration only — never advances the rail by itself
reconciliation_not_observablethe control point, and why the graph cannot see the controlcloses moment 2 honestly; the finding becomes mitigated by an active control (see below)
reopened_after_driftdrift finding reference, baseline version datecase reopens; prior verification retained in history, not deleted
control_rolled_back / control_removedactor, reason, provider read-back showing removal→ the case closes as no longer in force

Four rules follow from this shape and are the point of adopting it:

  1. Nothing is ever overwritten. A verification that later proves wrong is superseded by a new event, not edited away. The history is the audit trail; the rail is a projection.

  2. readback_mismatch revokes Applied. A control we believed was in force and is not is the most dangerous state in the model, because everything downstream reads as protected.

    And revoking the standing is not sufficient — the false claim must be retracted. Added 2026-08-01 after adversarial review. Between the moment we recorded a control as in force and the moment a read-back contradicted it, SecurityV0 told people a path was protected when it was not: other cases, reports, exports, tracker tickets and any decision taken on the strength of them. Silently flipping the case to contradicted leaves every one of those standing.

    A mismatch therefore requires a downstream ledger: what consumed this case's standing during the window, so the false assurances can be withdrawn and their owners told. Without it we have built a product that can be confidently wrong and then quietly correct itself, which is worse than not making the claim.

    This is a product requirement, not a screen affordance — the ledger has to exist in the domain before any surface can render it, and the exposure window is only meaningful once a read-back cadence exists to bound it.

  3. A terminal state can be honest rather than green. change_confirmed_effect_not_observable is a legitimate ending on control points that cannot produce a denial.

  4. Reopening does not erase. Drift after verification appends; it does not reset the case to a clean slate, or we lose the evidence that the control once worked.

mitigated by an active control is a new finding state and it does not exist yet. FINDING_STATUSES is active | acknowledged | remediated | false_positive (src/domain/findings/types.ts). None of those is correct for a finding whose path is still configured but whose exercise is refused at the provider: remediated is false (nothing was removed) and active is misleading (the action is blocked). Adding this state is part of P4, and it must not be approximated by reusing remediated — that would reintroduce, in the data, the exact "the grant was removed" claim §7 forbids in the narrative.

Ruling, 2026-07-31: it is a terminal standing beside Verified, not below it — and the case is closed. Two things follow, and both are deliberate:

  • It ranks with Verified, not under it. The provider refuses the action; that is the same causal proof Verified rests on. What is missing is corroboration in our own data, which is a limit of our ingestion rather than a weakness in the control. Rendering it as a lesser Applied would state the opposite of what the evidence says.
  • The case closes; the denial is not kept alive by a recurring replay. A control case records what was true of a named configuration at a named time, and every receipt is already scoped that way. A recurring re-replay would make the case's standing depend on a job that could silently stop, which is a worse failure than an honestly-dated closure.

Amended 2026-08-01, after adversarial review. The original justification was partly wrong and the correction matters.

I justified closure partly on the grounds that the case "reopens on its own if the configuration moves". For the flagship case it does not, and it cannot. reopened_after_drift fires on drift the connector can see. The control here is an SCP, which we do not ingest — so the single most important drift, the control being removed, is invisible to us by exactly the same mechanism that makes reconciliation impossible. Someone detaches the policy and nothing in SecurityV0 changes.

I also deferred the residual risk to "a fleet-level concern". That was a hand-wave: no such assurance exists, it has no owner, and nothing tracks it. Deferring to a capability that does not exist is not a resolution.

The corrected position:

  • A case may close as a dated historical record. "AWS refused this action at 10:04 UTC on 2026-07-30, citing this policy" is true then and stays true.
  • A standing of mitigated by an active control is a present-tense claim and must not be displayed without its age. Wherever that standing appears it carries when it was last confirmed, and it ages visibly. A CISO asked "is that control still in force?" must be able to answer from the screen, or see plainly that we cannot answer.
  • Where the control is outside our ingested model, re-confirmation cannot come from a scan. It requires either re-executing the provider check or ingesting the control type. Both are real work with real owners; neither is a footnote.
  • Fleet-level control assurance is therefore a named dependency of this state, not an aside. Until it exists, mitigated by an active control is defensible only for as long as its confirmation is recent — and the surface must say how recent.

Consequence for sequencing: shipping this standing without an assurance age would let a dated proof read as an ongoing guarantee. That is the same class of error as a permanently-pending rung, and it fails in the direction that costs the most trust.

Vocabulary ruling, same date. Approved Job is the domain object — the durable, owner-confirmed record described in §2, and the name used in schemas, APIs and this ADR. approved scope is the user-facing term in UI copy, column captions and empty-state remedies. One term per layer; do not mix them on a screen.

4. Verified requires an artifact, and there are two verification moments

The confirmed defect. completed → verified requires exactly one field: verified_by. A name. No provider receipt, no read-back, no replay. And verified is terminal, so an unevidenced assertion is permanent.

The decision: Verified becomes impossible without required evidence — at minimum the read-back observation with its source-stated timestamp, and, where the control point can produce one, the provider's own denial of the repeated action.

Migration note. Tightening TRANSITION_REQUIRED_FIELDS["completed->verified"] in place would begin rejecting existing callers. The evidence requirement belongs on a new, evidence-bearing transition rather than as a silent tightening of the work tracker.

The two moments

1 · Enforcement verification2 · SecurityV0 reconciliation
QuestionDid the provider block it?Is the dangerous path gone from the graph?
Who produces itThe providerSecurityV0
ArtifactRead-back of the active policy version and an identical replay returning a denialThe path is absent on a later refresh; the finding auto-resolves to remediated, stamped resolved_by_sync
TimingSeconds to minutesNext triggered or scheduled scan — lags by the cadence
Causal?Yes — the replay ties the denial to the controlNo

Both timestamps belong on the control case:

Provider enforced        — 10:04
SecurityV0 reconciled — 10:07

Auto-resolution is valuable but it is not causal. resolved_by_sync proves the path was absent as of that scan. It does not prove a particular control caused the absence — the workload may have been deleted, or a source may have gone unread. Causality comes from moment 1. Never present reconciliation alone as proof that a control worked.

Moment 2 does not fire for controls the graph cannot see. Reconciliation requires the applied control to change something the connector ingests. An AWS SCP changes nothing the connector reads: the connector does not ingest SCPs, and iam_policy_parser.py skips every Effect: Deny statement, so the configured Allow path survives in the graph after AWS starts refusing the call. For such controls the case ends at provider verified and the finding is marked mitigated by an active control — never "the grant was removed". See §7.

Verification is not uniform across control points

A demo that implies otherwise will be caught. The ceiling is agreed at installation, not argued about at verification time.

Control pointWhat can be verifiedCeiling
AWSActive policy and a CloudTrail AccessDenied on the repeated actionStrongest: the change and the block
AzureThe assignment changedThe 403 lands in the caller's logs, not ours — the customer supplies it
EntraThe grant is goneThe effect is not verifiable until existing tokens expire (60–90 min, randomised). Held as a live closed-loop demo control for this reason.
GitHubRepository-scope reduction or App suspension, with a provider denial on replayStrong, when the customer replays
Customer-published systemWhatever that system exposes; the denial its system issued can be published back as an execution_evidence recordMust be stated in the manifest at install time

A decision carries the ceiling it was verified at. change_confirmed_effect_not_observable is a legitimate terminal state. Forcing every control point into AWS's story is how a demo becomes a lie. Where a surface cannot produce a provider-issued denial, name the receipt it does produce and call that the ceiling.

One precision that must not be lost: a CloudTrail AccessDenied proves the API call was refused. It does not prove traffic did not leave. An egress claim needs a canary receipt or a VPC flow-log ACCEPT/REJECT record, separately. The two are not interchangeable.

5. Who applies

The customer applies, by default and in every customer environment. SecurityV0 holds no credential that can change anything in a customer's environment, and the installation contract records that explicitly.

Workstream / surfaceControl pointWho applies
A — Airbnb (Kubernetes → AWS)AWS IAMCustomer
B — Vertex (proprietary system)Vertex's own gateway / permission serviceCustomer, through their existing control process
C — KochCustomer-nativeCustomer
E1 — Enforcement flagshipAWS SCP in SecurityV0's own AWS organization, a single OUSecurityV0 — and only here
G — EnterpriseCustomer-native; borrows E for enforcementCustomer, per business unit

This is not a current limitation awaiting a release. It is the read-only connector position and it is held. E1 exists precisely so the capability to compile and apply a provider-native control can be proven without a customer, in an environment we own and can say so about.

The publishing contract in B carries authority and execution data in. It is not the mechanism that changes the customer's system. The customer's existing control process makes the change with their own credential; the contract's role afterwards is to report the resulting evidence — the next publish no longer carries the removed grant, and the denial their system issued can be published back as execution evidence (carrying outcome: "failure", per §6).

Who performs the replay

Applying and replaying are separate acts with separate actors, and conflating them is easy because both happen "at the provider".

In a customer environment, the customer replays. Replaying a prohibited action means acting as the denied principal. SecurityV0 holds no credential that can do that, and acquiring one would be a larger ask than the write access we already decline. The customer performs the replay through their own process; we receive the artifact and bind it to the case. Where a customer will not or cannot replay, the case ends at Applied with a read-back and no enforcement receipt — an honest ceiling, recorded as such, not a failure.

In E1 only, SecurityV0 replays, because the principal, the account and the credential are all ours. That is the whole reason E1 exists as a capability proof rather than a customer engagement.

A surface that shows SecurityV0 replaying inside a customer tenant is describing something we do not do and have not asked to do.

6. Execution evidence must be outcome-aware — a prerequisite, not a follow-up

ExecutionEvidenceDoc already carries EvidenceOutcome = "success" | "failure" | "unknown". The rules ignore it — a grep for outcome across src/evaluator/ returns zero lines — and treat the existence of a record as proof of activity.

RuleBehaviorConsequence of a denied attempt
dormant-authority.ts:59Takes the most recent evidence record; returns "not dormant" if its timestamp is inside the window, without inspecting outcomeA blocked attempt makes a dormant identity look recently active, suppressing the finding
unproven-execution.ts:39Returns "proven" on any evidence record for the entity or its RUNS_AS targets — no recency window at allA single denied attempt permanently suppresses unproven_execution for that entity

unproven-execution.ts is the sharper of the two: with no recency window, one denial silently converts a never-successfully-used path into a "proven" one for good.

Why this blocks the loop. The loop asks a customer to repeat a prohibited action and publish the resulting denial as evidence. On today's rules, doing exactly what the loop asks would:

  1. make the identity appear successfully active,
  2. suppress dormant_authority and unproven_execution on the very path a control was just applied to, and
  3. do so silently, with no counter and no completeness signal.

The evidence proving we fixed something would delete the finding that said it was broken — at exactly the moment the flagship demo is trying to prove enforcement worked.

Three meanings stay distinct everywhere execution evidence is consumed:

MeaningOutcomeWhat it licenses
Successful executionsuccessThe authority was exercised. Dormancy and unproven-execution rules stand down.
Attempted but deniedfailureThe authority was not exercised. Evidence about the control, not about the identity's activity. Dormancy rules unaffected.
Outcome unknownunknownNeither conclusion available. Must never be silently promoted to success.

A denial is excellent proof that enforcement worked. It is not proof that authority was exercised successfully. Those are close to opposite conclusions about different subjects.

This is the same class of error as the document's existing warning that execution_count_30d = 0 means "we did not see anything", not "nothing happened":

Zero is not "idle." A record is not "success."

Both are values that read as facts about the customer's system when they are facts about our observation of it. A denied attempt must be positive evidence for a control case (moment 1) and not evidence of exercised authority — both readings available from the same record.

Outcome-aware evaluation is a prerequisite for publishing denial receipts. Detail in Execution Evidence Fidelity §11.

7. E1 — the flagship enforcement flow, in SecurityV0's own AWS organization

Why our own organization: it needs no customer, so there is no trust ask, no pilot negotiation and no scheduling dependency. What it does and does not prove: it proves the decision compiles into a control that works and does not decay. It does not prove a customer would grant us the authority to do it in theirs — and we do not want that authority.

Section ownership. The authoritative E1 narrative belongs to the enforcement workstream and lives in its own page. What follows is the summary ADR-040 carries so the vocabulary has a worked example. It has been corrected against the round-4 verified facts below; where the enforcement workstream's page and this summary differ, that page wins and this section is amended to match.

Scenario — an indirect provider-action authority path. An evaluation worker holds lambda:InvokeFunction on a helper function whose role can act on a protected resource. The worker never gained the downstream permission directly. It gained an indirect authority path through another workload, expressed entirely through IAM and observable through CloudTrail.

worker → execution role → lambda:InvokeFunction → helper → helper role → protected resource

Do not call this an observed internet escape. We can represent the IAM path above. We cannot currently establish "the worker has no internet route" or "the helper has internet access": that needs route tables, NAT gateways and security groups the connector does not read. egress_category is never a network-reachability fact. For a Lambda it comes from _classify_lambda_egress(function_name, env_var_keys) — substring matching over the function name and its environment-variable keys, never their values (transformer.py:1047, :3713). Elsewhere it is _classify_secret_egress() over a secret's name, description and tags (:1574, :3632), or a hardcoded literal (:1392). No network object is inspected by any of the three.

Test-fixture safety. The pre-deny attempt is made against a disposable, recoverable fixture — a versioned sandbox bucket and a throwaway object, with rollback tested before the policy is attached. The dangerous effect must never touch production data, and an unrecoverable deletion may never be an acceptance step. s3:DeleteObject may remain the denied action.

The beat the whole flow exists for is the identical replay. Everything before it is setup. The durability beat is the most under-rated — adding a permissive Allow and being denied anyway demonstrates the fail-static property viscerally and proves we are not in the path.

SecurityV0 never sits in the request path. If the platform is down, an applied control is still in force.

E1 ends at provider verified. It cannot reconcile the graph, and it must say so.

This is the correction that most changes the demo. An earlier draft of this flow ended with a later scan showing the path gone and the finding reconciling. That cannot happen, for two independent reasons:

  • the AWS connector does not ingest SCPs, so an attached deny is invisible to the graph; and
  • iam_policy_parser.py skips every Effect: Deny statement — its own docstring: "Deny statements are skipped — they affect evaluation but are not materialized as graph permission nodes." (iam_policy_parser.py:126-131; the skip itself is at :86-87 and :155)

So after AWS correctly denies the call, the configured Allow path is still in the graph. Verification moment 2 (§4) does not fire for an SCP-based control, and claiming it would be a demonstrable falsehood at the climax of the flagship demo.

What E1 does instead: it ends at provider verified, and says out loud that the graph still overstates reach because SCPs are not modeled. The control case moves the finding to mitigated by an active controlnever to "the grant was removed", because no grant was removed.

Rejected alternative: make SCP and Deny ingestion an E1 prerequisite. It would restore reconciliation and it is the larger piece of work — a connector change plus a parser change plus a policy-evaluation model that can express deny precedence. Not chosen. Recorded here so the option is visible rather than forgotten.

The consequence nobody sees until they click back

A path mitigated this way stays in the Access Paths list, and in the graph, permanently. That is the correct behavior — it is configured, and we render configuration — but it means every surface that lists paths will show this one as live, next to a case that says it is mitigated.

Any list or graph surface that can render a path must be able to render "mitigated by control <id> · path not retired". Without it, a reviewer who follows the flagship demo and then clicks back to the list is shown an unqualified live path immediately after being told it was handled, and the strongest moment in the demo becomes the weakest. This is a small treatment on a shipped surface, and it is a precondition for demonstrating the E1 case at all — not a follow-up to it.

Why this control, and not the obvious alternatives

ChoiceReason
Deny s3:DeleteObjectA consequential action, so the stakes are legible — attempted only against a disposable, recoverable fixture with rollback tested beforehand. An unrecoverable deletion may never be an acceptance step
Not sts:AssumeRoleThat is credential minting — grant-time, not action-time. A technical reviewer will say so
Resource-anchored, not principal-anchoredThe downstream call is made by the helper's role, not the worker's. A principal-anchored deny on the worker misses it entirely — a confused-deputy hole
Principal allowlist, never a denylistA denylist built from a poll silently misses any principal created since the poll — precisely the adversary we exist to catch
Deny only; no grant removalRemoving a grant is write-back. Acceptable in our own sandbox, but it has no ADR behind it as a product claim and it muddies the property this flow exists to prove

Emitter safety constraints — non-negotiable

These are why this is safe to do at all.

  • Our own AWS organization, a single OU, never the root.
  • Deny statements only. Never emit, modify or reference an Allow — AWS documents that removing FullAWSAccess makes all member-account actions fail.
  • Always-present condition keys only (aws:PrincipalArn, aws:RequestedRegion, aws:PrincipalAccount). Under StringEquals an absent key evaluates false and the deny silently does not fire — a fail-open deny wearing a fail-static label.
  • Resource-anchored with a principal allowlist, never an identity denylist.
  • IAM Access Analyzer ValidatePolicy (policyType SERVICE_CONTROL_POLICY) as a pre-attachment check. It validates grammar and best practice, not behavior — a staged OU rollout is the real pre-flight.
  • Never attach a policy in a customer's organization. That needs organizations:AttachPolicy in their management account — the most powerful write in an AWS organization. We do not ask for it, and no document may imply a path that needs it.

Known limit — the read-back is not a connector round-trip

The AWS connector discovers Organizations accounts and OU paths but does not ingest SCPs. So the applied-policy read-back is a direct AWS call by the emitter, not a connector observation. Reading an applied control back through the same connector that found the problem would be a better story, and we do not have it.

E2 — preventive: the same product, before production

E2 is a preventive variation of E1, not a separate engineering stream. Same decision, same control, same receipts — the only difference is when the control exists. The control is active before the workload starts, so the first prohibited invocation is denied.

Nothing is detected, because nothing happened. There is no successful call to point at, because there never was one. This is prevention without an inline gateway — nothing routes through SecurityV0; the configuration changed before it mattered. CI/CD deployment blocking is held and is not the control point here.

E3 — public-incident reconstruction

Positioning and demo content only, explicitly labeled as reconstruction from public disclosures. It must never be presented as a faithful executable replay and must not become a product build.


The loop, end to end

Who acts at each step, and where the two verification moments fall.

Two readings of this diagram, and they differ at the last step.

  • Customer surfaces (A, B, C, G): the Customer participant is the customer, applying through their own control point, and moment 2 fires when the applied change is something the connector reads.
  • E1: the Customer participant is SecurityV0 acting in its own AWS organization — and the last two steps do not happen. An SCP is invisible to the connector, so the flow ends at moment 1 and the finding becomes mitigated by an active control. See §7.

Installation and the first Approved Job

An installation is where the Approved Job first becomes real, because it is the only moment where a named human from the customer is asked to state the job on the record.

A verbal answer is never canonical, and an unconfirmed job is unknown — a displayed state, not a blank. This mirrors the behavior the decision surface already implements for a missing baseline.


What this loop does not do

Design these in; do not append them.

Not coveredWhy
Detection during an attack sequenceIngest is scheduled full-snapshot polling — 6h on AWS where a cadence is configured, human-triggered on the Microsoft path. No webhook receiver exists; event-driven triggers are recorded as rejected in ADR-027.
Internet-connection attemptsexternal_egress reads entity.properties.egress_category — a configuration read. No attempt detection of any kind exists.
Forged, correctly-signed tokensStructural. A forged token changes no configuration, and a configuration reader cannot see it by construction.
Paths blocked by controls we do not modelConditional Access, deny assignments, ABAC conditions, resource locks and PIM are registered as not-modeled (src/rehearsal/coverage.ts:377-412); four carry direction over_states_reach and PIM carries both. Our own code says the analysis over-states reach.
Drift older than the baseline windowDrift rules diff against the oldest of a 100-version window. After 100 scans, "since baseline" silently means "since ~100 scans ago".
Breakage cones at enterprise scaleRevocation rehearsal hard-throws above 10,000 entities (SYNC_MAX, src/rehearsal/service.ts:55). The async tier is an enum value, not a code path.

The enforcement is real-time; the detection is periodic. Once a deny is attached the provider evaluates it on every request, forever, with SecurityV0 nowhere in the path. What is periodic is how fast we notice something worth denying. These are different clocks and documentation must not blur them.


Target versus shipped

No blurring. This table is the authority on what may be demonstrated as working today.

CapabilityStateEvidence
Authority path materialization across packaged and published sourcesShipped
Deterministic findings on published dataShippedEvaluator rules are overwhelmingly source-agnostic (src/evaluator/rules/); the exact count is unverified here — do not quote a ratio without checking it
Impact analysis / revocation rehearsal, bounded to the synchronous tenant limitShippedSYNC_MAX = 10_000
ApprovedBaseline as supplied report input, validated, with drift computed against itShippedpre-deploy-reachability/types.ts:148, reports.ts:77
Missing baseline renders as unknown, not cleanShippeddecision-surface-service.ts:725
Mitigation lifecycle, enforced transitions, status_history.changed_byShippedmitigation-actions/types.ts
Named approver, expiry, conditions, staleness on a review recordShipped as attestationdomain/attestations/types.ts
Finding auto-resolution on a later syncShipped, and not causalevaluator/index.ts:620
EvidenceOutcome on execution evidenceShipped (unread)field exists; grep outcome src/evaluator/ = 0 lines
Durable, owner-confirmed Approved Job bound to entity identityTarget
Approval presented as the gate on a controlTarget
Outcome-aware evaluationTarget
Verified gated on a required artifactTargettoday completed→verified needs only verified_by
Read-back after a change, as a modeled stepTarget
Control case joining decision, approval, control and receiptsTargetpacks have no such section — evidence-packs/types.ts:234
mitigated by an active control finding stateTargetFINDING_STATUSES is active | acknowledged | remediated | false_positivesrc/domain/findings/types.ts
reconciliation_not_observable as a first-class outcomeTargetno reconciliation status domain exists
Provider-native control applied in SecurityV0's own AWS organization (E1)Targetno mutating customer-system call exists in src/
Provider-native control applied in a customer environmentHeld

Implementation dependency order

The five decisions are not peers. Dispatching them as a flat list produces a control case with nothing to join and a verification rung with nothing to verify.

ArtifactDepends onAcceptance evidence
P1Durable Approved Job schema and APIexisting ApprovedBaselineTenant-scoped, versioned, stable IDs, named owner, constraints, expiry — and reused by two decisions, not one
P2Outcome-aware execution evaluationOnly successful execution proves activity; denied, failed and unknown never suppress dormancy or unproven-execution — and the connector-computed activity properties must be audited for the same defect, because both rules short-circuit on properties.last_observed_execution_timestamp / properties.execution_count_30d before any evidence record is read, and those carry no outcome concept at all. Filtering EvidenceOutcome alone does not meet this criterion
P3Provider control and verification receiptsP2Applied-control hash, provider read-back, provider outcome and reconciliation time stored separately — not collapsed into one field
P4Control case domain, API, UIP1 + P3One stable case ID joins finding, Approved Job, decision, approval, exact control version, receipts
P5E1 AWS proofP1–P4Sandbox OU, validation, named approval, attachment, read-back, identical replay denied, later Allow still denied, rollback tested

Sequencing. P1 and P2 may run in parallel — P1 is a schema-and-API promotion, P2 is a rules-and-migration change, and they share no surface. P3 follows P2, because receipts that a denial is a denial are meaningless while the evaluator reads evidence existence. P4 follows P1 and P3, because the case has nothing to join until the Approved Job is durable and receipts exist. P5 starts after domain acceptance passes — not after the code merges.

One owner per artifact. Two owners on P3 or P4 reproduces exactly the failure this ADR was consolidated to fix.

Documentation ownership is not implementation ownership. Owning the page that describes an artifact confers no claim on building it, and nothing in this ADR assigns implementation. The workstream attributions elsewhere in this document record who wrote the decision down. P1–P5 above are dispatched separately.


Held — not roadmap, not target state

Recorded so they are not quietly re-adopted. A document that lists these as planned is wrong.

  • Customer-environment automatic write-back. SecurityV0 changing a customer's configuration without a human applying it. Any change to this position needs its own ADR and a separate written agreement with the customer.
  • Airbnb CI/CD blocking — a deployment gate that holds a customer's deploy.
  • Autonomous enforcement — any control applied without a named human approving it.
  • Publishing denial records before outcome semantics are fixed (§6). This is a sequencing gate, not a permanent hold.
  • Koch / Happy Robot as a modeled workflow without customer confirmation.
  • Entra as a live closed-loop demo control — the grant's removal is visible, but the effect is not verifiable until tokens expire, so there is no prompt replay artifact.
  • The public OpenAI / Hugging Face reconstruction as a faithful executable replay.

Consequences

Good.

  • One vocabulary across seven documents. A reviewer comparing any two finds the same definitions.
  • The Approved Job is a promotion of working code, not a new subsystem, and the unknown handling the decision surface already implements becomes the model for the whole column.
  • Verified stops being assertable — the single change that most improves how our evidence reads to a hostile reviewer.
  • Naming the outcome defect before building denial receipts avoids shipping a demo that contradicts itself at its climax.
  • Once E1 works, A, B, C and G reuse the control case and evidence contracts rather than each inventing a decision model. Each still needs its own source adapter, control artifact and verification ceiling — the reuse is the contracts, not the implementation.

Costs and risks.

  • Requiring artifacts for Verified means some decisions sit in Applied indefinitely on control points that cannot produce a denial. That is honest and it will look like incompleteness. It is preferable to the alternative.
  • Surfaces that today show a verified count will show fewer, and that is correct.
  • Outcome-aware evaluation changes unproven_execution and dormant_authority behavior for every existing tenant, not just enforcement ones. It needs its own migration thinking.
  • Binding the Approved Job to entity identity will surface unknown rows on day one. That is correct behavior and will look like a regression to anyone comparing row counts.
  • Two verification moments is more UI than one, and the second lags by the scan cadence — which makes the cadence visible to customers. Honest, and also a support conversation. Collapsing them is a recurring temptation that loses the distinction between enforcement and reconciliation.
  • The Approved Job is customer-asserted at first. Its provenance must be displayed every time it is shown, or it will be mistaken for something we derived.
  • The control case is a new object with its own lifecycle, and it must not be solved by making evidence packs mutable.

When to reconsider

  • A provider offers push notification of policy state, making reconciliation immediate — then the two verification moments may collapse into one for that surface only. Do not generalize it.
  • The Approved Job proves unmaintainable at enterprise scale (many agents, frequent legitimate change) — reconsider whether approval attaches to the job or to a class of jobs, before weakening the named-owner requirement.
  • A customer demands automatic write-back into their environment. That reopens a held item and needs its own ADR; it does not fall out of this one.

Alternatives considered

  • Add a control-case section to the evidence pack. Rejected: packs are sealed at a point in time (integrity_hash, sealed_at) and a case accrues artifacts afterwards. Mutability would cost the auditor-grade property that makes packs worth sending to an auditor. The case references packs instead.
  • Treat the mitigation work-tracker statuses as the control rail directly. Rejected: it overstates what exists. verified there is a human assertion, and conflating the two is how the documents drifted in the first place.
  • Keep the approved baseline name-matched for now. Rejected: a rename silently voids coverage with no error, and the failure mode is a green row that is not true.
  • Deny by identity denylist rather than resource anchor plus principal allowlist. Rejected: a denylist built from a poll misses any principal created since the poll — the exact adversary.

References