EVD-9 window 20260905-evd9 — independent operator review
Prepared 2026-09-05 by a second live-deploy operator. This is a REVIEW of a window another lane is executing, not a competing plan and not authorization to run anything.
The window is owned and executed by the SPRINT_20260824_010 P8-8 lane, which set SPRINT_20260722_011 EVD-9 to DOING in 0ac6d5e237 on the owner’s authorization. This reviewer executed nothing: no container, database, route, grant, volume, network or image was created, changed, stopped or recreated, and neither the runbook nor the sprint row was edited. Every measurement below is read-only.
Step authority remains evidence-evd9-data-move.md. Where this review and that runbook disagree, the runbook is what the operator runs; the findings here are proposed corrections for its owner to fold in. The executing lane owns that file in-window.
Scope, as commissioned: an independent read of the same receipts, the runbook defects found offline, the corrected scope mapping and negative control, the ROA-1 registration inventory for the Evidence family, and a pass over runbook sections 8-13 for steps that cannot be performed as written.
1. Window state at review time
Reconstructed from receipt mtimes under the gitignored tmp/evd9-20260905-evd9/ (host clock UTC+03:00; UTC is local minus three hours). The window had not reached its point of no return.
| Local | Phase | Evidence |
|---|---|---|
| 09:18:42 | Row claimed DOING | commit 0ac6d5e237 |
| 09:22:54-09:24:45 | Preflight A: repo status, build-boundary, P19 transition, live topology, rollback capture, database and source-count census | preflight/{repository-status,build-boundary,evidence-p19-transition,containers-before,volumes-before,databases-before,source-real-counts-nonzero}, rollback/*.ABSENT.txt, rollback/stellaops-{attestor-tileproxy,tsa}.* |
| 09:41:23-09:47:26 | Preflight B: HEAD pin, digest-pin override, window approval, Authority descriptors/tests/token/feed, source grant census, both grant plans | preflight/{repository-head,evidence-digest-pin.override.yml,window-approval,authority-evidence-*,source-grants-before,grants-forward.sql,grants-rollback.sql} |
| 09:49:32 | Gateway rollback capture | rollback/stellaops-router-gateway.* |
| 09:57:04 | Three-schema full dump superseded (see D-G) | backup/source-domain-full.dump.three-schemas-superseded |
| 09:58:25 | Writer fence, absent-predecessor variant | preflight/writer-fence-stop.txt |
| 09:58:45 / 09:59:16 | Source frozen: two count snapshots 31 s apart, byte-identical | parity/source-counts-{a,b}.tsv, 32 tables |
| 09:59:37-09:59:58 | Fenced data-only dump, sha256, list, circular-FK gate | backup/source-domain-fenced-data.*, backup/evidence-bundles-supersession-gate.txt |
| 10:01:50-10:02:17 | Corrected full dump, isolated restore test, restore proof | backup/{source-domain-full.dump,restore-test.log,restoretest-counts.tsv}, preflight/backup-restore-proof.txt |
Not reached at review time: runbook section 5 (provision and converge the target) and section 6 (the copy). Confirmed independently: stellaops_evidence does not appear in pg_database on stellaops-postgres, and neither stellaops-evidence-web nor stellaops-evidence-worker exists.
Frozen source, as measured by the window itself
32 domain tables (migration ledgers excluded). Non-empty: attestor.entries 26, attestor.local_transparency_entries 3, evidence_locker.decision_capsules 4, evidence_locker.evidence_bundle_signatures 8, evidence_locker.evidence_bundles 8, evidence_locker.evidence_schema_version 5, evidence_locker.verdict_attestations 11, proofchain.graph_edges 9, proofchain.graph_nodes 8, proofchain.predicate_type_registry 35. Empty and load-bearing for sections 7 and 9: attestor.verdict_ledger 0, proofchain.spines 0, evidence_locker.evidence_artifacts 0.
The parity denominator has moved since the rehearsal. The 2026-09-03 receipt records parity 31/31; this window’s frozen snapshot holds 32 tables. 31/31 is therefore not the expected value for this window and must not be used as the acceptance number.
2. Preflight gate, evaluated independently against the runbook’s section 2 step 4 list
Receipt required by :283-296 | State when reviewed |
|---|---|
caller-repoints.tsv | present |
console-evd6.txt | absent — the one open row; preflight/console-build.log was in flight |
backup-restore-proof.txt | present |
authority-evidence-descriptors.txt | present |
authority-evidence-tests.txt | present |
authority-evidence-token-claims.txt | present |
authority-evidence-feed.txt | present |
evidence-web-image.txt / evidence-worker-image.txt | present, digest-form (see D-F) |
grants-forward.sql / grants-rollback.sql | present |
window-approval.txt | present |
Independently re-derived, agreeing with the lane’s receipts: build-boundary -Check OK with 0 unpinned, 0 stale, 0 grown, 0 activation mismatches, 0 impure client SDKs; check-evidence-p19-transition.ps1 -RequirePost reports the coherent post-state (0 target pairs, 0 active predecessor hosts); stellaops_evidence absent; both target containers absent; the three predecessor writers absent.
Two observations the receipts do not resolve, recorded as observations rather than defects:
preflight/volumes-before.txtis zero bytes. The file cannot distinguish “measured, and the carried volumes are absent” from “not measured”. The carried volumes really are absent (independently confirmed:compose_evidence-dataandcompose_attestor-proofchain-casdo not exist), so the conclusion is right; the receipt cannot carry it. A receipt whose empty state is indistinguishable from its unmeasured state should record the fulldocker volume lsand grep at read time.preflight/source-sessions-after-fence.txtshows three live sessions onstellaops_platform(twostellaops-platformat 172.19.0.7, onestellaops-schedulerat 172.19.0.38), allidle. Under the absent-predecessor variant there is no Evidence writer to fence, and the byte-identical 31-second count snapshots are the real freeze proof, so this is not an escape. It is worth naming in the closeout record so a later reader does not treat the non-empty session list as one.
3. Point of no return, and rollback per phase
The point of no return is runbook section 6, the pg_restore into stellaops_evidence. Before it the target does not exist and abandoning the window costs nothing on this estate, because the absent-predecessor variant means there is nothing to restart. After it the target holds a copy of production data whose provenance is one specific frozen instant of the source; a later rollback cannot reuse it, and a second attempt needs a fresh converge and a fresh freeze.
| Phase | Rollback | Artefact it depends on | Cost |
|---|---|---|---|
| Sections 1-2 preflight | nothing to undo | none | zero |
| Section 3 backup | delete artifacts | none | zero |
| Section 4 fence | nothing to restart (absent-predecessor variant) | rollback/*.ABSENT.txt | zero |
| Section 5 converge | DROP DATABASE stellaops_evidence and re-provision, or leave it converged and empty; nothing reaches it | none | minutes |
| Section 6 restore | the target becomes evidence: retain unchanged, do not drop | none | window re-runs end to end |
| Section 7 parity fails | keep the fence, change no grant or config, retain the target as the primary evidence | parity/* | as above |
| Section 8 start/repoint | stop both successors; restore the captured gateway/Console blobs and recreate only those on their captured config_files lists | rollback/stellaops-router-gateway.*, Console blobs | ~15 min |
| Section 10 revoke | grants-rollback.sql | that file, reviewed pre-window | minutes |
On this estate rollback reduces to: stop both successors, restore the gateway and Console blobs, leave stellaops_platform and stellaops_evidence intact. No predecessor restart is possible, and none is needed for parity with the pre-window state, because the predecessors served nothing.
4. Defects in the runbook
Each was verified against src/, the compose overlay, or the live estate on 2026-09-05. Line references are to evidence-evd9-data-move.md unless another file is named.
D-A — two unbounded health-wait loops
:516-518 (section 5 converge) and :772-774 (section 8 step 1) are until docker inspect -f '{{.State.Health.Status}}' stellaops-evidence-web | grep -qx healthy; do sleep 2; done. There is no deadline and no failure branch. until treats a failing docker inspect as “not yet”, so a container that exits, never becomes healthy, or does not exist spins forever while writing stderr every two seconds.
The successor has a documented route into exactly that state: docker-compose.evidence.yml:175-179 records that a missing Authority__ResourceServer__Authority starts cleanly and then answers 500 to every request including the anonymous /health/ready, which the EVD-8 rehearsal saw as Up (unhealthy) with no startup error anywhere in the log.
Proposed fix: bound both loops with a deadline, dump docker logs on expiry, and exit non-zero.
D-B — section 9 names no scopes, and the policy labels are not the claims
Section 9 lists three forcing functions and never states which scopes their endpoints require. The obvious source for them is the route inventory (src/Evidence/__Tests/StellaOps.Evidence.WebService.Tests/Fixtures/consolidated-route-inventory.tsv, 95 routes), which labels routes with the policy names attestor:write, attestor:verify, attestor:read. Those are internal labels. The claims the policies test are different strings: attest:create and attest:read (src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs:803,429, consumed at src/Evidence/__Libraries/StellaOps.Attestor.Application/AttestorWebServiceComposition.cs:368-385, whose own comment states the split). A token minted as attestor:write satisfies none of the 52 attestor-policy routes.
Corrected mapping:
| Forcing function | Endpoint policy label | Claim the token must carry |
|---|---|---|
| FF-1 attestation create, then verify | attestor:write, then attestor:verify | attest:create, then attest:read or attest:create |
| FF-2 capsule store, then retrieve | evidence:create, then evidence:read | evidence:create, then evidence:read |
| FF-3 proof chain read and render | attestor:read | attest:read or attest:create |
| FF-4 anonymous repeat | any of the above | none, by construction |
No new identity is required on this estate, which was checked rather than assumed. The stellaops-cli client’s allowed_scopes already contains attest:create, attest:read, evidence:create, evidence:hold and evidence:read; the admin user’s role grants all of them plus attest:admin and ops.health; admin is not locked out (failed_login_attempts 0, locked_until null). stella-ops-ui carries attest:read and evidence:read, covering the Console half of FF-3. This is the one place the AAI-9 precedent does not repeat: that window needed a purpose-scoped identity provisioned before its FF-2 could run at all.
What needs care is the request. The 2026-09-03 rehearsal minted its operator token with evidence:read evidence:create evidence:hold ops.health and no attest:*. FF-1 and FF-3 would have returned 403 under that token, and the rehearsal never ran them.
D-C — the anonymous control is vacuous (corrected)
A first version of this finding claimed the probe would return 200 on an attestor route and trigger a needless rollback. That is withdrawn: through the gateway it will not. The endpoints attach their policy by name (.RequireAuthorization("attestor:read") and siblings, 29 sites in AttestorWebServiceEndpoints.cs), so the discovery mapper places the policy name in Policies, HasAuthorization is true (AspNetEndpointDescriptor.cs:300-305), and RequiresAuthentication=true is published (AspNetCoreEndpointDiscoveryProvider.cs:255-270). The gateway rejects an anonymous caller before the service is reached, and the service-side bypass only fires when the Authorization header is absent (AttestorWebServiceComposition.cs:718-730). Section 9 requires every probe to cross the gateway, so section 9 step 4 stays green as written and must not be changed.
What survives is the vacuity, and it is the more important half. For all 52 attestor-policy routes the mapper publishes RequiringClaims = []: AssertionRequirement falls through ExtractPolicyClaims’s default: branch, TryExtractScopeClaims does not match it, and zero claims are returned (DefaultAuthorizationClaimMapper.cs:260-319). Consequences:
- Section 9 step 4’s anonymous 401 measures the gateway’s authentication gate. It is not evidence about scope enforcement on those routes, which exists only in-process.
- A token-bearing but under-scoped caller is still rejected — by the service assertion, not the gateway. A negative control against an attestor route therefore proves the service enforces, and should be recorded in those words.
- The tokenless in-range path is real but narrow: reachable only by a caller that skips the gateway, container-to-container on the estate networks or from the Docker host against a published port. It is the in-estate service-call posture the overlay documents, not a window blocker.
Proposed negative control, three probes with stated expectations, replacing the single one:
| Probe | Source | Token | Expected |
|---|---|---|---|
| N-1 | through the gateway | none | 401 at the gateway (proves the gateway’s authentication gate) |
| N-2 | through the gateway | valid, lacking the route’s claim | 403 from the service (proves in-process scope enforcement) |
| N-3 | direct to the container from an in-range address, no Authorization header, attestor route | none | authorized by design; record as the measured bypass posture, not as a failure |
Only N-2 distinguishes a working authorization posture from a token that merely authenticates, and it is the probe the runbook does not currently contain.
D-D — “not a widening” is true against the predecessors and false against this estate
docker-compose.evidence.yml:181-191 justifies the bypass set as carried verbatim from both predecessors, concluding “Not a widening: no network is added that either predecessor did not already bypass.” That is sound against the predecessor baseline.
It is not sound against the current baseline. On this estate the three predecessor writers are absent — containers, images and both carried volumes — which the rehearsal recorded (D-EVD9-5) and this window’s own rollback/*.ABSENT.txt re-confirms. Nothing serves those routes today. Relative to the live baseline the window therefore moves the tokenless in-network writable attestor surface from 0 routes to 52, including POST /api/evidence/v1/attestations:sign, POST /anchors, DELETE /anchors/{anchorId} and POST /internal/api/v1/attestations/verdict.
This is very likely the intended restoration of capability. The finding is that it belongs in the window closeout record as an explicit line the owner can see, rather than behind a comment asserting the opposite.
Supporting live measurement, 2026-09-05: stellaops is 172.19.0.0/16 and stellaops_frontdoor is 172.18.0.0/16, both exactly matching docker-compose.evidence.yml:196-199.
D-E — an invalid enum value in the shared environment file (for the ROA-1 lane, not this window)
devops/compose/.env:458 sets ROUTER_ON_MISSING_AUTHORIZATION=Fail. Fail is not a member of MissingAuthorizationBehavior, whose only values are RequireExplicit, AllowAuthenticated and WarnAndAllow (src/Router/__Libraries/StellaOps.Microservice.AspNetCore/StellaRouterBridgeOptions.cs:198-217). All 27 running services carry Router__OnMissingAuthorization=Fail and are healthy, so the value is inert or silently defaulting estate-wide rather than crashing. The Evidence overlay consumes the same variable at docker-compose.evidence.yml:218, so the successor inherits whatever it resolves to. UNVERIFIED: which of the three behaviours is in force at runtime; determining that needs a binding test or an instrumented boot, neither of which this review performed.
RESOLVED 2026-09-07 by ROA-1(b), which this review handed it to. The answer is WarnAndAllow, and not because anything parsed Fail: the key was never bound at all. Hosts bind the Router: section into StellaRouterOptionsBase, which has no OnMissingAuthorization property, and TryAddStellaRouter never copies one, so StellaRouterOptions kept its own default. That is why 27 services carried an unparseable value and stayed healthy. The key was deleted from every compose file, both env examples and the release bundle in the same change; the property remains a code-level option on AddStellaRouter. Compose_NeverSetsTheUnboundOnMissingAuthorizationKey fails if it returns.
D-F — the image-receipt gate cannot fail on missing provenance
The required-input table demands “web/worker image digests, provenance receipts, runtime render, authenticated rehearsal”. The only executable check for it, :294-295, is grep -Eq '@sha256:[0-9a-f]{64}$' against each receipt. A file containing nothing but a digest line passes. Both receipts in this window contain exactly that one line and carry no gitSha, worktreeState or dirtyFileCount, so the provenance half of a required input is unrecorded while its gate reads green.
Proposed fix: have each image receipt carry the image’s buildinfo.json triple and assert gitSha equals the intended commit. Per the live-deploy playbook the acceptance check is gitSha == the commit you intended to pin, never worktreeState == clean on its own, because a build mis-invoked against a clean main clone produces an honest clean stamp of the wrong commit.
D-G — section 3’s dump omits a schema its own restore needs (found and fixed by the executing lane)
Recorded for completeness because it is the highest-value defect of the window and belongs in the runbook’s history. Section 3 dumps only attestor, proofchain and evidence_locker. Every evidence_locker RLS policy calls evidence_locker_app.require_current_tenant(), so a standalone restore of that artifact fails at the first policy with schema "evidence_locker_app" does not exist. Data loads before policies, so per-table counts still match, which is how the gap stays hidden. The 2026-09-03 rehearsal did not hit it because it restore-tested the fenced data-only dump into an already-converged target that had the functions.
The executing lane found this independently, re-dumped with the fourth schema included, kept the superseded artifact as backup/source-domain-full.dump.three-schemas-superseded, and is folding the missing --schema into section 3 in this window’s change set.
5. Sections 8-13: steps that cannot be performed as written
The 2026-09-04 route-leg retirement banner amended the forward path (section 7 step 3 and section 8 step 3). It did not amend the rollback section or section 8 step 5, and the absent-predecessor variant note in the rehearsal section does not reach section 10. The result is that retired premises survive in the places least likely to be read before they are needed.
| Ref | Step as written | Why it cannot be performed | Consequence |
|---|---|---|---|
| Rollback step 2 | “Verify the fifteen predecessor routes … before restarting writers” | All fifteen rows are gone from both route tables; the tables hold 22 routes total (banner). There is nothing to verify | A void gate on the rollback path. An operator entering rollback under pressure cannot satisfy it and has no stated alternative |
| Rollback step 4 | “Restart stellaops-attestor, stellaops-evidence-locker-web, stellaops-evidence-locker-worker using captured image IDs” | All three containers, their images and both carried volumes are absent | The rehearsal section says this; the rollback section does not. Rollback here is: stop both successors, restore the gateway and Console blobs, leave both databases intact |
| Rollback step 5 | “Repeat predecessor health, authorization, capsule digest, attestation/timestamp, and Console proof checks” | No predecessor exists to check | Only the final clause, “confirm source counts still equal source-counts-b.tsv”, is performable |
| Section 8 step 5 | “Verify first-match routing for the consolidated group, both Policy false friends, and the Platform /api/v2/evidence route” | “The consolidated group” was the retired fifteen-to-one route group | The false-friend and /api/v2/evidence checks remain valid and should be kept; the group check should be replaced by the banner’s own instruction to confirm the auto-published layer serves /api/evidence/v1/** |
| Section 9 step 3 | “Open a copied proof spine in deployed Console, render its chain, and run proof verification” | proofchain.spines holds 0 rows in the frozen source | FF-3 cannot be discharged by carried data at all. Same shape as the capsule case the runbook already flags, and not flagged for spines |
| Section 10 step 4 | “Keep predecessor writers stopped but present. Retain their images, compose definitions, source schemas, and volumes through rollback retention” | Nothing to keep, retain or stop | Only the source-schema retention clause is live, and it is the one that matters |
The FF-3 finding, stated precisely
The runbook already records that the 8 copied bundle headers have no payloads and that FF-2 is therefore “discharged only by a NEW capsule stored through the successor, never by carried data”. The identical argument applies to FF-3 and is not made: with proofchain.spines at 0 there is no copied spine to open. What the carried data does contain is proofchain.graph_nodes 8 and proofchain.graph_edges 9.
Two honest ways to close FF-3, either acceptable, neither silent:
- Create a spine through the successor during the window and render that in Console, mirroring the FF-2 treatment. This exercises the write path as well as the read path.
- Redefine FF-3 against the graph rows that do exist, and state in the closeout record that the spine-rendering half is not exercised because the source holds no spines.
What must not happen is FF-3 being recorded green on a Console page that rendered nothing — which the runbook’s own wording (“a direct API 200 without Console rendering does not satisfy this gate”) shows it was already alert to in the other direction.
A third vacuous gate in section 7, unflagged
The rehearsal section flags 7.2 (verdict chain) and 7.3 (spine recomputation) as vacuous on this estate, both 0 rows. Section 7.4 is vacuous for the same reason and is not flagged: evidence_locker.evidence_artifacts holds 0 rows, so the dangling-bundle_id count is trivially 0 and the gate cannot fail. The fk_artifacts_bundle constraint that 7.4 then adds is still correct and worth adding — it constrains future inserts — but adding it to an empty table proves nothing about the copy. Record the row count beside the verdict, exactly as the runbook already instructs for 7.2 and 7.3.
6. ROA-1 registration inventory for the Evidence family
Commissioned as “6 Evidence + 1 EvidenceLocker RequireAssertion registrations”. The inventory does not confirm that split.
The family has SIX production RequireAssertion registrations, all in one file. The seventh is not a production registration.
| # | Registration | Policy | Requires authenticated user | Scopes checked | Routes guarded |
|---|---|---|---|---|---|
| 1 | AttestorWebServiceComposition.cs:368-372 | attestor:write | no | attest:create, OR trusted-network bypass | 22 (16 prefixed, 6 unprefixed) |
| 2 | AttestorWebServiceComposition.cs:374-378 | attestor:verify | no | attest:read, attest:create, OR bypass | 2, plus 2 shared with attestor:read |
| 3 | AttestorWebServiceComposition.cs:380-385 | attestor:read | no | attest:read, attest:create, OR bypass | 26 (13 prefixed, 13 unprefixed) |
| 4 | AttestorWebServiceComposition.cs:387-401 | watchlist:read | yes | trust:read, trust:write, trust:admin, watchlist:{read,write,admin}, watchlist.{read,write,admin} | 4 |
| 5 | AttestorWebServiceComposition.cs:403-414 | watchlist:write | yes | trust:write, trust:admin, watchlist:{write,admin}, watchlist.{write,admin} | 3 (paired with watchlist:read) |
| 6 | AttestorWebServiceComposition.cs:416-424 | watchlist:admin | yes | trust:admin, watchlist:admin, watchlist.admin | 0 — registered, guards no route |
The seventh is a test double, not a live fail-open. src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceLockerWebApplicationFactory.cs:267 is .RequireAssertion(_ => true) inside the retiring predecessor’s test project. It has no production counterpart and is not gateway-reachable. Its sibling in the consolidated host’s own suite is src/Evidence/__Tests/StellaOps.Evidence.WebService.Tests/EvidenceHostCompositionTests.cs:469, also a test. A sweep of src/ finds no other RequireAssertion in the Evidence, EvidenceLocker or Attestor families outside test projects, and none in src/__Obsoleted/.
Gateway enforceability — the same for all six. RequiringClaims is empty for every route behind them, because AssertionRequirement is unmappable at DefaultAuthorizationClaimMapper.cs:260-319. RequiresAuthentication is published as true for all of them, since each endpoint attaches its policy by name. The gateway therefore requires a token and can never require a scope; scope enforcement for 59 routes lives solely in-process.
UNVERIFIED: whether the 18 unprefixed carried Attestor routes are gateway-reachable in practice. The consolidated host was not running during this review, so this could not be probed. The prefixed /api/evidence/v1/** surface is served by the auto-published layer per the runbook banner.
The design note ROA-1 needs before it touches this family
Registrations 1-3 are not a like-for-like conversion the way AdvisoryAI’s were. They differ from 4-6 in two ways that both matter:
- They omit
RequireAuthenticatedUser(). - Their assertion is a disjunction whose second branch is a trusted-network bypass (
AttestorWebServiceComposition.cs:718-730, matcher built at:662-687fromAuthority:ResourceServer:BypassNetworks).
Converting them to a typed StellaOpsScopeRequirement would make them gateway-enforceable and would remove the bypass, because a scope requirement has no second branch. That bypass is a deliberate, tested capability, not an oversight: AttestorBypassTenantHeaderTests pins that a tokenless bypass POST /api/evidence/v1/rekor/entries carrying X-StellaOps-TenantId returns 200 and persists the entry under the caller-supplied tenant, that the same call without the header still returns 400 tenant_required, and that an authenticated principal cannot spoof a tenant through that header. The header exception is Attestor-specific and does not generalise.
ROA-1’s remediation for this family is therefore a design decision with a named consumer to re-home first — the keyless in-estate submit path — rather than a mechanical swap. Registrations 4-6 have no bypass and are a clean mechanical conversion. Registration 6 guards no route and can be deleted or wired.
A precedent for the target shape exists: RemediationAuthTests (src/__Obsoleted/Remediation/__Tests/StellaOps.Remediation.WebService.Tests/RemediationAuthTests.cs:98,132) asserts that each policy must enforce a StellaOpsScopeRequirement and not RequireAssertion(_ => true).
7. Estate finding: compose chains that resolve through tmp/
Found while checking whether this window’s digest-pin override, which lives at tmp/evd9-20260905-evd9/preflight/evidence-digest-pin.override.yml, would become a permanent dependency of the live service.
It would, and the estate already carries the problem at scale. Measured 2026-09-05 by reading com.docker.compose.project.config_files from every running container:
- 21 running containers resolve through 72 distinct chain files under
tmp/. - 70 of those 72 still exist. Two are gone, both under
tmp/w301-window-20260824/pins/:registry-web-inherited-name.pin.ymlandtimeline-web-new.pin.yml. - The two affected containers are
stellaops-registry-webandstellaops-timeline-web. Neither can be recreated on its recorded chain today:docker composefails outright on a missing-ffile, and reconstructing a chain without those files silently drops whatever they pinned.
This is pre-existing and estate-wide, not introduced by EVD-9, and the immediately preceding AAI-9 window used the same pattern (stellaops-advisoryai-web resolves through four files under tmp/aai9-20260904-aai9/compose/). It is recorded here because the recommendation follows directly:
Whatever this window pins, put the pin somewhere durable, or copy it into the window’s rollback/ directory and re-point the chain at the copy before the window closes. A pin that is the last entry in a live service’s chain is not a receipt, it is a load-bearing part of the deployment. The two dead pins above are what that looks like a fortnight later, and they deserve their own row on someone’s board.
8. Failure modes
| # | Failure mode | Detection | Response |
|---|---|---|---|
| F-01 | A forcing function passes vacuously | Each FF has a named vacuity trap: FF-1 green proves the data path, not authorization posture (D-C); FF-2 green on carried data proves nothing because the 8 bundle headers have no payloads; FF-3 has no spine to render at all (section 5) | Before accepting any green, state what would have made it red. Where nothing could, label it a belt and record it as one |
| F-02 | The converge boot never reaches healthy and the window hangs | D-A: no deadline on either wait loop | Bound the loop; on expiry dump docker logs and check Authority__ResourceServer__Authority first, per docker-compose.evidence.yml:175-179 |
| F-03 | A forcing function fails on a scope that was never requested | 403 on FF-1 or FF-3 with a token minted from the inventory’s policy labels | D-B: request attest:create and attest:read, not attestor:* |
| F-04 | Rollback is entered and its own steps cannot be executed | Section 5: three of the six rollback clauses have void premises | Use the absent-predecessor rollback: stop both successors, restore the gateway and Console blobs, leave both databases intact |
| F-05 | The window’s image pin is lost and the service cannot be recreated | Section 7: two live containers are already in this state | Copy the pin somewhere durable and assert the copy, not the original |
| F-06 | A parity verdict is read against the wrong denominator | The rehearsal recorded 31/31; this window’s frozen source holds 32 tables | Use 32. Record the three zero-row tables (verdict_ledger, spines, evidence_artifacts) beside the verdict so the vacuous gates stay visible |
| F-07 | Scheduled host maintenance interrupts the window | Two Windows tasks run every two days at 01:00 and 02:00 UTC; the estate does not restart itself (measured gaps 2 h 58 m and 7 h 04 m) | Check Get-ScheduledTask StellaOps-WslVhdxOptimize | Get-ScheduledTaskInfo and clear NextRunTime by the full window duration |
| F-08 | A recreate lands on a pruned image record | docker image inspect on a running container’s image id says “No such image” while the container runs | Sweep before section 8 by comparing docker inspect --format '{{.Image}}' across all containers against docker image ls -a --no-trunc --format '{{.ID}}' |
9. What this review did not determine
- Which
MissingAuthorizationBehaviorthe invalidFailvalue resolves to at runtime (D-E). - Whether the 18 unprefixed carried Attestor routes are gateway-reachable (section 6). The consolidated host was not running.
- Any live behaviour of the successor. No probe was run against it, because it did not exist.
- The contents of
console-evd6.txt, which had not been produced when this review was written.
