ADR-0041: CASE_MANAGER-Authoritative Case Initialization
Identity vs. role (ADR-0088). This ADR predates ADR-0088 and its prose says "CaseActor" where it means the participant holding the
CASE_MANAGERrole. Authority to create and write the canonical case is conferred by that role, never by an actor namedcase-actoror by any URL shape. Read every "CaseActor" below as "the CASE_MANAGER"; see ADR-0088 for the identity/role distinction and the resolver that enforces it.
Context and Problem Statement
ADR-0015 established that the receiver (vendor) creates the VulnerabilityCase
object in its own DataLayer at report receipt (RM.RECEIVED), then
retroactively introduces the CaseActor via the CaseProposal protocol
(ADR-0023). This produces an architecture gap: the vendor is not the
authoritative creator of the canonical case, yet it creates the case object
before the CaseActor exists.
The concrete problems this gap causes:
-
AS2 authorship violation: the vendor creates a
VulnerabilityCasewith itself asattributed_to, but per ADR-0023 the CaseActor is the authoritative case creator and the only correctactoronCreate(VulnerabilityCase). -
Prologue back-fill:
WritePrologueLedgerEntriesNode(Issue #1688) was introduced as a workaround to stamp vendor-authored initialization entries into the CaseActor's canonical ledger when the CaseActor accepts theOffer(CaseManagerRole). Its own docstring acknowledges "the vendor actor is not the CaseActor and MUST NOT commit canonical case-ledger entries." -
Two overlapping init mechanisms: the
CaseProposalaccept path (ADR-0023) and the prologue back-fill path (#1688) both attempt to establish the initial case at the CaseActor. Their overlap is the direct cause of Issue #1767:add_case_status_to_caseis rejected by the_validate_canonical_entryguard because the CaseActor is stamping a vendor-authored snapshot.
The guard has two distinct failure modes, and they need separate fixes.
It only rejects when the snapshot's actor equals the CaseActor's own
ID. In a multi-actor deployment the vendor URI is the snapshot actor, so the
guard never fires and removing the back-fill is sufficient. In a single-actor
deployment (the fvv scenario, where the vendor is the CaseActor) the
snapshot actor equals case_actor_id, the guard does fire, and the
("Add", "CaseStatus") signature must additionally be authorized.
- Latent scenario-dependent breakage: the prologue
add_case_status_to_caseentry is silently skipped in passing runs (the genesisCaseStatusis already inlined in thecreate_caseentry), so the ledger "works" while quietly dropping an entry. Attempting to commit it (PR #1746, reverted) shifted ledger indices and brokefvcv-extensionVFD replication timing — evidence that the back-fill is load-bearing scar tissue, not a clean layer.
The question is: when and by whom should the VulnerabilityCase be
created, and what does the receiver hold locally during the proposal window?
Decision Drivers
- Preserve AS2 "I created this" semantics end-to-end (ADR-0023)
- Single authoritative init path: no back-fill workarounds
- Receiver needs no local
VulnerabilityCasebefore the CaseActor responds - Remove
WritePrologueLedgerEntriesNode, and removeOffer(CaseManagerRole)from the initialization path, as both become unnecessary there
Considered Options
-
CaseActor-authoritative initialization (chosen): receiver stores the report and sends
Create(CaseProposal), then waits. CaseActor creates the case, adds participants, initializes embargo, and commits canonical ledger entries natively when accepting the proposal. Receiver creates noVulnerabilityCaseuntilCreate(VulnerabilityCase)arrives from the CaseActor. -
Keep vendor-created case, fix back-fill signing: allow the vendor to create the case but have it authored correctly as a vendor-local copy, with the CaseActor superseding it on
Create(VulnerabilityCase)arrival. Requires complex merge/supersession logic and still violates AS2actorsemantics on the vendor-side copy. -
Authorize
("Add", "CaseStatus")and stop there: add the signature to_CASE_AUTHORED_SIGNATURESas the whole response to Issue #1767, leaving the two-init-path architecture intact. Rejected: it silences the guard while the vendor continues to author and back-fill initialization entries it has no authority over. Attempted in commit256ef3e1and reverted inf6578c22.
Note that authorizing the signature is not rejected in itself — Option 1 requires it, because the CaseActor authors that entry natively and must be permitted to. What is rejected is treating it as a substitute for removing the back-fill. See "Signature authorization" under Decision Outcome.
Decision Outcome
Chosen option: CaseActor-authoritative initialization (Option 1).
New protocol flow
Reporter → Offer(VulnerabilityReport) → Receiver inbox
Receiver: store report + write VultronReportCaseLink(status=PENDING_PROPOSAL)
Receiver → Create(as_CaseProposal) → CaseActor inbox
CaseActor (on Accept(CaseProposal)):
- Create VulnerabilityCase (attributed_to=CaseActor)
- Add receiver as CASE_OWNER participant (RM.RECEIVED)
- Add reporter as participant (RM.ACCEPTED)
- Initialize default embargo
- Commit canonical ledger entries natively (no back-fill)
- Emit Accept(as_CaseProposal) → Receiver inbox
- Emit Create(VulnerabilityCase, actor=CaseActor) → Receiver inbox
Receiver (on Create(VulnerabilityCase)):
- Seed local replica via CreateCaseReceivedUseCase (already exists)
- Record trust anchors in VultronReportCaseLink
- CaseActor is now the comms hub for the case
What is removed
CreateCaseNode,CreateCaseOwnerParticipant,InitializeDefaultEmbargoNode,CreateCaseActivity/UpdateActorOutboxfrom the vendor'sreceive_report_case_tree.py. The vendor tree becomes: store report → write pending link →ProposeReportCaseToActorNode→ done.WritePrologueLedgerEntriesNode(Issue #1688) — the back-fill is no longer needed when the CaseActor commits init entries natively.SendOfferCaseManagerRoleNodefromreceive_report_case_tree.py, and the prologue node fromoffer_case_manager_role_received_tree.py— the CaseActor adds itself asCASE_MANAGERwhen creating the case natively, so theOffer(CaseManagerRole)handshake is no longer part of case initialization.
This ADR originally scoped the removal to initialization only and stated
that Offer(CaseManagerRole) survived as a standalone delegation operation.
That is no longer true (ADR-0039, CONCERN-2322, #2429): the
CaseManagerRole patterns and use cases were deleted entirely and replaced by
OFFER_CASE_PARTICIPANT_ROLE; DEMOMA-08-006 through DEMOMA-08-009 are marked
superseded. Only the removal from initialization originated with this ADR;
the operation's later deletion belongs to ADR-0039.
- CreateCaseActorNode from the vendor's receive_report_case_tree.py — the
CaseActor is a pre-existing service; the vendor does not spawn it at report
receipt. The node itself was retained for create_tree.py's standalone
case construction, but that factory had no production caller and was
deleted (#4330); the node and all other orphaned case-construction nodes
were deleted in issue #4353.
Signature authorization
Because the CaseActor now authors the four initialization entries itself,
_CASE_AUTHORED_SIGNATURES MUST authorize every signature it emits during
initialization: ("Create", "VulnerabilityCase"), ("Add", "VulnerabilityReport"),
("Add", "ParticipantStatus"), and ("Add", "CaseStatus"). The last of these
was previously absent, which is the mechanical half of Issue #1767. Normative in
CLP-12-001 (the specific entry) and CLP-12-002 (the general completeness rule).
This is a consequence of Option 1, not an alternative to it — see Option 3.
What stays
ProposeReportCaseToActorNode— still needed; the vendor sendsCreate(as_CaseProposal)to initiate the CaseActor-authoritative flow. (The original node named in this ADR,ProposeCaseToActorNode, was superseded by the slimmed-tree rename and deleted in issue #4353.)CreateCaseReceivedUseCase— already handlesCreate(VulnerabilityCase)arriving at the vendor inbox; now actually seeds the case rather than hitting the idempotency skip.VultronReportCaseLink— vendor's interim record during the proposal window (status=PENDING_PROPOSAL). NoVulnerabilityCaseexists vendor-side until the CaseActor'sCreate(VulnerabilityCase)arrives.- All of
case_proposal_received_tree.pystructure — enriched with native participant, embargo, and ledger initialization.
Consequences
- Good: AS2
actorsemantics are correct end-to-end — only the CaseActor bearsactoronCreate(VulnerabilityCase). - Good: Single initialization path;
WritePrologueLedgerEntriesNodeis removed entirely andOffer(CaseManagerRole)no longer participates in initialization. - Good: Issue #1767 is resolved by the two halves together — removing the
back-fill eliminates the vendor-authored snapshot in multi-actor deployments,
and authorizing
("Add", "CaseStatus")covers the single-actor deployment where the guard still fires. Neither half is sufficient alone. - Good:
receive_report_case_tree.pybecomes significantly simpler. - Neutral:
case_proposal_received_tree.pygains participant creation, embargo initialization, and ledger commit responsibility. This is the correct place for it. - Bad: The embedded participant payload in
Create(VulnerabilityCase)must be rich enough for_store_embedded_participantsandEnsureReporterParticipantAtAcceptedNodeto seed the vendor's replica correctly. This is an implementation constraint on the CaseActor-side tree.
Implementation order
Issues must be implemented in this order to keep each step independently testable:
-
Enrich CaseActor tree (Issue B, blocked by #1771 docs PR): add native participant creation, embargo init, and ledger commits to
case_proposal_received_tree.py. Additive; vendor tree is untouched. Demo still passes. -
Slim vendor tree (Issue A, blocked by Issue B): remove case-creation nodes from
receive_report_case_tree.py. Depends on Issue B being merged so the CaseActor'sCreate(VulnerabilityCase)carries full participant data. -
Remove prologue and decouple
Offer(CaseManagerRole)from init (Issue C, blocked by Issue A): deleteWritePrologueLedgerEntriesNode, remove the prologue node fromoffer_case_manager_role_received_tree.pywhile keeping its accept/reject path functional, removeSendOfferCaseManagerRoleNodeandCreateCaseActorNodefrom the vendor's report-receipt tree, and authorize("Add", "CaseStatus"). Closes Issue #1767. -
Spec and notes updates (Issue D, parallel with B/C): update CP, CM, CLP specs and
notes/case-proposal.md,notes/case-bootstrap-trust.md.
More Information
- Supersedes:
docs/adr/0015-create-case-at-report-receipt.md - Refines:
docs/adr/0023-case-proposal-protocol.md - Refined by:
docs/adr/0088-consolidate-case-authority-determination.md(narrows the identity narrative: authority is theCASE_MANAGERrole, not the CaseActor identity or URL) - Source concern: Issue #1771
- Symptom issue resolved: Issue #1767
- Workaround removed: Issue #1688 (
WritePrologueLedgerEntriesNode) - Generated spec requirements:
specs/case-proposal.yamlCP-09,specs/case-management.yamlCM-22 (CASE_MANAGER-authoritative init),specs/case-ledger-processing.yamlCLP-12 - Later deleted by ADR-0039 (CONCERN-2322):
Offer(CaseManagerRole)as a standalone delegation operation, formerly DEMOMA-08-006 through DEMOMA-08-009 (replaced byOFFER_CASE_PARTICIPANT_ROLE)
Revision history
Revised 2026-07-31 during Issue #1777 (PR #1851), the implementation of step 3. The decision is unchanged; three statements that misdescribed it were corrected in place rather than appended to, so the document reads as the decision now stands:
- Option 3 previously read as rejecting the
("Add", "CaseStatus")authorization outright, which the chosen option in fact requires. It now rejects only substituting that authorization for removal of the back-fill. - "What is removed" listed the
Offer(CaseManagerRole)accept path andCreateCaseActorNodewithout qualification, conflicting with MUST-level DEMOMA-08 specs and with the then-livecreate_tree.pyuse of the node (that factory had no production caller and was deleted, #4330; the node was deleted in #4353). Both entries are now scoped to the report-receipt path. - The Issue #1767 consequence claimed removal of the back-fill was sufficient on its own. It is sufficient only for multi-actor deployments.
Amendment — 2026-10-02
Problem 1 and the creation flow above place authorship of the case on the CaseActor.
The decision stands (the CASE_MANAGER is the actor of Create(VulnerabilityCase)), but one detail is replaced: who attributed_to names.
Replaced text, in Context problem 1: "the vendor creates a VulnerabilityCase with itself as attributed_to, but per ADR-0023 the CaseActor is the authoritative case creator and the only correct actor on Create(VulnerabilityCase)."
And in the creation flow: "Create VulnerabilityCase (attributed_to=CaseActor)".
Now: attributed_to on a VulnerabilityCase is the CASE_OWNER, the actor that received the report, on every creation path (CM-02-008, CM-22-001, CP-09-001).
The CASE_MANAGER records that it created the case as the actor of Create(VulnerabilityCase).
The original text had conflated ADR-0023's rule about the Create actor with the case's attributedTo.
An earlier change (commit 0882ed1c7, #3979) made this correction in place without a trail; the 2026-10-02 audit of unsupervised agent decisions (#4195) confirmed the reversal, and the original text above is restored so this section is the record of it.
Confirmed by Allen D. Householder, 2026-10-02.