Protocol Specifications
This page is normative
This page is considered a core part of the Vultron Protocol. This is a normative section of the documentation.
The Vultron layer: what every implementation, in any language, must obey to take part in Vultron. It covers Vultron's message forms on the wire, including where ActivityStreams allows several forms and Vultron requires one. It also covers which messages are sent and when, what a receiver does given its state, including on duplicate delivery, and the state machines underlying those behaviors. Rules Vultron inherits unchanged from ActivityStreams 2.0, ActivityPub, or HTTP are on the Foundation page.
Behavioral Specifications
The following specifications define Event-Condition-Action (ECA) behavioral requirements with typed preconditions, ordered steps, and postconditions.
Activity Factory Functions
Requirements governing the factory function API for constructing outbound Vultron protocol activities. Factory functions are the sole public construction API for activity objects outside the wire vocabulary layer, replacing direct subclass instantiation to enforce consistent field population and layer separation.
File ID: AF
Purpose and Scope
| ID | Priority | Requirement | Related |
|---|---|---|---|
AF-01-004 |
SHOULD | Factory functions SHOULD accept **kwargs for optional AS2 fields that are not protocol-significant, forwarding them to the underlying AS2 constructor |
Internal Vocabulary Classes
Actor Knowledge Model
Defines the Actor Knowledge Model: a Vultron Actor's knowledge of the world is strictly bounded by the ActivityStreams 2.0 activities it has received. Actors must not assume recipients hold objects that were not explicitly included in prior activities; DataLayer access is private and non-shareable, and all inter-Actor communication occurs exclusively through the AS2 wire protocol.
File ID: AKM
DataLayer Isolation
Knowledge Boundaries
Outbound Activity Object Integrity
Future: Known-Object Tracking
Actor Addressing and Discovery
| ID | Priority | Requirement | Related |
|---|---|---|---|
AKM-05-003 |
SHOULD | An implementation SHOULD support object dereference: fetching the full object named by a URI through an API call to the actor or service that holds it. Several rules let a sender name an object by reference instead of inlining it, and each is sound only where the receiver can resolve the reference. The CP-01-009 by-reference actor form is one. A prototype may inline everything and resolve nothing, so the capability is a SHOULD there (AKM-05-004 makes it a MUST in production). |
|
AKM-05-004 |
MUST | A production implementation MUST support object dereference (AKM-05-003): given a URI it has not seen, it MUST be able to fetch the object through an API call. | Refines: AKM-05-003 |
Agentic Readiness
Requirements for API and CLI design that enable reliable interaction by automated agents and AI systems. Covers machine-parseable interfaces, structured response formats, and observable behavior that supports agentic workflows.
File ID: AR
CVD Action Rules API
Behavior Tree Integration
Requirements for integrating py_trees behavior trees into Vultron handler functions. Covers the BT execution model, state management via the DataLayer, handler integration patterns, actor isolation, concurrency constraints, and failure diagnosis. Complex protocol workflows such as report validation, case creation, and embargo management must be implemented as BT nodes or subtrees.
File ID: BT
State Management
| ID | Priority | Requirement | Related |
|---|---|---|---|
BT-03-001 |
MUST | BTs MUST use DataLayer as persistent state store | |
BT-03-004 |
MUST | State changes MUST be committed to DataLayer on successful execution |
DataLayer Integration
| ID | Priority | Requirement | Related |
|---|---|---|---|
BT-07-003 |
MUST | State transitions MUST be logged at the level specified by SL-04-001 | Depends on: SL-03-001 Depends on: SL-04-001 |
Actor Isolation
| ID | Priority | Requirement | Related |
|---|---|---|---|
BT-09-001 |
MUST | Each actor MUST have isolated BT execution context |
Case Manager Role Management
| ID | Priority | Requirement | Related |
|---|---|---|---|
BT-10-001 |
MUST | Report validation MUST trigger VulnerabilityCase creation | Implements: VP-02-015 Implements: VP-02-020 |
BT-10-002 |
MUST | Case creation MUST establish the case's CASE_MANAGER by recording a participant that holds CVDRole.CASE_MANAGER. It MUST NOT require a per-case Service object, and MUST NOT mint a per-case actor identity: the actor enacting the role (the "CaseActor") has its container's identity, and the case a message concerns is carried by activity.context.The prior text — "Case creation MUST create corresponding CaseActor (Service)" — treated the CaseActor as a distinct per-case object, and implementations followed it by deriving {case_actor_service_url}/actors/case-actor-{slug} from the report or case id. That identity was unhostable by construction: the sender* computed it, so no container could have registered it, and POST /actors/case-actor-<slug>/inbox/ answered a permanent 404 (ISSUE-1872). Provisioning could not rescue it either, because the slug is not computable until a report exists, by which point the proposal has already been delivered. |
|
An actor participates in many cases; the CASE_MANAGER is whichever participant the case owner delegated CVDRole.CASE_MANAGER to. That is the role, not an entity, which is why the role is the marker and a Service record is redundant with it. This also subsumes the eventual one-process-per-case direction: such a process gets its own first-class actor identity rather than a slug suffix on somebody else's.* |
Refines: CP-04-002 | ||
BT-10-003 |
MUST | The actor holding CVDRole.CASE_MANAGER for a case MUST process that case's messages. The authority is that role; "CaseActor" names only the concrete prototype/demo actor that enacts it, not the role itself.Reworded with BT-10-002 to remove the entity-vs-role assumption: the prior "CaseActor MUST manage case-related message processing" reads as an obligation on a distinct object, which is what led implementations to mint one per case (ISSUE-1872). |
Refines: BT-10-002 |
BT-10-004 |
MUST | The actor holding CVDRole.CASE_MANAGER for a case MUST enforce that case's authorization decisions. Gates MUST test for the role rather than for a CaseActor object or a per-case actor identity.Same entity-vs-role correction as BT-10-002 and BT-10-003. Testing for the role is also what makes the gate work for a non-software case manager, which is the general case; CheckIsCaseManagerNode already does this and needs no notion of a CaseActor entity. |
Refines: CLP-09-001 Refines: CLP-09-003 Refines: CLP-09-004 |
BT-10-005 |
SHOULD | The system SHOULD record an invitee's RM state as RM.RECEIVED via BT execution when the CASE_MANAGER emits the stub Invite (CM-11-006), and SHOULD leave it at RM.RECEIVED when the invitee accepts the stub (CM-11-001). The invitee's triage is driven by its replies to the full-case Invite (RM.VALID, RM.INVALID or RM.CLOSED, CM-11-011) and then by Join(VulnerabilityCase) or Ignore(VulnerabilityCase) (RM.ACCEPTED or RM.DEFERRED). |
Implements: CM-11-006 Implements: CM-11-001 |
Concurrency
Peer Broadcast Failure Semantics
Routing-Gated State Mutation in Lifecycle BT Sequences
| ID | Priority | Requirement | Related |
|---|---|---|---|
BT-19-003 |
MUST | In Vultron protocol lifecycle BTs, routing prerequisites for EM/RM/CS state-transition sequences MUST include Case Manager actor ID resolution and outbox availability. A missing Case Manager entry in actor_participant_index MUST cause the sequence to fail at the guard with zero DataLayer state change.Protocol state divergence (e.g., local EM=EXITED while peers hold EM=ACTIVE) is the primary failure mode when routing prerequisites are absent. Residual mid-send failures after state mutation are recoverable via ledger-sync catch-up (SYNC-10); prerequisite-missing failures are not, because the broadcast was never attempted. |
Refines: BT-19-001 — Specifies the CVD-protocol-specific routing prerequisites that BT-19-001 requires to be resolved in the guard node. Refines: BT-06-001 — Protocol state transitions are protocol-significant behavior; their ordering relative to routing checks is part of the tree's auditable contract. |
Behavior Tree Node Design
Formal requirements for BT node parameterization, composability, and reuse in the core behaviors package. Covers node context injection, blackboard hygiene, module ownership, and the fractal composability pattern that applies at every depth of the tree hierarchy.
File ID: BTND
Actor Configuration for Participant Nodes
BT Status-Write Node Transition Validation
| ID | Priority | Requirement | Related |
|---|---|---|---|
BTND-10-001 |
MUST | A BT node that persists a ParticipantStatus snapshot containing a non-None VFD, RM, or PXA state MUST validate that (current_state → target_state) is a permitted transition before constructing or saving the new dimension object. A same-state write (target == current) is permitted as a status confirmation. An illegal jump (e.g. vfd → VFD, skipping Vfd) or a backward move MUST cause the node to return Status.FAILURE with a descriptive feedback_message. Exception: on the receive side, a dimension the CASE_MANAGER has already accepted under RSH-09 / RSH-06 (including a non-adjacent forward RM move, RSH-06-006) MAY reach the write node with adjacency validation relaxed for that dimension; a backward move is still refused at the write boundary. The dimension objects' transition() machinery (SDO-02-002) and the is_valid__transition() helpers in vultron/core/states/ enforce forward-only state machine rules, but constructing VfDimension(state=target) or DDimension(state=target) directly bypasses them. A BT guard upstream is necessary but not sufficient: if the guard is too weak (as in issue #1825) or absent (as in the trigger path), an illegal state jump reaches the write boundary unchecked. Validating at the write node makes illegal transitions fail-closed regardless of guard coverage. Same-state writes are permitted because a ParticipantStatus snapshot records current state; confirming "still at VFd" is a legitimate protocol observation, not a transition error. ECA Details- A non-None target VFD, RM, or PXA state is passed to the write node Steps 1. [write_node] Read the participant's current state for the dimension being written (e.g. resolve_participant_state_from_dl) → Current state available 2. [write_node] If target == current, proceed (status confirmation). If is_valid__transition(current, target) is True, proceed. Otherwise return Status.FAILURE with a feedback_message identifying the illegal source→target pair. → Only valid forward transitions or same-state writes reach the DataLayer persist step; invalid jumps return FAILURE. Postconditions - No ParticipantStatus record with an illegal state jump is persisted |
Implements: SDO-02-002 — Write-node compliance with the transition() validation contract Implements: BTND-09-001 — Validation failure returns FAILURE from update(), consistent with error handling contract |
Build Workflow Learnings Policy
Defines the content policy for the per-entry incoming learnings queue used by the build and bugfix skills to pass implementation observations upstream to the learn skill. Covers what belongs in the queue, what must not go there, file naming and format, how the learn skill processes and archives entries, and the learning history entry type. The queue is narrow by design: findings with a definite owner are routed to a GitHub issue or an in-session fix at the moment of discovery, and only unverified general claims and spec divergences are queued for batch processing (BW-07).
File ID: BW
Upward-Reflection Finding Routing
| ID | Priority | Requirement | Related |
|---|---|---|---|
BW-07-002 |
SHOULD | Incoming learning files SHOULD include a signal: frontmatter field set to one of theme-candidate, spec-gap, spec-ambiguity, or spec-contradiction. The values design-question, concern, tooling-issue, and process-issue are retired: findings of those kinds are routed to a GitHub issue or an in-session fix per BW-07-004 and do not produce a learning file.The retired values all named findings with a definite owner and a definite action, which made the queue a waiting room for work that could already have started. The surviving values name the two things that genuinely require a batch — an unverified general claim, and a divergence between spec and reality. |
Derives from: BW-07-004 — The taxonomy is a consequence of the routing table rather than an independent list. |
Case Bootstrap Trust
Requirements for establishing trust in a VulnerabilityCase and its CASE_MANAGER at non-owner sites. Governs the bootstrap process by which a non-owner participant verifies that an incoming case and its managing actor are legitimate before accepting case state updates.
File ID: CBT
Original Report-Submission Bootstrap
| ID | Priority | Requirement | Related |
|---|---|---|---|
CBT-01-001 |
MUST | When a case is created in response to an inbound Offer(VulnerabilityReport), the actor that received the report (the report receiver) MUST establish trust at the original report submitter by sending it, as itself, a one-time Create(VulnerabilityCase) activity. When the CASE_MANAGER is a different actor from the report receiver, that activity introduces the CASE_MANAGER the receiver delegated case maintenance to (ADR-0132).The submitter chose the report receiver and wrote a record of that choice before the case existed. A case that arrives first from an actor the submitter never addressed gives it nothing to check the sender against, and a roster the sender wrote is not an anchor. The receiver already knows the CASE_MANAGER, so it vouches for it. |
Refines: CM-12-001 Refines: PCR-02-005 |
CBT-01-002 |
MUST | The bootstrap Create(VulnerabilityCase) activity MUST carry the complete VulnerabilityCase, inline or by reference, rather than a minimal stub or partial trust-only payload. |
Implements: AKM-03-001 |
CBT-01-003 |
MUST | The bootstrap case snapshot MUST distinguish the actor that asked for the case from the actor that created it. The actor whose CaseProposal caused the case to exist — the report receiver, whatever other roles it also holds — MUST be a participant with the CASE_OWNER role. The actor that created the case MUST be a participant with the CASE_MANAGER role, and SHOULD also carry the COORDINATOR role. The CASE_OWNER role MUST NOT be delegated to the CASE_MANAGER.Under ADR-0041 the CASE_MANAGER creates the case, so "creator/owner" names two different actors and the earlier wording invited code to collapse them. They confer different authority: CASE_OWNER is the standing of the party whose disclosure decision the case exists to serve, and it is never delegated; CASE_MANAGER is the privilege of writing the canonical case ledger, which the creating actor holds precisely because it created the case in its own store (ADR-0073). Keeping them distinct is what makes "the CASE_MANAGER owns write privileges" true without making "the CASE_MANAGER owns the case" true. |
Refines: CM-02-001 Refines: CM-02-010 Refines: PCR-01-002 |
CBT-01-004 |
MUST | When the original report submitter is intended to receive ongoing case updates immediately, the bootstrap case snapshot MUST include that actor as a participant from the start. | |
CBT-01-005 |
MUST | A report submitter MUST NOT trust a newly introduced CASE_MANAGER unless the bootstrap Create(VulnerabilityCase) both comes from the same actor that received the report and references the previously submitted report. The submitter MUST check this against a record it wrote itself before the case arrived; the roster in the snapshot, which the sender wrote, MUST NOT serve as that record. |
Refines: PCR-05-001 Refines: AKM-02-001 |
CBT-01-006 |
MUST | After validating the bootstrap Create(VulnerabilityCase), the receiving actor MUST persist the report-to-case link, the report receiver's identity (the actor that introduced the case), and the trusted CASE_MANAGER identity for that relationship. |
Refines: PCR-05-001 |
CBT-01-007 |
MUST | The bootstrap Create(VulnerabilityCase) payload MUST carry every nested domain object of the case (e.g., each CaseParticipant the case lists), each inline or by reference. |
Refines: CBT-01-002 |
CBT-01-008 |
MUST | The bootstrap case snapshot MUST include the original report submitter's participant record with an RM state of RM.ACCEPTED. The CASE_MANAGER MUST set this state directly rather than inserting intermediate transitions (RM.START, RM.RECEIVED, RM.VALID) that the CASE_MANAGER never observed for the reporter.A report submitter can only be in RM.ACCEPTED at the time of submission: reporting is the act of accepting that the vulnerability warrants coordinated disclosure. A submitter who considered the item invalid would not have filed a report, and one who had deferred or closed their involvement would not be reporting at all. Because the CASE_MANAGER cannot observe the reporter's internal state progression, inserting intermediate RM records on the reporter's behalf would place unobserved events into the canonical case ledger. The CASE_MANAGER MUST instead reflect the only state the reporter could actually be in: RM.ACCEPTED. |
Refines: CBT-01-004 Refines: SM-04-001 |
CBT-01-009 |
MUST | The CASE_MANAGER MUST generate its Create(VulnerabilityCase) activity only after the VulnerabilityCase domain object has been fully initialized — all participants set to their correct initial RM states, all roles assigned. The activity MUST be a faithful record of a completed initialization, not an intent message constructed before initialization is finished. The report receiver's introduction to the submitter (CBT-01-001) MUST relay that completed snapshot.ActivityStreams 2.0 activities represent completed events, not intentions. A Create(VulnerabilityCase) sent before the case is fully initialized would misrepresent the state that existed at creation time. Receivers replay the bootstrap snapshot as authoritative ledger content; if that snapshot is incomplete, every replica is permanently wrong. Sequencing the activity after initialization is the structural guarantee that the bootstrap snapshot is always accurate. |
Refines: CBT-01-002 Refines: CBT-01-007 |
CBT-01-010 |
MUST | When the actor enacting CASE_MANAGER is not the report receiver, the CASE_MANAGER MUST NOT address its Create(VulnerabilityCase) to the original report submitter. That activity goes to the report receiver (the Case Owner), which relays the introduction (CBT-01-001). The CASE_MANAGER's first message to the submitter is an Announce. A submitter that receives it before the introduction holds it under CBT-03-001 and CBT-03-002.The submitter never addressed the CASE_MANAGER, so a Create from it cannot be checked against anything the submitter wrote (CBT-01-005). Introducing the CASE_MANAGER through the actor the submitter chose keeps one trust anchor for the submitter and removes the route that trusted a sender because its own snapshot named it (ADR-0132). |
Refines: CBT-01-001 Refines: CBT-01-005 |
CBT-01-011 |
MUST | A participant MUST refuse a bootstrap Create(VulnerabilityCase) from an actor that no trust anchor the participant wrote names as the expected sender, and MUST store nothing from it. Naming the sender as CASE_MANAGER in the carried snapshot MUST NOT make the sender trusted.The sender wrote the snapshot, so a roster in it proves nothing about the sender's authority (ADR-0132; notes/participant-case-replica.md). |
Refines: CBT-01-005 |
Post-Bootstrap Update Authority
| ID | Priority | Requirement | Related |
|---|---|---|---|
CBT-02-001 |
MUST | For the original report-submission path, Create(VulnerabilityCase) is a one-time bootstrap message only; subsequent full-case synchronization MUST use Announce(VulnerabilityCase) from the trusted CASE_MANAGER. |
Refines: PCR-02-001 Refines: PCR-03-001 |
CBT-02-002 |
MUST NOT | Transport authentication or encrypted direct messaging alone MUST NOT be treated as establishing authority to trust a previously unknown CASE_MANAGER; the explicit bootstrap trust handoff is required. | Refines: AKM-01-002 |
CBT-02-003 |
MUST | Once a CASE_MANAGER has been trusted for a case, that identity MUST NOT be replaced implicitly by ordinary case-update traffic; CASE_MANAGER rotation or transfer MUST use an explicit transfer workflow. | Refines: CM-02-010 |
Out-of-Order Handling and Replay Requests
| ID | Priority | Requirement | Related |
|---|---|---|---|
CBT-03-001 |
SHOULD | If a CASE_MANAGER-originated case message arrives before the required bootstrap trust handoff, the receiving actor SHOULD queue it only for a short bounded interval rather than accepting or permanently trusting it immediately. | Refines: PCR-06-002 |
CBT-03-002 |
MUST NOT | A pre-bootstrap CASE_MANAGER message MUST NOT update local case state before the corresponding trust-establishing bootstrap message has been validated. | Refines: PCR-06-003 |
CBT-03-003 |
MUST | When the bounded pre-bootstrap queue interval (CBT-03-001) expires, the receiver MUST drop the queued message and require a resend after bootstrap. |
Late-Joiner Bootstrap
| ID | Priority | Requirement | Related |
|---|---|---|---|
CBT-04-001 |
MUST | For a participant added after case creation, the trust-establishing message MUST be InviteActorToCase from the case creator/owner rather than Create(VulnerabilityCase). |
Refines: CM-11-001 |
CBT-04-002 |
MUST | The trust-establishing invite for a late joiner MUST identify both the case and the CASE_MANAGER that will send subsequent Announce(VulnerabilityCase) updates. |
|
CBT-04-003 |
MUST | After the late joiner has accepted the stub Invite (Accept(Invite(Actor, VulnerabilityCaseStub))), the CASE_MANAGER MAY send Announce(VulnerabilityCase) without a separate Create(VulnerabilityCase) bootstrap. |
Refines: PCR-07-007 |
Verification
| ID | Priority | Requirement | Related |
|---|---|---|---|
CBT-05-008 |
MUST | When a receiver processes a bootstrap Create(VulnerabilityCase) and finds that required participant data is absent (in violation of CBT-01-007), the receiver MUST reject the bootstrap as a protocol error. The receiver MUST NOT silently skip the missing participant, infer participant state from domain knowledge, or synthesize a participant record from information not present in the bootstrap payload.Silent reconstruction of missing protocol data masks protocol bugs. This system exists to demonstrate that the CVD protocol works correctly end-to-end; when the protocol fails to deliver required data, that failure MUST be visible so it can be diagnosed and fixed. A fallback that fills in missing data "from domain knowledge" creates a false impression that the protocol is functioning when it is not. Replicas MUST be built exclusively from ledger content; if data is not in the ledger, it did not happen. |
Implements: CBT-01-007 Implements: CBT-01-008 |
One Report Offered to Several Recipients
| ID | Priority | Requirement | Related |
|---|---|---|---|
CBT-06-001 |
MUST | Each Offer(VulnerabilityReport) a reporter sends MUST address exactly one recipient and MUST carry an activity id distinct from every other Offer of the same report, so that each submission attempt is identified by at least the pair (report, recipient).A reporter that gets no answer from its first recipient routinely offers the same report to a Coordinator, and either recipient may then accept and create its own case. The protocol permits this on purpose. The two attempts, and the cases that follow from them, are only distinguishable if each Offer names one recipient and has its own id (issue #3366). |
Refines: HP-09-001 |
CBT-06-002 |
MUST | A reporter that has offered one report to several recipients MUST accept a valid bootstrap Create(VulnerabilityCase) from each of them, checking each bootstrap (CBT-01-005) against the recipient that sent it and recording the resulting case and CASE_MANAGER for that recipient alone. Offering the report to a further recipient, or accepting one recipient's bootstrap, MUST NOT change whether another recipient's bootstrap is accepted or which case is recorded for it.In the common sequential escalation, a reporter re-offers an unanswered report to a Coordinator and either recipient may create a case. A reporter that trusts one case creator per report refuses the second recipient's bootstrap and cannot join that case (issue #3366). How the reporter stores per-recipient trust is an implementation choice; this constrains only which bootstraps it accepts. |
Refines: CBT-01-006 Depends on: CBT-01-005 |
Case Ledger Processing
Requirements for participant assertions, CASE_MANAGER canonicalization, local case audit ledgering, recorded-history projection, and replication-facing CaseLedgerEntry objects. Covers the commit authorization model, per-case genesis hash derivation, the canonical entry criteria that separate the case ledger from the process log, the CASE_MANAGER inbox routing path that enables ledger entry authorship, and the causal-ordering model that makes log_index order the authoritative causal sequence.
File ID: CLP
Assertion Intake
Canonical CaseLedgerEntry
| ID | Priority | Requirement | Related |
|---|---|---|---|
CLP-02-001 |
MUST | For each case-resolvable assertion the CASE_MANAGER admits at the case layer, it MUST append a canonical CaseLedgerEntry |
|
CLP-02-002 |
MUST | CaseLedgerEntry MUST be a canonical content object distinct from any transport envelope used to replicate it |
|
CLP-02-003 |
MUST | CaseLedgerEntry MUST include the asserted activity payload as its payloadSnapshot, containing all fields required for deterministic replay: the received body for an activity the CASE_MANAGER received, or the body it delivered for one it emits (CLP-07-011, VM-08-003). |
|
CLP-02-004 |
MUST | Every CaseLedgerEntry records a canonical admitted assertion; a CaseLedgerEntry MUST NOT carry a disposition, a reason code or a reason detail, because the ledger contains only admitted entries (see CLP-04-007). |
|
CLP-02-005 |
MUST | When a precondition guard at the CASE_MANAGER refuses an incoming assertion, the refusal MUST be emitted as a Reject protocol activity directed at the asserting participant (CLP-05-001); a refusal MUST NOT be written to the canonical case ledger. A refusal by the sender guard (HP-01-006) and traffic after the case has fully closed (CM-23-013) are answered by no Reject. |
|
CLP-02-006 |
MUST | CaseLedgerEntry MUST include a log_index field corresponding to SYNC-01-002. |
|
CLP-02-008 |
MUST | The log_index field of a CaseLedgerEntry MUST be assigned by the CASE_MANAGER alone, when it commits the entry.Centralising log_index assignment in the single authoritative write path prevents gaps, duplicates, and out-of-order index values that would break hash-chain integrity. |
|
CLP-02-009 |
MUST NOT | The log_index field of a CaseLedgerEntry MUST NOT be assigned by external parties.Allowing external parties to set log_index would enable index spoofing, enabling forked or replayed ledger entries that corrupt the canonical append-only history. |
|
CLP-02-007 |
MAY | CaseLedgerEntry MAY include an optional term field reserved for Raft cluster use. |
|
CLP-02-012 |
MUST | "Accept" and "Reject" MUST be used only for the AS2 activities. The CASE_MANAGER's verdict on whether an assertion enters the ledger is admitted or refused, and the rules that keep refusals off the ledger (CLP-02-005, CLP-04-007, CLP-05-002) govern that verdict, not the Reject activity. A participant's Reject or TentativeReject answering a request (an Offer or an Invite) is a case event: when the CASE_MANAGER admits it, it MUST commit a CaseLedgerEntry for it under CLP-02-001, as it does for the matching Accept.The same word named both the CASE_MANAGER's ledger verdict and the protocol activity, so "rejections are not ledgered" was read as "Reject activities are not ledgered". Handlers then committed nothing for a declined request, the request stayed open on every replica, and each case was argued afresh (#3748, ownership transfer). A declined request closes a request/reply pair, which the ledger must record (CLP-11-001). |
Refines: CLP-02-001 Refines: CLP-05-002 |
CLP-02-013 |
MUST | Every activity the CASE_MANAGER admits at the case layer is a case event and MUST be ledgered (CLP-02-001), except the following, which are not case events: activities processed before a case exists (report submission, case proposal intake); RK, which answers the reporter's Offer outside the case and is never sent to the CASE_MANAGER (RMB-08-003); a replica's receipt of Announce(CaseLedgerEntry), which applies an entry rather than asserting one; activities about the ledger itself, namely the CASE_MANAGER's refusal notice (CLP-05-001) and a replica's Reject(CaseLedgerEntry) asking for redelivery; the activity CLP-10-013 deliberately leaves unledgered, namely a replica's receipt of Announce(VulnerabilityCase) (ADR-0111); an activity only the CASE_MANAGER sends, received by a participant (the bootstrap Create(VulnerabilityCase), its Announce(EmbargoEvent) notice, its Accept or Reject answering a recommender's Offer(Actor)): a participant never commits (CLP-01-003), so such an activity is ledgered, if it is a case event, where the CASE_MANAGER sends it, and the bootstrap Create(VulnerabilityCase) is the case's genesis entry itself (CLP-07-011); a Create(ProcessingFault) NACK (ADR-0080), which reports on message handling rather than changing the case; a late Accept of an embargo invitation that arrives when the case has no embargo for it to bind (EMB-17-004), which changes nothing at any receiver (ADR-0152); and a message whose meaning the receiver does not recognize or whose object cannot be resolved. The list is closed: a new kind of activity that carries no entry MUST be added here first.Whether something is ledgered was decided handler by handler, so a handler that "changed nothing" committed nothing and nobody noticed. A closed list turns each exception into a recorded decision and gives a commit coverage test a fixed set of grounds for exemption. A handler that runs only at a participant has no commit to reach. An activity that changes nothing at any receiver is an observation, not an act, and ledgering it would record a fact every holder of the ledger can already derive (ADR-0152). |
Refines: CLP-02-001 |
CLP-02-014 |
MUST | CaseLedgerEntry MUST carry the entry's semantic type under the wire key semanticType, and the entry hash MUST cover that value under the key semantic_type.The field holds a semantic type, the registry's label for the entry's payload (ADR-0140), and the glossary lists "event type" as a term to avoid for it. |
Refines: CLP-02-001 |
Audit Scope and Continuity
Canonical Ledger and Replication
Refusal Handling
What the CASE_MANAGER does with an assertion it refuses to admit to the ledger. "Refused" is the CASE_MANAGER's ledger verdict, not the AS2 Reject activity: a participant's Reject answering a request is a case event and is admitted and ledgered like any other (CLP-02-012).
Pending Assertion Suppression
Canonical Entry Criteria and Ledger/Process-Log Separation
| ID | Priority | Requirement | Related |
|---|---|---|---|
CLP-07-001 |
MUST | A CaseLedgerEntry committed to the canonical case ledger MUST correspond to exactly one CASE_MANAGER-accepted protocol-significant assertion. A state change the CASE_MANAGER itself writes, such as the RM transitions it records for a leaving participant (CM-23-001), is such an assertion, with the CASE_MANAGER as asserter. |
|
CLP-07-011 |
MUST | The payloadSnapshot of a CaseLedgerEntry MUST be the verbatim AS2 activity that was asserted. For an activity the CASE_MANAGER received, it is the JSON body as received (VM-08-002); for one the CASE_MANAGER emits, it is the body it delivered (VM-08-003). An activity the CASE_MANAGER sends and ledgers is one activity: the case's genesis entry is the Create(VulnerabilityCase) it sends for the case, not a second activity built for the ledger. The CASE_MANAGER MUST NOT drop, re-serialize, inline, patch or merge any field of it; the metadata it adds (index, hashes, commit and receipt times, case_id) lives on the entry, outside the snapshot.The payload snapshot is the replication substrate, and the entry hash covers it, so what it holds is what every replica treats as the event. A snapshot rebuilt from the receiver's object graph records the receiver's reconstruction, not what the sender claimed, and every rewrite is one more thing a replica must reproduce exactly to verify a hash (ADR-0107). |
|
CLP-07-012 |
MUST NOT | The payloadSnapshot of a CaseLedgerEntry MUST NOT be empty for non-rejection entries.An empty payload snapshot for a recorded entry breaks deterministic replay and makes it impossible for replica nodes to reconstruct the case state from the canonical log. |
|
CLP-07-002 |
MUST | Each logical state change MUST produce at most one canonical CaseLedgerEntry; the CASE_MANAGER MUST NOT commit both an admitted-participant-assertion entry and a separately-synthesized CASE_MANAGER entry for the same state change. An act and the state transitions the CASE_MANAGER records as its consequence are different facts, not the same state change. The close_case entry records a received Leave, and the status entries that follow it record the RM transitions the Leave caused (CM-23-001). |
|
CLP-07-013 |
MUST | Every change the CASE_MANAGER makes to the case file MUST be committed as its own CaseLedgerEntry, and a change with no committed entry MUST NOT be made. The act's entry comes first. When the act's own activity states a change, such as Add(Note, target=Case) carrying the note or an invitee's Accept of the stub Invite marking its own consent and joining, that entry is the change's entry, and applying it writes only what the activity states. Each other change the act causes MUST follow as its own CASE_MANAGER-authored entry, in the order the CASE_MANAGER writes them, whose payloadSnapshot is a Create of a record the CASE_MANAGER brings into existence or an Update of a changed record, carrying the object in full (CLP-07-006). A decision the CASE_MANAGER takes from local policy or the clock that changes the case file is committed as the change it makes, its own entry, before the change is made; a decision that changes nothing, such as the observation that a deadline has passed or that a closure is complete, is not committed (ADR-0152). Moving the case's ledger position (ledgerPosition) to an entry as it is committed (SYNC-16-005) is part of applying that entry, not a change to the case file of its own, and takes no entry.Two replicas at the same ledger position must hold the same case file, and a replica infers nothing: if a change is not in the ledger, it did not happen (ADR-0124). One act can cause several changes, such as a stub Invite and the inert invitee's participant record it creates; recording only the act left replicas to rebuild the record with their own ids and clock times (#4425). Full records are ledger content the hash chain must cover, not the inline-object shortcut (CLP-07-006). |
Refines: CLP-07-002 |
CLP-07-014 |
MUST NOT | Case bookkeeping, the records the CASE_MANAGER keeps alongside a case while it works it (such as who recommended whom, offer records and pending markers), is not part of the case file. It MUST NOT be committed to the case ledger, and MUST NOT be read to compute any case-file value, on the CASE_MANAGER or on a replica. The case file is what replicates and what "same ledger position, same case" covers. A field the CASE_MANAGER keeps for its own working, stored on the case object, made replicas at the same position disagree on a value no entry carried ( recommendation_recommender_index, #4425). |
Refines: CLP-04-006 |
CLP-07-015 |
MUST | The CASE_MANAGER MUST commit every act's entries, the case-file changes those entries carry, and every consequence the act commits, such as a re-issued stub Invite, a relayed Invite with its invitee's row, a closure step's status entry or a notice it queues, atomically, so that when any of them fails none of them is written. For a received act, the act's own entry is one of those entries. An embargo act any of whose entries will not apply MUST be refused before anything is written. An act commits its own entry and then one entry per change it causes (CLP-07-013). Committed one at a time, a failure part-way left the act committed without its consequences, a state no single act produces: a proposal in the register that some participant holds no row for, an invitee whose row never moved to its invitation (ADR-0122), or a received Leave whose close_case entry is kept while the status entries it caused are missing. Nothing re-commits them, because a repeated trigger starts a new act and a redelivered act is stopped by its idempotency guard (#4764). An act succeeds or fails as a whole, so no later run finishes it and no repair pass re-decides its consequences (ADR-0146). The same holds for the consequences a later node commits, such as the stub re-issue (CM-11-016), a proposal's relay (EP-09-002), the relay to a joiner (EP-09-011) and a reinstated participant's embargo Invite (CM-31-013): they run in the act's transaction, so a failed later node keeps none of the act's consequences, and a trigger act keeps nothing at all (#4949). A received act's own entry is in the same transaction: had it committed on its own first, a received proposal whose relay failed would stay in the register with no consent rows and no Invites, and its redelivery would be refused, so nothing would ever relay it. With the entry in the act's transaction, a failed received act keeps nothing, and a later delivery of the activity is not stopped as a repeat (ADR-0151, #4933). Through the inbox that later delivery is routed as the copy intake archived, carrying the archived body as its evidence (IE-10-001, VM-08-002), so it commits with the first delivery's body; the sender is told of the failure by an internal-error fault (ASK-07-010, #4981). |
Refines: CLP-07-013 |
CLP-07-003 |
MUST | The payloadSnapshot.actor field of a CaseLedgerEntry(recorded) MUST preserve the original asserting actor's identity. payloadSnapshot.actor MUST name the same actor as the inbound activity's actor, the two compared as normalized actor ids; any other mismatch indicates identity substitution. The CASE_MANAGER's identity appears on the transport envelope and in recording metadata; it MUST NOT be substituted for a participant-asserter's identity. When the CASE_MANAGER also holds a participant role in the case, their identity in payloadSnapshot.actor is the correct asserter identity and is not a substitution. A state change the CASE_MANAGER writes itself has no inbound activity to compare against. Its payloadSnapshot.actor is the CASE_MANAGER, the true asserter, and the comparison does not apply. The RM status entries that follow a close_case entry (CM-23-001) are of this kind, and the leaving participant's own act stays attributed to them in the close_case entry.ADR-0088 makes authority role-based rather than identity-based, so a CVDRole.CASE_MANAGER holder may simultaneously hold a participant role and legitimately assert any participant-role activity. Enforcing this at the receive-pipeline layer — where the original sender's actor_id is still available for comparison — avoids false positives that a commit-boundary identity comparison cannot distinguish. |
|
CLP-07-004 |
MUST NOT | The canonical case ledger MUST NOT contain runtime diagnostics, demo checkpoints, synthetic verification markers, troubleshooting events, or any per-actor observability content; such content belongs in each actor's own logs. | |
CLP-07-006 |
MUST | A payloadSnapshot MUST carry each nested object exactly as the asserted activity carried it. An entry the CASE_MANAGER authors carries in full every object it records. A received entry is the received body (CLP-07-011), so an object is inline where its sender inlined it, as a sender does every object its recipient cannot be expected to hold, and bare where its sender named an object the case's replicas already hold (AKM-02-002). The CASE_MANAGER MUST NOT inline, resolve or replace a reference when it records an entry, and MUST NOT refuse an entry for a bare reference anywhere but the snapshot's own object, whose type is half of the entry's signature. A replica that does not hold an object named by reference dereferences it.A protocol rule, not this prototype's inline-object shortcut (AKM-03-003): the entry hash covers the payloadSnapshot and nothing outside it, so an object carried in full is under the hash every participant verifies, and an object named by id is only identified by it. Filling a reference in at the CASE_MANAGER would put the receiver's reconstruction in place of what was sent, and a replica could not tell the sender's claim from the fill-in (ADR-0107). |
|
CLP-07-007 |
MUST | payloadSnapshot context values MUST use the case URI once a case exists; report URIs MUST NOT appear as the context for ParticipantStatus or other case-scoped snapshots after the report→case promotion has occurredThe obligation falls on whoever builds the activity. A received snapshot is the body as sent (CLP-07-011), so the CASE_MANAGER neither writes a context in nor rewrites one; the commit boundary refuses a context naming another case, and the entry names its case on the envelope (case_id), so a snapshot that names no context, such as the Offer that opened the case, is still that case's entry. |
Per-Case Genesis Hash Derivation and Origin Binding
CASE_MANAGER Inbox Routing for Canonical Ledger Entry Authorship
Requirements for the end-to-end routing path that delivers protocol-significant activities to the CASE_MANAGER's inbox so the CASE_MANAGER can author canonical ledger entries. Covers outbound addressing rules, the rule that the CASE_MANAGER commits its own emissions in the emitting tree rather than mailing itself a copy (ADR-0109), and the pre-flight guard that prevents non-CASE_MANAGER invocations from committing ledger entries.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CLP-10-001 |
MUST | Every protocol-significant act a participant originates MUST send at least one activity to the case's CASE_MANAGER, so that the CASE_MANAGER receives the activity and can author a canonical ledger entry. This requirement applies to ALL participants — the CASE_MANAGER is a participant with extra duties; it is not excluded from CLP-10-001. When the CASE_MANAGER itself originates a protocol-significant activity (e.g., inviting an actor to the case), it MUST commit the canonical entry itself as it sends the activity, and MUST NOT address a copy of the activity to itself in any addressing field. A participant reaches the CASE_MANAGER only by sending it the participant's own activity, never by acting as the CASE_MANAGER (CM-24-004, ADR-0109). Before ADR-0021, several trigger trees (e.g., validate_report, ack_report, close_case) emitted no activity addressed to the CASE_MANAGER. The CASE_MANAGER therefore had no protocol event to commit, and the canonical ledger was missing those semantic types entirely. Delivery to the CASE_MANAGER's own inbox already ran over the uniform HTTP path like any other recipient (OX-12-001); the missing piece was the emit node itself. The self-routing requirement also applies when the CASE_MANAGER is the originator: invite_actor_to_case_trigger_bt emitted an Invite addressed only to the invitee — the CASE_MANAGER never received its own activity and the canonical ledger had no invite_actor_to_case entry. The first fix (ADR-0021 § Clarification) had the CASE_MANAGER add its own id to cc: so a copy looped back through its inbox. That copy compensated for the case owner's container emitting the Invite in the CASE_MANAGER's name from a store that was not the ledger's home; where the two were co-hosted it also committed the same Invite twice (#2996). ADR-0109 replaced the loopback with the rule that the emitting tree commits, and that only the hosting container emits as the CASE_MANAGER. |
|
CLP-10-004 |
MUST NOT | An Announce(CaseLedgerEntry) replication envelope MUST NOT itself be recorded as the payloadSnapshot of a canonical ledger entry; it is the envelope that carries canonical entries to participants, not a ledger event.Including announce_case_ledger_entry in EXPECTED_SEMANTIC_TYPES creates a circular requirement: the CASE_MANAGER would need to commit a ledger entry whose payload is the Announce activity used to broadcast ledger entries, which is self-referential and unachievable without corrupting the hash chain. The Announce(CaseLedgerEntry) wire envelope is a transport artifact, not a protocol-significant domain event; it belongs to the SYNC replication layer (SYNC-02-002), not the canonical ledger content layer. |
|
CLP-10-015 |
MUST | When a receiver must identify a subject that an incoming message names (the invitee of an embargo invitation, the actor whose consent an accept or reject records, or the target of any participant-scoped action), it MUST take that identity from the received activity, never from its own identity as the receiving actor. The two coincide in the ordinary single-recipient case and diverge whenever a message is relayed on a participant's behalf. The receiving actor and the message subject coincide in the ordinary single-recipient case, which is why substituting one for the other went unnoticed. They diverge whenever a message is relayed on a participant's behalf (the CaseActor forwarding), CLI dispatch, or log replay — there the running store's actor is not the invitee. Resolving the subject as resolve_receiving_actor_id() then records consent against, or addresses an action to, the wrong participant, and the divergence is protocol-observable: a conforming peer sees the state change land on an actor the message never named. Source: ISSUE-2762, ADR-0022. |
Refines: CLP-10-005 |
CLP-10-016 |
MUST | Subject-identity resolution (CLP-10-015) MUST NOT select a subject by its position in the to: list.A positional pick resolves a several-recipient message to the same subject in every replica, so each recipient after the first would apply the message to someone else's record. Source: ISSUE-2762, ADR-0022. |
Refines: CLP-10-015 Depends on: EP-09-010 |
Protocol Request/Reply State Detection
Requirements for querying whether a protocol-level request/reply pair (e.g., Offer(CaseParticipant) → Accept/Reject) is still open or has been closed. These rules establish the canonical pattern for duplicate-emission suppression and CASE_MANAGER decision-gate checks.
CaseLedger Causal Ordering — CASE_MANAGER Timestamp Invariants
Normative requirements establishing that log_index order is the canonical causal order from the CASE_MANAGER's perspective, and the temporal invariants that conformance tests can verify against any ledger. See ADR-0079 for the decision record and rationale.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CLP-14-001 |
MUST | A case ledger MUST record entries in causal order from the CASE_MANAGER's observation perspective: if the CASE_MANAGER observes event A before event B, then log_index(A) MUST be less than log_index(B). The log_index sequence is the authoritative causal order; timestamp order is supporting evidence only. The CASE_MANAGER is the single authoritative writer (CLP-01-003, ADR-0041). A single-writer regime produces a total order by construction, so no distributed consensus mechanism is needed. ADR-0079 formalises this design intent as a normative requirement; DEMOMA-22-005 depends on it. |
Refines: CLP-01-003 Refines: SYNC-01-002 |
CLP-14-002 |
MUST | The published timestamp on the CaseLedgerEntry envelope — the CASE_MANAGER's commit stamp, as distinct from the claimed payloadSnapshot.published — MUST be non-null. The CASE_MANAGER MUST stamp each entry with its own authoritative clock at commit time.A null entry timestamp makes invariant checks on ordering impossible and breaks any consumer that tries to order entries by time. Naming the field is load-bearing: an entry carries two timestamps, and applying an envelope requirement to the claimed timestamp (or the reverse) yields a check that is either vacuous or falsely rejecting (ISSUE-2824). |
|
CLP-14-003 |
MUST | The envelope published timestamps (the CASE_MANAGER's commit stamps) on consecutive recorded entries MUST be monotonically non-decreasing in log_index order. An entry with a higher log_index MUST NOT carry an envelope published timestamp earlier than the entry with the immediately preceding log_index. This requirement does NOT apply to the claimed payloadSnapshot.published, whose cross-entry ordering is constrained only per participant by CLP-15-003.A timestamp inversion would contradict the causal-order claim: if entry N has a later log_index than entry N-1 but an earlier timestamp, observers cannot use timestamps as corroborating evidence of the order the CASE_MANAGER recorded. Scoped to the envelope stamp because a single writer stamps every entry from one clock, which makes the property hold by construction. Applied instead to the claimed timestamps it would compare two participants' unsynchronised clocks — the wall-clock ordering ADR-0079 rejected as option C — and would refuse a well-formed assertion whenever two clocks disagreed (ISSUE-2824). |
Refines: CLP-14-001 |
CLP-14-004 |
MUST | All entries in a single case ledger MUST share the same case_id. An entry whose case_id does not match the ledger's case_id MUST be treated as malformed and rejected. A ledger mixing entries from multiple cases makes causal comparisons meaningless and breaks hash-chain verification (which is scoped per case). |
|
CLP-14-005 |
MUST | log_index values MUST be unique within a single case ledger. No two entries for the same case_id may share a log_index value. Duplicate indices would make it impossible to identify which entry is causal-order position N, breaking any test or consumer that relies on log_index to determine ordering. |
Refines: SYNC-01-002 |
CLP-14-006 |
MUST | Every CaseLedgerEntry's envelope published timestamp MUST be on or after the parent VulnerabilityCase's own published timestamp. A ledger entry MUST NOT be stamped with a time that precedes the case's creation. The CASE_MANAGER MUST additionally reject a recorded assertion whose claimed payloadSnapshot.published precedes the parent case's published timestamp by more than the configured clock-skew tolerance (CLP-14-009).The CASE_MANAGER is the single writer and creates the case before committing any entries. An entry predating the case indicates a clock error or commit-path bug; treating it as a protocol violation catches the defect at the boundary. The envelope half holds by construction and is verified by the conformance harness. The claimed half needs a tolerance because the participant's clock is not the CASE_MANAGER's — a zero-tolerance comparison across two clocks would refuse well-formed assertions from any participant whose clock lags, which is the wall-clock coupling ADR-0079 rejected as option C. |
|
CLP-14-007 |
SHOULD | The CASE_MANAGER SHOULD reject a payload assertion whose payloadSnapshot.published timestamp is more than a configurable tolerance period ahead of the CASE_MANAGER's clock at the time of receipt. The default tolerance SHOULD be no more than five minutes to match common clock-skew conventions. A payload timestamped in the future relative to the CASE_MANAGER's clock indicates a participant clock error or a malformed assertion. Accepting it silently would produce ledger entries where the payload claims to have been published after the ledger envelope was stamped — a contradiction of the observation model in ADR-0079. A small tolerance accommodates legitimate clock drift without rejecting well-formed messages from participants whose clocks are slightly ahead. |
Refines: CLP-14-001 |
CLP-14-008 |
SHOULD | The CASE_MANAGER SHOULD reject a payload assertion whose payloadSnapshot.published timestamp is more than a configurable staleness window in the past relative to the CASE_MANAGER's clock. The default staleness window SHOULD be seven days. A payload with a timestamp far in the past suggests the participant is replaying old activities, potentially to reconstruct or manipulate ledger history. Rejecting stale payloads limits the window in which a replayed or delayed assertion can be inserted into the canonical ledger. |
Refines: CLP-14-001 |
CLP-14-010 |
MUST | The log_index values of a case ledger MUST form a gapless sequence starting at 0. The genesis entry MUST carry log_index 0, and every later entry MUST carry the log_index of its immediate predecessor plus one. The CASE_MANAGER MUST NOT skip an index when it assigns one. Receivers detect a missing entry by index arithmetic. An entry whose log_index is greater than tail_index + 1 is a forward gap (SYNC-14-001), and a replica that lags is valid only while its local prefix has no gaps. Both tests are meaningful only if the CASE_MANAGER never leaves a gap by design. CLP-14-001 requires only that indices increase, and CLP-14-005 requires only that they are unique. Neither excludes a sparse ledger, and ADR-0079 once said that sparse numbering was allowed (ISSUE-2752). |
Refines: SYNC-01-002 |
Participant Assertion Timestamp Obligations
Normative requirements on what well-behaved participants MUST do when emitting assertions to the CASE_MANAGER, so that the CASE_MANAGER's observation-order model (ADR-0079, CLP-14) yields a ledger that faithfully reflects real-world causal order within a single participant's event stream.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CLP-15-001 |
MUST | A participant MUST emit protocol activities for a given case in causal order. If activity A causally precedes activity B in the participant's own event stream, A MUST be emitted to the CASE_MANAGER before B. The CASE_MANAGER uses arrival order as the authoritative sequence. If a participant emits B before A despite A causing B, the CASE_MANAGER's ledger will record the wrong causal order. The participant is responsible for correct ordering; the CASE_MANAGER cannot reconstruct causality it did not observe. |
|
CLP-15-002 |
MUST NOT | A participant MUST NOT batch causally ordered activities and submit them to the CASE_MANAGER in arbitrary order. If events A, B, C are causally ordered (A → B → C), the participant MUST NOT submit them as C, A, B or any permutation that reverses causal dependencies. Batched out-of-order submission produces a malformed ledger from the CASE_MANAGER's perspective. This is a protocol violation on the participant's part, not a CASE_MANAGER failure. |
Refines: CLP-15-001 |
CLP-15-003 |
MUST | For causally related activities within the same participant's event stream, the published timestamp on activity B MUST be greater than or equal to the published timestamp on activity A, where A causally precedes B. A participant MUST NOT assign a published timestamp to B that precedes A's published timestamp when A and B are causally ordered. A participant that assigns earlier timestamps to later events produces a payloadSnapshot whose timestamp order contradicts the submission order. Even if the CASE_MANAGER's own entry timestamps are correct, the snapshot timestamps become evidence against the order recorded in the ledger. |
Refines: CLP-15-001 |
CLP-15-004 |
SHOULD | A participant SHOULD assign published timestamps that reflect the actual time the event occurred, not the time the batch was submitted or a retry was attempted. Backdated or forward-dated timestamps degrade the forensic value of the ledger even when causal order is preserved. Timestamps that reflect actual occurrence time allow CaseActors and auditors to detect anomalies like stale replays (CLP-14-008). |
|
CLP-15-006 |
MUST | A participant MUST include a published timestamp on every protocol activity it emits. A receiver MUST treat an inbound activity that carries no published as malformed and reject it at message validation, and MUST NOT substitute its own clock for the missing value.ActivityStreams makes published optional, so a receiver that defaults the field cannot afterwards distinguish a time the sender claimed from one the receiver invented. Every downstream check that reads the claimed timestamp then compares the receiver's clock against itself and can never fire: this is exactly how CLP-14-007 and CLP-14-008 were left vacuous (ISSUE-3149). Rejecting at the threshold keeps the distinction, and keeps a fabricated value from entering a participant's claimed stream where CLP-15-003 would read it as a regression. A receiver's own outbound authoring is unaffected — stamping the local clock on an activity you are creating is correct. |
Refines: CLP-15-004 |
CLP-15-005 |
MUST | The CASE_MANAGER's ledger is authoritative for what the CASE_MANAGER observed from its own perspective. It is NOT a guarantee of real-world causal fidelity when external participants self-report events. The CASE_MANAGER MUST record the observation-order sequence it received; it MUST NOT attempt to reconstruct participant-internal causal order it cannot verify. Even a compliant participant's clock may drift, and a non-compliant participant may submit events in arbitrary order. The CASE_MANAGER's role is to record faithfully what arrived, in the order it arrived, while applying the timestamp sanity checks in CLP-14-007 and CLP-14-008. Attempting to reorder entries based on payload timestamps would require trusting externally-supplied data to override the CASE_MANAGER's own observation — a violation of the single-writer authority model. |
Refines: CLP-01-003 |
CLP-15-007 |
MUST | A receiver MUST carry the published and updated timestamps of every object nested in an inbound activity as received. A timestamp the sender omitted MUST remain absent; the receiver MUST NOT substitute its own clock for it, whether at parsing, extraction, storage or rendering. Where a decision requires a nested timestamp that is absent, the receiver MUST reject the message at message validation.AS2 makes object timestamps optional. A receiver-minted value is later indistinguishable from the sender's claim: it gave each receiver its own genesis hash for one case and ordered received statuses by arrival (ISSUE-3257). Absence harms only a decision that needs the time, so that decision refuses it early and loudly; every other consumer tolerates it (ISSUE-2553). |
Refines: CLP-15-006 |
Case Management
Requirements for VulnerabilityCase lifecycle management, CASE_MANAGER responsibilities, and the RM/EM/CS/VFD state model. Covers actor isolation at the protocol level, the distinction between participant-specific and participant-agnostic state, case initialization sequencing, embargo acceptance ownership, and the invitation acceptance lifecycle.
File ID: CM
Actor Isolation
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-01-001 |
MUST | Each actor MUST have an isolated protocol state domain | |
CM-01-002 |
MUST | Each actor's RM state MUST be maintained independently per case |
CASE_MANAGER Lifecycle
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-02-001 |
MUST | Each VulnerabilityCase MUST have exactly one associated CASE_MANAGER | |
CM-02-002 |
MUST | CASE_MANAGER MUST be the authoritative source of truth for case state | |
CM-02-003 |
MUST | CASE_MANAGER MUST persist until the associated VulnerabilityCase is closed | |
CM-02-004 |
MUST | CASE_MANAGER MUST know the case owner | |
CM-02-005 |
MUST | CASE_MANAGER MUST restrict the following activities to the case owner: case closure, approving an embargo proposal to become active on the case and terminating an active embargo, approving or rejecting participant invitations, case ownership transfer, and offering a participant an additional role (CM-33-002) (see CM-02-006 for the authorisation mechanism). | |
CM-02-006 |
MUST | CASE_MANAGER MUST enforce case-level authorization for all case mutations | |
CM-02-007 |
MUST | A VulnerabilityCase MUST carry a notes collection recording the case-scoped notes added to it by Add(Note) activities that target the case |
|
CM-02-008 |
MUST | When a VulnerabilityCase is created from an originating VulnerabilityReport Offer, the actor that received the Offer (the CASE_OWNER) MUST be recorded as the initial primary participant, and the case's attributed_to MUST be set to that actor's ID on every creation path, including when the CASE_MANAGER creates the case (CP-09-001).attributed_to is the case's owner field: ownership checks read it (MSM-07-008) and ownership transfer rewrites it alongside the CASE_OWNER role (CM-21-002). Who created the case is carried by the actor of Create(VulnerabilityCase), not by attributed_to. |
|
CM-02-009 |
MUST | The CASE_MANAGER MUST apply its own trusted timestamp to every state-changing event it receives, regardless of any timestamp supplied by the sending participant The CASE_MANAGER's clock is the only trusted source of time for event ordering within a case; using participant-supplied timestamps would allow different copies of a case (held by different actors) to disagree on event order, undermining auditability and the single-source-of-truth guarantee |
Depends on: CM-02-002 |
Case State Model
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-03-001 |
MUST | The system MUST implement the three interacting state machines: RM (Report Management), EM (Embargo Management), and CS (Case State) | Implements: VP-01-001 Implements: VP-13-001 Implements: VP-14-001 |
CM-03-002 |
MUST | RM state MUST be participant-specific | Implements: VP-01-002 Implements: VP-02-002 Implements: VP-03-001 |
CM-03-003 |
MUST | A shared EM state MUST be tracked at the case level in the case's CaseStatus, to represent the collective embargo agreement |
Implements: VP-04-002 Implements: VP-13-009 Implements: VP-14-001 |
CM-03-008 |
MUST | Each CaseParticipant MUST track their own embargo consent through its per-embargo consent rows (CM-10-001, CM-18-001) |
Implements: VP-04-002 |
CM-03-004 |
MUST | CS (PXA sub-state) MUST be participant-agnostic (shared per case) | |
CM-03-005 |
MUST | VFD (Vendor Fix Deployment) state MUST be participant-specific | |
CM-03-007 |
MUST | RM state transitions MUST use the established protocol activity types without introducing new activity types for reverse transitions Keeps the RM symbol set minimal and produces clean audit histories; Undo conflates historical negation with a new forward transition |
Implements: VP-02-004 |
State Transition Correctness
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-04-001 |
MUST | A receiver processing an RM state transition MUST update the RM state in the participant status of the sending actor's CaseParticipant |
Implements: VP-02-002 Implements: VP-02-003 Implements: VP-03-001 Implements: VP-13-005 |
CM-04-002 |
MUST | A receiver processing a VFD state transition MUST update the VFD state in the participant status of the sending actor's CaseParticipant |
|
CM-04-003 |
MUST | When processing an EM state transition and the accepting actor is the case owner, the receiver MUST update the case-level EM state in the case's CaseStatus. |
Implements: VP-06-001 Implements: VP-09-001 Implements: VP-11-001 Implements: VP-11-002 Implements: VP-14-001 Implements: VP-14-002 Depends on: CM-03-008 |
CM-04-004 |
MUST | A receiver processing a PXA state transition (public disclosure, exploit publication, attack observation) MUST update the case-level PXA state in the case's CaseStatus |
Implements: VP-03-002 Implements: VP-14-003 Implements: VP-14-004 |
CM-04-005 |
MUST | A receiver processing a VFD or PXA transition MUST validate the requested transition against the authoritative current state before recording it | |
CM-04-006 |
MUST NOT | A participant-specific transition (RM or VFD) MUST NOT change any case-level (participant-agnostic) state (EM or PXA), and a case-level transition (PXA or EM) MUST NOT change any participant-specific state (RM or VFD). | Implements: VP-13-009 Depends on: CM-03-008 |
CM-04-007 |
MUST | RM transition validation MUST treat RM.CLOSED as terminal before applying any same-state no-op shortcut. A repeated CLOSED -> CLOSED update MUST be treated as a terminal no-op that does not append new participant status records, trigger broadcast, or create new case-ledger entries.Checking equality first can let duplicate close retries flow into downstream append/broadcast paths as fresh state changes. |
|
CM-04-008 |
MUST | When processing an EM state transition and the accepting actor is any participant other than the case owner, the receiver MUST update only that participant's own consent row for the accepted embargo. Non-owner participants do not advance the shared EM state; their individual acceptance updates only their own consent record. |
Object Model Relationships
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-05-001 |
MUST | VulnerabilityReport, VulnerabilityCase, VulnerabilityRecord, CaseReference, CaseParticipant, and ParticipantStatus MUST be distinct, non-interchangeable object types, and a message MUST NOT carry one of them where another is expected. The CASE_MANAGER is not in this list: it is an ordinary actor designated for a case, not an identity or a distinct object type (ADR-0088). |
|
CM-05-002 |
MUST NOT | A VulnerabilityCase MUST reference at least one VulnerabilityReport; a case with zero reports MUST NOT exist |
Implements: VP-02-015 Implements: VP-02-020 |
CM-05-003 |
SHOULD | A VulnerabilityCase SHOULD reference at least one VulnerabilityRecord before closure |
|
CM-05-004 |
MAY | A VulnerabilityCase MAY have zero or more associated CaseReference objects |
|
CM-05-005 |
MUST | CaseReference objects MUST include at minimum a url field. |
|
CM-05-006 |
MAY | One report MAY describe multiple vulnerabilities; one case MAY group multiple reports (e.g., when overlapping participants are involved) | |
CM-05-007 |
MAY | Multiple CaseReference objects MAY arise from a single case (e.g., coordinated simultaneous disclosure by multiple vendors) |
|
CM-05-008 |
MUST NOT | VulnerabilityRecord identifiers MUST be treated as opaque strings; the system MUST NOT restrict identifier values to CVE ID format or any other specific namespaceDifferent organizations use different identifier namespaces (CVE, CERT/CC VU#, vendor-assigned IDs, etc.); requiring a specific format excludes valid records and prevents multi-namespace support |
|
CM-05-009 |
SHOULD | A VulnerabilityRecord SHOULD support a list of alias identifiers from different namespaces for the same vulnerabilityMultiple IDs from different namespaces may refer to the same underlying vulnerability (e.g., a CVE ID and a CERT/CC VU# ID) |
|
CM-05-010 |
MUST | When a VulnerabilityRecord is a CVERecord (i.e., it carries a CVE ID), its data MUST conform to the CVE JSON schema (https://github.com/CVEProject/cve-schema) |
|
CM-05-011 |
SHOULD | CaseReference objects SHOULD include a name field containing a human-readable title.A human-readable title makes references easier to identify in case summaries and reports. |
|
CM-05-012 |
MAY | CaseReference objects MAY include a tags field (array of one or more type descriptors, e.g., "patch", "vendor-advisory", "third-party-advisory", "exploit", "release-notes").Type descriptor tags allow consumers to filter or categorise references without parsing the URL or name. |
Case Update Broadcast
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-06-001 |
MUST | When the CASE_MANAGER updates canonical case state, it MUST notify every active participant (CM-10-004). Every change the CASE_MANAGER makes to the case file is its own ledger entry (CLP-07-013), so the ledger fan-out is the notice: each change reaches the active participants as Announce(CaseLedgerEntry). The CASE_MANAGER also announces the updated case as Announce(VulnerabilityCase) after admitting a participant's engagement. No participant sends the CASE_MANAGER an Update(VulnerabilityCase); one that arrives is the shape of the CASE_MANAGER's own update_case_owner act, which no received use case handles, so it is reported "no use case found" (SE-05-002, ADR-0140). |
Implements: VP-02-019 Implements: VP-03-012 Implements: VP-08-001 |
CM-06-002 |
MUST | Participants receiving a case update notification MUST treat the update as authoritative only when it originates from the CASE_MANAGER | Depends on: CM-02-002 |
CM-06-003 |
MUST | Case update notifications MUST include the updated CASE_MANAGER's actor URL so participants can fetch the current case state if needed | |
CM-06-004 |
MUST | Participants MUST authenticate case update notifications before accepting them as authoritative | |
CM-06-005 |
MUST | When a note is added to a case, the CASE_MANAGER MUST broadcast the note to every active participant (CM-10-004), excluding the note author. | Depends on: OX-03-001 Depends on: CM-02-001 |
Redacted Case View
Per-Participant Embargo Acceptance
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-10-001 |
MUST | CaseParticipant MUST record its consent per embargo: one consent row for every entry in the case's embargo register, naming the embargo and the participant's answer to it, UNINVITED until it is asked (CM-18-001). A participant agrees to an embargo explicitly, by an Accept, as its proposer, by seeding (CM-14-003, CM-14-005), or by containment, when the case owner activates a revision that ends no later than terms the participant had agreed to (EP-05-001). Each invitation's RSVP deadline is stored on that invitation's INVITED row (CM-28-013). No other record of embargo consent may exist on the participant.Consent is per embargo (ADR-0093, ADR-0122). The rows are what the CM-10-004 content gate reads, so they must name every embargo the participant is bound to, including one carried over by containment: agreeing to N days is agreeing to every shorter period, so no explicit Accept is owed for it. A second record of consent beside the rows — the scalar PEC state this replaced — could disagree with them (Concern #3884); there is now nothing to disagree with. |
Implements: VP-05-001 |
CM-10-002 |
MUST | Embargo acceptances MUST be timestamped by the CASE_MANAGER at the time of receipt, not using the participant's claimed timestamp The CASE_MANAGER applies the only trusted timestamp; the participant's reported time cannot be verified |
Depends on: CM-02-002 Implements: CM-02-009 |
CM-10-004 |
MUST | The CASE_MANAGER MUST share case content only with active participants. A participant is active when it was seated by the case initialization sequence (CM-14-001: the Case Owner, the CASE_MANAGER and the reporter) or has accepted the stub Invite (Accept(Invite(Actor, VulnerabilityCaseStub))), has not been removed (CM-31-001), and, only when an embargo is active, its consent row for that embargo is AGREED (CM-18-001). Every other participant is inert and MUST NOT receive case content: no CaseLedgerEntry announcements, no Announce(VulnerabilityCase), no status broadcasts, no embargo announcements (ADR-0114).Many cases have no active embargo — one not yet established, or one already exited — so "has accepted the active embargo" alone cannot be the entitlement test. Before ADR-0114 this rule held only because a participant that had not accepted was absent from the roster; the Invite now creates the record, so entitlement has to be checked rather than implied by membership. |
|
CM-10-005 |
MUST | While a case has an active embargo, the CASE_MANAGER MUST NOT send an Announce(CaseLedgerEntry) to a participant that has not accepted that embargo (the CM-10-004 content gate), whether as fan-out of a new entry or as a replay answering that participant's Reject(CaseLedgerEntry) (SYNC-03-002); the participant's replica stays at the last entry it was sent. Embargo meta-protocol activities addressed to the participant directly (an embargo Invite and its responses) are not ledger fan-out and MUST still be delivered.The ledger fan-out is how participants learn every accepted case change, and each entry carries its payload verbatim — an added report or note reaches the recipient whole. Fan-out is therefore "sharing case updates" in CM-10-004's sense. An entry cannot be redacted per recipient without breaking the hash chain. Skipping a single entry does not work either: the replica buffers the next one as a forward gap and Rejects to request the missing entry (SYNC-14-002), so the replay would deliver the withheld content anyway. The gate therefore pauses the participant's whole stream, replay included, rather than filtering entries. |
Refines: CM-10-004 |
CM-10-006 |
MUST | When the CM-10-004 content gate admits a participant whose ledger stream it had paused (CM-10-005) — whenever the active check turns from false to true for that participant, whether because it accepts the active embargo, the embargo ends, or it is reinstated (CM-31-011) — the CASE_MANAGER MUST send that participant every canonical entry it was not sent, in log order starting with the first entry withheld from it, before or together with the entry that records the change admitting it. A paused participant's replica is a contiguous prefix of the ledger; backfilling the withheld suffix in order restores it to the leader's tip without a gap the catch-up gate (SYNC-10-004) would block on. Ending the embargo lifts the gate for everyone, so a participant that never accepted it is caught up at that point too. The trigger is the active check itself, not any one message (ADR-0116). |
Depends on: CM-10-005 Depends on: SYNC-04-001 |
Joining a Case — Stub Invite, Inert Participant, Full-Case Invite
How an actor goes from invited to participating: the stub Invite creates an inert participant record, the participant's reply to it decides whether it joins, and a joined participant judges the case by replying to a full-case Invite (ADR-0114, ADR-0121; notes/case-joining.md).
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-11-001 |
MUST | When a participant accepts the stub Invite (Accept(Invite(Actor, VulnerabilityCaseStub))), the CASE_MANAGER MUST leave the participant's RM state at RM.RECEIVED, set its VF state to V when it holds the VENDOR role, and, when an embargo is active, mark its consent row for that embargo AGREED, making it a signatory. Accepting the stub is joining the case and consenting to the active embargo; it is not a judgement of the case.The invitee has seen only the stub. Its judgement of the case comes after it has received the case and its history, as its reply to the full-case Invite (CM-11-010). |
Implements: VP-03-012 Depends on: CM-03-001 |
CM-11-002 |
SHOULD | After joining a case, a participant SHOULD judge the case by replying to the full-case Invite (CM-11-010, CM-11-011) and then SHOULD decide to engage or defer with Join(VulnerabilityCase) (RM.ACCEPTED) or Ignore(VulnerabilityCase) (RM.DEFERRED). |
Depends on: OX-02-001 Depends on: CM-11-001 |
CM-11-004 |
MUST | The CASE_MANAGER MUST NOT treat a joined participant as having committed to the case (RM.ACCEPTED) until it receives Join(VulnerabilityCase) from that participant.Only the participant can say it has chosen to engage. Neither accepting the stub Invite (joining) nor accepting the full-case Invite (RM.VALID) is an engagement decision. |
Depends on: CM-11-001 |
CM-11-003 |
MUST | A receiver processing a reply to a stub Invite (Accept or Reject of Invite(Actor, VulnerabilityCaseStub)) MUST identify the case from the case the nested Invite's stub names, never by deriving it from the stub's ID; the top-level target of the reply is not populated.The reply wraps the Invite as its object_, so the stub is on object_.target, which extract_event maps to inner_target. The stub has its own ID (<case-id>/stub) and names the case explicitly (CM-11-013); parsing one ID out of the other would couple every receiver to the ID scheme. |
|
CM-11-006 |
MUST | When the CASE_MANAGER sends a stub Invite (Invite(Actor, VulnerabilityCaseStub)), it MUST create the invitee's CaseParticipant in the same step, carrying the roles the Invite names, at RM RECEIVED, VF v when it holds the VENDOR role, and an INVITED consent row for the active embargo when one is active. The participant is inert (CM-10-004). The ledger holds the wire messages exchanged and every state change has a message, so sending the Invite commits two entries: the Invite's (invite_actor_to_case) and, separately, the CASE_MANAGER's own act of creating the record, create_case_participant (Create(CaseParticipant)), whose object is the whole record as the CASE_MANAGER stored it, with its statuses and consent rows and their ids and times. A replica applying create_case_participant MUST store that record as the entry carries it, MUST NOT derive any part of it, and MUST derive nothing from the Invite's entry (ADR-0114, ADR-0124).The CASE_MANAGER needs to track every invitee from the moment it is invited — whether it has been told and how it answered — including the invitees that decline (ADR-0114). |
|
CM-11-007 |
MUST | A stub Invite has exactly two valid replies, Accept (CM-11-001) and Reject. On Reject(Invite(Actor, VulnerabilityCaseStub)) the CASE_MANAGER MUST move the participant's RM state to RM.CLOSED, set VF V when it holds the VENDOR role, record embargo consent DECLINED when an embargo is active, and keep the participant record, which stays inert. The invitee's Reject message is its own ledger entry (reject_invite_actor_to_case) and is the entry for the DECLINED consent row, which a replica applies through the same write; the closing status is the CASE_MANAGER's add_participant_status_to_participant entry, which a replica stores. TentativeReject(Invite(Actor, VulnerabilityCaseStub)) MUST be refused.A stub reject is a hard no to joining. Keeping the record keeps the history that the actor was told and declined, which is what the early record exists for. Accepting the stub is not a judgement of the case, so there is nothing for a tentative reject to say. |
|
CM-11-008 |
MUST | When the CASE_MANAGER processes Accept(Invite(Actor, VulnerabilityCaseStub)) and the participant is active (CM-10-004), it MUST send the participant Announce(VulnerabilityCase) and begin replaying the case ledger to it without waiting for any further signal from the participant.The participant is entitled to case content from the moment it is active, and the history is part of what it then judges. The Accept is the readiness signal; out-of-order arrival is buffered (SYNC-14, SYNC-15) and a real gap is recovered by Reject(CaseLedgerEntry) and replay. |
|
CM-11-009 |
MUST | Any reply from a VENDOR-role invitee to its stub Invite MUST set its VF state to V. The CASE_MANAGER or Case Owner MAY set an inert invitee's VF to V before it replies (PRM-06-003); that write MUST NOT change its RM state.Any reply is evidence the vendor is aware of the case. |
|
CM-11-010 |
MUST | After CM-11-008, the CASE_MANAGER MUST send the participant Invite(Actor, VulnerabilityCase) — the full-case Invite — queued after the last replayed ledger entry, referencing the case by ID and carrying the log_index and entry_hash of its own ledger tail when it issued the Invite. A receiver MUST refuse a full-case Invite whose sender is not the case's CASE_MANAGER, as recorded on its replica or, before the replica exists, as the trust anchor recorded from the stub Invite (PCR-03-004), and a receiver with neither MUST refuse it.The participant already holds the case from the Announce, so the Invite only asks the question and carries no copy of the case. Delivery to each recipient is ordered (ADR-0112), so queuing the Invite after the replay means it arrives once the participant can answer it; its ledger position is the floor the participant must reach before answering (ADR-0121). |
|
CM-11-011 |
MUST | A reply to the full-case Invite MUST carry the replier's own ledger position (log_index and entry_hash of its tail when it decided). The CASE_MANAGER MUST record Accept(Invite(Actor, VulnerabilityCase)) (RV) as RM.RECEIVED → RM.VALID, TentativeReject(Invite(Actor, VulnerabilityCase)) (RI) as RM.RECEIVED → RM.INVALID, and Reject(Invite(Actor, VulnerabilityCase)) (RC) as RM.RECEIVED → RM.CLOSED. |
|
CM-11-012 |
MUST | The CASE_MANAGER MUST refuse a reply to the full-case Invite whose ledger position is behind the Invite's, or names an entry its own ledger does not hold at that index. It MUST accept a reply at or beyond the Invite's position. It MUST also refuse a reply from a participant that has not joined the case (an inert participant), and write nothing. The Invite's position is a floor, not a pin. A reply behind it was made without seeing what the participant was asked to judge; a reply beyond it is better informed. Pinning the reply to the Invite's position would make every reply on a busy case stale (ADR-0121). |
|
CM-11-013 |
MUST | A case stub MUST carry its own wire type, VulnerabilityCaseStub, and its own ID, the case's ID with /stub appended, and MUST name the case it stands for in a field of its own. A receiver MUST classify a stub by its type, never by which fields it carries or by parsing its ID.A receiver classifies an activity by its shape. Sharing the case's type and ID made the stub Invite and the full-case Invite indistinguishable (ADR-0114). |
|
CM-11-014 |
MUST | A stub Invite MUST carry a reply deadline. When the deadline passes unanswered, the CASE_MANAGER MUST close the Invite as an expired ask and MUST NOT change the invitee's participant state: the record stays inert at RM.RECEIVED, and its consent row for the embargo stays INVITED — the CM-28-004 time-out-to-TIMED_OUT rule applies to an Invite(EmbargoEvent), not to the embargo terms a stub Invite carries. Case-wide closure checks ("all participants closed") MUST count only participants that have joined: those seated by the case initialization sequence (CM-14-001) and those that have accepted their stub Invite.RM is the participant's own judgement (ADR-0121), so a timeout cannot stand in for one. An invitee that never answered must not keep the case from closing (ADR-0114). |
|
CM-11-015 |
MUST | When the CASE_MANAGER re-invites an invitee that has not answered its stub Invite, or whose stub Invite expired, the re-invite MUST be a fresh stub Invite on the same participant record with a new deadline, and its inReplyTo MUST be the id of the invitee's earlier stub Invite. A stub Invite to a participant that has joined or is at RM.CLOSED MUST be refused, wherever the CASE_MANAGER would send one, before any ledger entry is committed for it (ADR-0085).One actor has one participant record per case. RM.CLOSED is terminal with no rejoin path, and a stub reject is a closure (CM-11-007). Naming the earlier stub in the standard AS2 inReplyTo property leaves the invitee one live stub, and CM-11-016 then refuses an Accept of the earlier one, naming the fresh one. |
|
CM-11-016 |
MUST | When the active embargo is activated, revised or terminated while a stub Invite is outstanding, the CASE_MANAGER MUST re-issue the stub Invite on the same participant record with the current embargo terms and a new deadline, and the replacement MUST name the Invite it supersedes by setting its inReplyTo to that Invite's id. The CASE_MANAGER MUST refuse an Accept of a superseded stub Invite, naming the replacement in the refusal, and MUST honour a Reject of it.Accepting a stub consents to the terms it carried. Accepting stale terms would make the participant join already lapsed, or join without consent, and need a second answer at once. Naming the superseded Invite tells the invitee the old one is dead in whichever order the two arrive; a retraction ( Undo) was considered and rejected (ADR-0114). Declining to join does not depend on the terms. |
|
CM-11-017 |
MUST | The CASE_MANAGER MUST act on a reply to a stub Invite only when the sender is the invitee of a stub Invite it sent and recorded, and MUST take the participant's roles from that recorded Invite, never from the copy of the Invite embedded in the reply. A reply from any other sender MUST be refused with no participant created or changed. Today the Accept handler admits whichever actor the embedded Invite names, with the roles that copy carries, without looking for an Invite in the store; any actor can therefore admit itself or a third party as, say, a VENDOR and then receive the case and its full ledger. See #3733. |
Refines: HP-01-006 Refines: CM-11-001 |
CM-11-018 |
MUST | When the CASE_MANAGER receives Reject(Invite(Actor, VulnerabilityCaseStub)) and holds no participant record for the invitee, it MUST refuse the reject with a reported reason. A Reject whose invitee has no recorded participant MUST NOT succeed as a silent no-op.The participant record created at invite-send time (CM-11-006) is the evidence that the CASE_MANAGER sent the stub Invite. A Reject with no matching record is either a protocol violation (the CASE_MANAGER never sent the Invite) or a replay of a Reject already processed to RM.CLOSED; both cases are erroneous and must be reported, not silently accepted. Accepting them would hide inconsistencies in the CASE_MANAGER's own audit trail. |
Refines: CM-11-007 |
CM-11-021 |
MUST | When the CASE_MANAGER receives Accept(Invite(Actor, VulnerabilityCaseStub)) and holds no participant record for the invitee, it MUST refuse the accept with a reported reason, and MUST NOT create a participant record, commit a ledger entry or send any activity. An Accept never creates a participant: the record exists from the stub Invite (CM-11-006).The participant record created at invite-send time (CM-11-006) is the evidence that the CASE_MANAGER sent the stub Invite, and CM-11-017 ties every reply to a recorded Invite. An Accept that finds no record would otherwise rebuild one on acceptance, which is the old join model (participant created on Accept(Invite)) and would admit an actor whose Invite the CASE_MANAGER's roster no longer vouches for. This is the Accept counterpart of CM-11-018. |
Refines: CM-11-017 Refines: CM-11-001 |
CM-11-019 |
MUST | A stub Invite MUST carry at least one CVD role for the invitee. The CASE_MANAGER MUST refuse to emit a stub Invite with an empty role list and MUST NOT create a participant record with an empty case_roles list. The CASE_MANAGER MUST NOT substitute a default role (such as VENDOR) when the inviter provides no roles. This applies to the case owner's own stub Invite, where the owner names the roles and no role is guessed; the default role list for a suggested actor is a separate flow (CM-16-003).A participant with no roles cannot be routed to the right recipients or granted the right access. Defaulting to VENDOR would silently assign a role the inviter never requested, violating the inviter's intent. Refusing at send time catches missing roles before the Invite is delivered, keeping the error close to its source. |
Refines: CM-11-006 |
CM-11-020 |
MUST NOT | A participant that joined a case through an Invite MUST NOT send Accept, TentativeReject or Reject of the original Offer(VulnerabilityReport), and MUST NOT validate, invalidate or reject the case's report. It was never offered that report; it judges the case by replying to the full-case Invite (CM-11-011).A reply must answer a message sent to the replier. And a joined participant — a late joiner especially — judges the case as it stands, with its history, not the original submission, which may have been far sparser (ADR-0121). |
Case Creation Timing
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-12-002 |
MUST | At case creation, the system MUST create two initial CaseParticipant records in the order defined by CM-14-001 |
Depends on: CM-03-001 Depends on: CM-02-008 Part of: CM-14-001 |
CM-12-003 |
MUST | When a case is created from an Offer(Report), the case owner (the report receiver) MUST queue a Create(Case) activity to notify the reporter that a case has been opened, naming the CASE_MANAGER it delegated to (CBT-01-001) |
Depends on: OX-02-001 |
CM-12-004 |
MUST | A default embargo MUST be initialized at case creation (RM.RECEIVED) following the sequence in CM-14-001 whenever the case is embargo-eligible — from the receiver's actor default where one is configured, and otherwise from the protocol default of EP-04-005 | Refines: DUR-07-002 — which now applies at case receipt Part of: CM-14-001 Depends on: EP-04-005 |
Embargo Acceptance Ownership
Case Initialization Sequence
Defines the canonical ordered sequence of initialization steps that MUST occur when a VulnerabilityCase is created at Offer(Report) receipt (CM-12-001). All steps MUST complete before the case is considered fully initialized.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-14-001 |
MUST | The case initialization sequence MUST execute exactly 6 ordered steps (CM-14-008 through CM-14-013) before the case is considered fully initialized. No step may be omitted or reordered. Step CM-14-009 precedes CM-14-010 so that the embargo is always created by a participant (the case owner). The Create(Case) notification in CM-14-011 is flushed before the reporter is added in CM-14-012 so the reporter's system receives the case object before any subsequent Announce(CaseLedgerEntry) fan-out that references it. |
Depends on: CM-12-001 |
CM-14-002 |
MUST | The case-owner CaseParticipant MUST be created before the default embargo is initialized, so that the embargo is always associated with an existing participantAn embargo without any participant lacks an owning actor. Creating the owner participant first ensures that the embargo is initialized in the context of an actor who can be responsible for it. |
Depends on: CM-14-001 |
CM-14-003 |
MUST | When the case initialization sequence creates a default embargo, the case owner MUST be seeded as a signatory on that embargo immediately after the embargo reaches EM.ACTIVE stateThe case owner initiated the embargo by accepting the default policy on behalf of the case. Requiring a separate accept step would leave the case owner as a non-signatory on their own active embargo. |
Depends on: CM-14-001 Refines: CM-13-006 |
CM-14-004 |
MUST | The Create(Case) notification MUST be queued and the outbox flushed before the reporter is added as a CaseParticipant in step 5 of CM-14-001The reporter's system must receive and store the case object before any Announce(CaseLedgerEntry) fan-out that lists them as a participant. Reversing this order causes "case not found" errors on the reporter side when the log entry arrives referencing a case the reporter has not yet seen. |
Depends on: CM-14-001 Depends on: CM-12-003 |
CM-14-005 |
MUST | When the reporter is added as a CaseParticipant during case initialization, they MUST be seeded as a signatory on any active embargoThe reporter already possesses the vulnerability information before the case is created. Requiring an explicit embargo acceptance before sharing case content would create a paradoxical deadlock: the reporter has the information that the embargo is meant to protect, yet cannot receive case updates until they accept the embargo. Seeding them as signatory reflects their implicit consent, expressed by submitting the report. |
Depends on: CM-14-001 |
CM-14-006 |
SHOULD | If the reporter included an embargo proposal with their Offer(Report), the case initialization SHOULD reconcile the reporter's proposed duration against the CASE_OWNER's published embargo policy (its actor default, EP-04-003) and initialize the embargo at the shorter of the two durations |
Depends on: CM-14-001 Depends on: CM-12-004 Depends on: EP-04-004 |
CM-14-007 |
MUST | Additional participants MUST NOT be invited and embargo modifications MUST NOT be initiated until the case initialization sequence defined in CM-14-001 (including any applicable conditional steps) is complete The initialization sequence establishes the minimum valid case state: a case object, an owner, an optional embargo, and a reporter. Inviting third parties or modifying the embargo before this baseline is in place creates race conditions and protocol violations. |
Depends on: CM-14-001 |
CM-14-008 |
MUST | Step 1 of the case initialization sequence MUST create the VulnerabilityCase with the receiving actor set as attributed_to.Creating the case object first establishes the anchor to which all subsequent initialization steps (participants, embargo, ledger) are attached. |
|
CM-14-009 |
MUST | Step 2 of the case initialization sequence MUST create the case-owner CaseParticipant with RM.RECEIVED and the CASE_OWNER role.The case-owner participant must exist before the default embargo can be initialized, so that the embargo is always associated with an existing participant. |
|
CM-14-010 |
MUST | Step 3 of the case initialization sequence MUST initialize the default embargo to EM.ACTIVE and seed the case owner as a signatory whenever the case is embargo-eligible, using the actor default where one is configured and the EP-04-005 protocol default otherwise.The embargo must be in place before any participant other than the case owner is added, so that all subsequent participants are subject to the embargo from the moment they join. |
Depends on: EP-04-005 |
CM-14-011 |
MUST | Step 4 of the case initialization sequence MUST queue a Create(Case) activity addressed to the reporter and flush the outbox.The reporter's system must receive and store the case object before any Announce(CaseLedgerEntry) fan-out that references it. |
|
CM-14-012 |
MUST | Step 5 of the case initialization sequence MUST add the reporter as a CaseParticipant with RM.ACCEPTED and the REPORTER role, and seed them as a signatory on any active embargo.The reporter is added after the Create(Case) notification is flushed (CM-14-011) so that the case object exists on the reporter's system before the fan-out of the ledger entry that records their participation. |
|
CM-14-013 |
MUST | Step 6 of the case initialization sequence MUST commit a case ledger entry, triggering Announce(CaseLedgerEntry) fan-out to all participants.The ledger entry commits the completed initialization as a single protocol-significant event and notifies all participants atomically. |
Actor Suggestion Routing (Recommend-Actor Flow)
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-16-001 |
MUST | A participant wishing to suggest that another actor be invited to the case MUST send Offer(object=Actor, target=Case) to the CASE_MANAGER's inbox, not directly to the Case Owner's inbox.All case-scoped protocol messages MUST route through the CASE_MANAGER so the event can be recorded in the canonical case ledger before any downstream action is taken (ADR-0021, PCR-08-001). |
Implements: PCR-08-001 |
CM-16-002 |
MUST | When the CASE_MANAGER receives Offer(object=Actor, target=Case), it MUST commit a CaseLedgerEntry for the received Offer and fan out the entry to every active participant (CM-10-004) BEFORE taking any downstream action.Ledger entries record events that have already occurred; pre-committing a decision before communicating it is not permitted (CLP-10-006 commit ordering). |
Implements: CLP-10-006 |
CM-16-004 |
MUST | The CASE_MANAGER MUST transform the received Offer(Actor) into Offer(object=CaseParticipant{attributed_to=actor, case_roles=roles}, target=Case) and send it to the Case Owner's inbox, with origin set to the ID of the original recommender's Offer activity.The Case Owner makes a membership decision about a CaseParticipant (actor + roles), not about a bare Actor identity. The origin field preserves the causal chain without a side-channel data store. |
|
CM-16-005 |
MAY | The recommender's Offer(Actor) MAY include a content or summary field explaining why the actor is being suggested. The CASE_MANAGER SHOULD forward this text as the content of the transformed Offer(CaseParticipant) it sends to the Case Owner. |
|
CM-16-006 |
MUST | After receiving Accept(Offer(CaseParticipant)) from the Case Owner, the CASE_MANAGER MUST (1) commit a ledger entry for the Accept, (2) fan out the ledger entry, (3) send an Accept of the recommender's Offer(Actor, Case) to the original recommender with in_reply_to set to the ID of the original recommender's Offer activity, and (4) send the stub Invite (Invite(Actor, VulnerabilityCaseStub), CM-11-013) to the suggested actor per CM-17. It MUST refuse the Accept, with a reported reason and before it commits or sends anything, when the suggested actor has already joined or is at RM.CLOSED (CM-11-015). |
Implements: CM-17-001 |
CM-16-007 |
MUST | After receiving Reject(Offer(CaseParticipant)) from the Case Owner, the CASE_MANAGER MUST (1) commit a ledger entry for the Reject, (2) fan out the ledger entry, and (3) send a Reject of the recommender's Offer(Actor, Case) to the original recommender with in_reply_to set to the ID of the original recommender's Offer activity. |
|
CM-16-008 |
MUST | When a second Offer(Actor X, Case) arrives while a pending decision for Actor X is still outstanding (no Accept or Reject yet received from the Case Owner), the CASE_MANAGER MUST record the duplicate recommendation in the ledger. |
|
CM-16-009 |
MUST | When a second Offer(Actor X, Case) arrives after an Invite has already been sent to Actor X (i.e., the invite is in-flight or the actor is already a participant), the CASE_MANAGER MUST record the recommendation in the ledger, fan out the entry, and auto-send an Accept of that recommender's Offer(Actor, Case) (with in_reply_to = original recommender's Offer ID) back to the second recommender. The CASE_MANAGER MUST NOT send a duplicate Invite to Actor X. |
|
CM-16-010 |
MUST | In every step of the actor suggestion flow, the CASE_MANAGER MUST commit the ledger entry for a protocol event AFTER the corresponding message has been sent or received, never before. Pre-announcing a decision in the ledger before communicating the decision creates a window where the ledger shows an effect without its cause. A sent message is sent once it is in the CASE_MANAGER's outbox (SYNC-09-002). |
|
CM-16-016 |
MUST | When a second Offer(Actor X, Case) arrives while a pending decision for Actor X is still outstanding, the CASE_MANAGER MUST record a Note for the Case Owner indicating that a reinforcing suggestion was received, including the Case Owner's identity, the second recommender's identity and any content from the Offer. The note is the CASE_MANAGER's own add_note_to_case entry, attached to the case, and reaches the Case Owner's inbox as that entry's Announce(CaseLedgerEntry) (SYNC-02-002); a direct Add(Note) to the Case Owner is refused there (HP-01-005).Notifying the Case Owner of reinforcing suggestions provides additional context for the pending decision without creating a duplicate offer workflow. Every active participant sees the note; a private message to the Case Owner is not implemented (#4885). |
|
CM-16-017 |
MUST NOT | When a second Offer(Actor X, Case) arrives while a pending decision for Actor X is still outstanding, the CASE_MANAGER MUST NOT send a second transformed Offer(CaseParticipant) to the Case Owner.Sending a duplicate transformed Offer would create a second pending decision for the same actor, leading to conflicting state if the Case Owner responds to both. |
|
CM-16-018 |
MUST | When the CASE_MANAGER processes Accept(Offer(CaseParticipant)) from the Case Owner, the roles assigned to the new participant MUST be read from the stored outgoing Offer(CaseParticipant) that the CASE_MANAGER constructed and sent — not from the embedded CaseParticipant object inside the received Accept.The received Accept is untrusted: the accepting actor may have modified or omitted roles, or sent only a bare ID reference. Reading roles from the received Accept would allow a malicious or minimally-conformant acceptor to substitute unauthorized roles. The stored Offer is the CASE_MANAGER's own record of the intended participant roles and is the authoritative source of truth. |
Refines: CM-16-006 |
CM-16-019 |
MUST | The CASE_MANAGER MUST act on Accept(Offer(CaseParticipant)) or Reject(Offer(CaseParticipant)) only when the sender is the case's Case Owner, and MUST take the actor to invite from the Offer(CaseParticipant) it recorded, never from the reply. A reply from any other sender MUST be refused, and the CASE_MANAGER MUST NOT invite anyone or notify the recommender.The Case Owner decides on a recommendation (CM-16-006), but the handler checks only that the recommendation was recorded, and reads the invitee from the inbound message; anyone who learns a recommendation id can accept it and choose who is invited. See #3733. |
Refines: HP-01-006 Refines: CM-16-006 |
Invitation Flow — Enrichment and Roles
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-17-001 |
MUST | The stub Invite sent by the CASE_MANAGER to a new actor MUST be Invite(object=Actor, target=VulnerabilityCaseStub), with the stub shaped as CM-11-013 requires. This requirement is extended by CM-17-002 and CM-17-003. |
|
CM-17-002 |
MUST | When an embargo is in force at the time the stub Invite is sent (EM ACTIVE or REVISE: the case's embargo register holds an ACTIVE entry), the invitation MUST let the invitee learn the active embargo's terms before it accepts: the case stub MUST carry the case's EM state (caseStatus.emState) and the active embargo (activeEmbargo), either inline or by an ID the invitee can ask the inviter to resolve. The stub MUST NOT carry an open revision proposal; the invitee learns of it from caseStatus.emState and receives it after joining (EP-09-011).The invite-accept cycle is the only consent signal for late-joining participants when an active embargo already exists. Accepting without seeing embargo terms is not meaningfully informed consent (issue #1289, MV-10-006). An invited actor that has not joined holds nothing but the invitation, so the terms come from it: inline in the stub, or fetched from the inviter by the ID the stub names. ActivityStreams allows either form; which one this prototype sends is CM-17-011. In EM REVISE the prior embargo stays in force until the owner decides the revision (EP-05-002), and accepting the stub binds the invitee to it (CM-11-001, CM-11-006), so the terms are owed in REVISE as in ACTIVE (#4697). The revision itself stays out of the stub: it is not yet binding, CM-17-010 keeps other case content out, and EP-09-011 already relays each open proposal the joiner has not yet been asked about once it holds the case. |
Implements: MV-10-006 |
CM-17-004 |
MUST | When the CASE_MANAGER receives Accept(Invite(Actor, VulnerabilityCaseStub)) from an invitee, it MUST, in this order: (1) commit a ledger entry for the Accept and fan it out to every active participant (CM-10-004); (2) record the Accept's effect on the participant (CM-11-001); (3) when the participant is now active, send it Announce(VulnerabilityCase) and replay all prior CaseLedgerEntry records to it in log-index order (CM-11-008); and (4) send it the full-case Invite (CM-11-010). |
Implements: CM-11-001 Implements: CM-11-008 Implements: CM-11-010 Implements: PCR-08-010 |
CM-17-005 |
MUST NOT | Presence in the case participant list MUST NOT be treated as acceptance of the invitation or as entitlement to case content. An invitee is in the list from the stub Invite onward (CM-11-006); only an active participant (CM-10-004) is entitled to case content. The CASE_MANAGER tracks every invitee from the moment it is invited, including one that declines, so the participant list holds inert participants. Any consumer that reads membership as "accepted" would hand case content to an actor that has not consented. |
|
CM-17-009 |
MUST | Any canonical CaseLedgerEntry the CASE_MANAGER commits while processing a stub Invite's Accept whose fan-out reaches the joining participant — every entry committed once that participant is active (CM-10-004) — MUST be committed after CM-17-004 step (3), the Announce(VulnerabilityCase) and the start of the ledger replay, so the participant's first ledger entries arrive after its case seed and in log-index order.CM-17-004 orders the announce and the replay with no fan-out to the joiner in between, but an entry committed during the same admission whose fan-out includes the newly active participant would otherwise reach it ahead of its case. The late joiner received a ledger entry before its case (SYNC-15 pre-genesis Reject, then a from-genesis replay that interleaved with the backfill): in the rcvcv scenario vendor2 received entry 19 one second before the case, and vendor-deployer received entries 26 and 0 before it (#2898). Committed last, the entry reaches the invitee as the next entry in chain order. |
Refines: CM-17-004 Depends on: SYNC-15-004 |
CM-17-010 |
MUST | A case stub that a sender emits MUST carry exactly its id, its type, the caseId naming the case it stands for (CM-11-013) and a non-empty summary (MV-10-001), plus, when an embargo is in force, the embargo terms CM-17-002 requires (activeEmbargo and caseStatus.emState). The sender chooses the summary: it MAY be the case name, or a deliberately less revealing string, and the sender MUST NOT assume the case name is safe to share with an invitee not yet bound by the embargo. The sender MUST NOT put any other case content in the stub, and MUST NOT name the case's coordinator or Case Owner in it: the Invite that carries it already identifies the inviter in its actor and attributedTo. This constrains what is sent; the JSON-LD @context is not content.An invitee is entitled to no case content yet (CM-10-004, CM-17-005). Informed consent needs the case's identity, a human-readable account of what it is asked to join, and the embargo terms (CM-17-002); the Invite envelope says who is asking. The case name can itself be an opsec concern, so the summary is chosen for the invitee, not copied from the case. #4165 implemented this. Whether the stub should name the coordinator and the Case Owner explicitly, for Invites relayed by a third party, is the open Idea #4184; until it is decided the rule above stands. |
Refines: CM-11-013 Refines: MV-10-001 Depends on: CM-17-002 |
Participant Embargo Consent (per-embargo rows)
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-18-001 |
MUST | A participant's embargo consent MUST be tracked per embargo, as one row for every entry in the case's embargo register, each row holding one of five states: UNINVITED (not asked; the start state), INVITED (asked; no answer yet), AGREED, DECLINED (explicitly refused, or withdrawn from) and TIMED_OUT (invited, and the RSVP deadline passed with no answer). Every row MUST be written: proposing an embargo writes an UNINVITED row for it on every current participant, and creating a participant record writes one for every entry already in the register. A missing row is a defect and MUST NOT be read as any consent state (CM-18-017). A participant is a signatory when its row for the register's ACTIVE entry is AGREED. A participant has lapsed when an entry is ACTIVE, its row for that entry is neither AGREED nor DECLINED, and, following replaces back from the ACTIVE entry, the first entry whose row is AGREED or DECLINED has an AGREED row. Both are read from the rows and the register, never stored, and with no ACTIVE entry nobody is a signatory and nobody has lapsed.The scalar ParticipantEmbargoConsent value this replaces answered one question — is this participant bound by the active embargo? — while accepted_embargo_ids recorded which terms it had accepted. The two disagreed (Concern #3884) and were kept in step by reconciliation rules. One row per (participant, embargo) states each answer once; "bound by the active embargo" is a lookup of the active embargo's row, so there is nothing to reconcile and no first-proposal versus revision asymmetry (ADR-0122, which supersedes the seven-state machine of ADR-0048, ADR-0093 and ADR-0118). Writing every row names the start state, so no reader infers "not asked" from an absent record, and the register lets anything check that each participant has a row for each embargo. The "pocket veto" survives as the TIMED_OUT row state, kept distinct from DECLINED so every reader can tell silence from a refusal (ADR-0118). A terminated embargo is the register's fact, not a per-participant one. Lapsed follows the replaces chain, so only embargoes that were once in force can bind, and a withdrawal from a later embargo ends the binding of an earlier one. |
|
CM-18-002 |
MUST | The INVITED → TIMED_OUT transition MAY be driven by a timer expiry (pocket veto) rather than requiring an explicit participant action. The timer period SHOULD be a configurable policy window (default: 7 days). The pocket veto materialises when the timer fires without a prior answer; the system applies the TIME_OUT trigger to the row of the invitation whose deadline passed and records TIMED_OUT, never DECLINED, which only an explicit refusal reaches. A lapse is NOT timer-based and is not a stored transition: it is what the rows say once the case owner activates a revised embargo that ends later than the one the participant agreed to and the participant has not agreed to the revised terms (CM-18-001, EP-05-001). A lapsed participant is re-invited (an INVITED row for the embargo in force), and the deadline attaches to that invitation.Per ADR-0093, ADR-0122 and notes/participant-embargo-consent.md: a participant has lapsed when the active embargo was replaced by longer terms it has not accepted. While a revision is merely proposed the prior embargo is still in force and every signatory to it is still bound by it, so nothing about their consent has changed. Per ADR-0118, a participant that did not answer has made no decision, so a timeout records TIMED_OUT, distinct from an explicit DECLINED; both are re-invitable. A pocket veto exists so that an unanswered invitation cannot stall coordination; nothing stalls on a lapsed participant, who is already excluded from embargoed content by the CM-10-004 gate, and there is no invitation from which to derive a deadline. An RSVP deadline belongs to one invitation and is stored on that invitation's INVITED row (CM-28-013), so its passing times out only that row, never the participant's other invitations. |
|
CM-18-004 |
MUST NOT | A participant's consent row MUST NOT move from AGREED to INVITED directly. The only paths back to INVITED from a resolved row are DECLINED → INVITED (re-invite after explicit decline) and TIMED_OUT → INVITED (re-invite after an unanswered invitation timed out); a participant that has lapsed is re-invited on its row for the new embargo (an INVITED row for it), which leaves its AGREED row for the old embargo alone.Consent already given cannot be withdrawn by re-inviting the participant. A signatory to the active embargo is bound by it; only replacing that embargo with longer terms (which lapses the participant by derivation — ADR-0093, ADR-0122) or the participant's own withdrawal ( DECLINE) ends that position. |
Refines: CM-18-003 |
Coordinator-as-Participant Role Constraints
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-20-001 |
MUST | A Coordinator invited to a case as a regular participant MUST hold COORDINATOR in their case_roles list.CASE_OWNER confers case administration authority (CM-02-001, CM-02-004, CM-02-005). CASE_MANAGER is a distinct role: it grants single-writer authority over the canonical ledger (CLP-09) and is held by the case-creating actor enacting CVDRole.CASE_MANAGER, which SHOULD also carry CVDRole.COORDINATOR (CBT-01-003). It is not conferred merely for being invited as a facilitation participant, so a Coordinator invited as a regular participant holds neither CASE_OWNER nor CASE_MANAGER. |
Refines: CBT-01-003 |
CM-20-002 |
MUST | When a Coordinator is suggested via the Offer(Actor, Case) path (CM-16) and the Case Owner approves the invitation, the roles assigned to the Coordinator participant MUST include COORDINATOR and MUST NOT include CASE_MANAGER or CASE_OWNER.Invitation and case ownership transfer are distinct protocol operations. An Offer(Actor, Case) invite adds a participant; it does not transfer CASE_OWNER authority (CM-02-001) or the CASE_MANAGER single-writer authority (CM-20-001). Role assignments at invite time must reflect the invited actor's actual function in the case, not their organizational identity. |
Refines: CM-20-001 Refines: CM-16-004 |
CM-20-003 |
MUST | When a Coordinator participant (holding COORDINATOR without CASE_MANAGER) suggests that another actor be added to the case via Offer(Actor, Case), the suggestion MUST route through the CASE_MANAGER inbox per ADR-0026 (CM-16-001). The Case Owner — not the Coordinator — MUST remain the sole decision-maker for approving or rejecting the invitation (CM-02-001).A Coordinator-as-participant should be able to exercise their facilitation role (suggesting additional vendors) without requiring ownership transfer. The CASE_MANAGER-routed suggest flow (ADR-0026) already provides this: the Coordinator sends Offer(Actor, Case) to the CASE_MANAGER, which records and forwards it to the Case Owner for approval. |
Refines: CM-16-001 Refines: CM-20-001 |
CM-20-004 |
MUST NOT | A Coordinator invited to a case as a regular participant MUST NOT hold the CASE_MANAGER role in its case roles.CVDRole.CASE_MANAGER grants single-writer authority over the canonical ledger and is held by the actor that created the case and enacts that role (CBT-01-003, ADR-0088). It MUST NOT be conferred on an organizational or human actor invited to the case as a regular facilitation participant. |
Refines: CM-20-001 |
CM-20-005 |
MUST NOT | The CASE_MANAGER role MUST NOT be assigned to any organizational or human participant joining a case as a facilitation actor rather than as the actor enacting the CASE_MANAGER role (CBT-01-003).CVDRole.CASE_MANAGER grants single-writer authority over the canonical ledger (CLP-09). Conferring it on a participant invited for facilitation would let them author ledger entries that only the actor enacting CVDRole.CASE_MANAGER is authorized to write. |
Refines: CM-20-004 |
Ownership Transfer Protocol Effects
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-21-001 |
MUST | A VulnerabilityCase MUST have at most one participant holding CASE_OWNER at any point in time. If a participant already holds CASE_OWNER when a new owner is designated, the system MUST remove CASE_OWNER from the previous owner's CaseParticipant record before or atomically with granting it to the new owner.CASE_OWNER confers exclusive case administration authority (CM-02-004, CM-02-005). Two simultaneous holders would create conflicting authority: both could terminate embargoes, authorise invitations, and mutate case state independently. The invariant must be enforced at the data-layer level, not just assumed by convention. |
Refines: CM-02-004 Refines: CM-02-005 |
CM-21-002 |
MUST | When an Accept(Offer(VulnerabilityCase)) ownership-transfer activity is processed, the system MUST add CASE_OWNER to the accepting actor's case_roles and MUST update the case's attributed_to to the accepting actor's ID.A role-gated protocol node (for example CheckIsCaseOwnerNode) determines case-owner authority by reading CaseParticipant.case_roles, not by comparing actor IDs against attributed_to. Updating only attributed_to without also updating the participant record leaves the new owner unable to trigger embargo teardown, case closure, and other CASE_OWNER-gated operations. Both fields MUST be kept in sync. Root cause of the RCCV-handoff CI failure (2026-07-23): AcceptCaseOwnershipTransferNode updated attributed_to but did not add CVDRole.CASE_OWNER to C2's participant record, so _sender_is_case_owner() returned False and embargo teardown was skipped. |
Refines: CM-21-001 Refines: TRIG-11-002 |
CM-21-003 |
MUST | When an Accept(Offer(VulnerabilityCase)) ownership-transfer activity is processed, the system MUST remove CASE_OWNER from the previous owner's case_roles.Transferring ownership does not evict the previous owner from the case; they continue to participate but no longer hold administrative authority. Stripping only CASE_OWNER (and not other roles) preserves their standing as a Coordinator, Vendor, or other role while satisfying the at-most-one invariant (CM-21-001). |
Refines: CM-21-001 Refines: TRIG-11-002 |
CM-21-005 |
MUST | A Case Owner that wants to transfer ownership MUST propose it to the CASE_MANAGER by sending Offer(Offer(VulnerabilityCase)), with the case as the proposal's context, addressed only to the CASE_MANAGER (PCR-08-001), not to the transferee. The inner Offer names the case as its object, the transferee as its target and, optionally, the deadline to answer as its end_time (CM-21-016). On admitting the proposal the CASE_MANAGER MUST check that the inner Offer's object is the proposal's case and that its target is an actor it can address (present, neither itself nor the proposer), MUST ignore any id, actor or to inside it, and MUST make the one ownership-transfer Offer(VulnerabilityCase) itself: under an id it derives from the proposal, with actor itself and attributed_to the owner (CM-24-001, CM-24-002), addressed to the transferee. It MUST ledger that Offer once it is in its outbox (SYNC-09-002); the ledgered, answered, closed and expired Offer is that one. A proposal whose inner Offer fails the check MUST be refused with no ledger commit and no outbound Offer.Routing the transfer through the CASE_MANAGER lets it ledger the offer in flight and broadcast it to every active participant (CM-10-004), so no participant is unaware a transfer is being negotiated. Making the Offer itself rather than forwarding the owner's gives a transfer one Offer, under an id no other party chose, told apart from the proposal by its shape (ADR-0148, DL-06-006). The CASE_MANAGER cannot be the transferee: its Offer would have itself as its only recipient, which BT-17-008 forbids, the same ground on which CM-33-003 refuses a role offer to it. |
Refines: CM-21-001 |
CM-21-006 |
MUST | An actor emitting Accept(Offer(VulnerabilityCase)) (ownership-transfer variant) MUST address the activity only to the CASE_MANAGER (PCR-08-001), not directly to the offerer.Routing the Accept through the CASE_MANAGER is required for the AcceptCaseOwnershipTransferReceivedUseCase to run on the authoritative CASE_MANAGER and trigger the post-transfer CaseLedgerEntry cascade (CM-21-007). Addressing the Accept to the offerer's inbox means the use case only runs there; the CASE_MANAGER never learns of the completed transfer and cannot broadcast it. |
Refines: CM-21-001 Refines: CM-21-005 |
CM-21-007 |
MUST | After the CASE_MANAGER successfully applies the ownership-role changes of an admitted ownership-transfer Accept (CM-21-002, CM-21-003, CM-21-004), it MUST commit a CaseLedgerEntry recording the completed transfer and MUST broadcast Announce(CaseLedgerEntry) to all case participants.The CaseLedgerEntry broadcast is the only mechanism by which participants not involved in the transfer (e.g., a Finder or second Vendor) learn that the case owner has changed. Without it, those participants have no reliable way to determine who is currently responsible for the case, leading to messages being directed to the wrong actor and coordination breakdown. This requirement closes the surface-level symptom described in CONCERN-1755. |
Refines: CM-21-002 Refines: CM-21-006 |
CM-21-008 |
MUST | When an ownership transfer is processed, the previous owner MUST retain any other roles they held (e.g., COORDINATOR, VENDOR) in their case_roles.Stripping only CASE_OWNER (and not other roles) preserves the previous owner's standing as a Coordinator, Vendor, or other role while satisfying the at-most-one CASE_OWNER invariant (CM-21-001). |
Refines: CM-21-003 |
CM-21-009 |
MUST | When an ownership transfer is processed, the previous owner MUST remain a participant in the case. Ownership transfer is not case departure. The previous owner has institutional knowledge of the case and continues to participate; only their administrative authority is transferred. |
Refines: CM-21-003 |
CM-21-010 |
MUST | An actor emitting Reject(Offer(VulnerabilityCase)) (ownership-transfer variant) MUST address the activity only to the CASE_MANAGER (PCR-08-001), not directly to the offerer.Rejection routing must be symmetric with the Accept path (CM-21-006): the transfer was proposed to the CASE_MANAGER (CM-21-005), so its rejection must be too. If the transferee's Reject goes only to the offerer, the CASE_MANAGER — the authoritative record of ownership — never learns the transfer was declined and its view of who owns the case can silently diverge from reality. |
Refines: CM-21-001 Refines: CM-21-006 |
CM-21-011 |
MUST | The CASE_MANAGER MUST act on Accept(Offer(VulnerabilityCase)) only when the sender is the transferee named in a pending ownership-transfer Offer it recorded for that case and that has not been answered; a participant replica MUST apply the ownership change only from the CASE_MANAGER (PCR-03-001). Any other sender MUST be refused, with the case's attributed_to and every participant's CASE_OWNER role unchanged.The handler takes the sender to be the new owner and never looks up the Offer, so any actor that knows a case id can claim ownership on every receiver holding the case. See #3733. |
Refines: HP-01-006 Refines: CM-21-002 |
CM-21-012 |
MUST | When the CASE_MANAGER admits a Reject(Offer(VulnerabilityCase)) (ownership-transfer variant) under CM-21-013, it MUST commit a CaseLedgerEntry recording the Reject and MUST broadcast Announce(CaseLedgerEntry) to all case participants, as it does for the Accept (CM-21-007). The entry closes the Offer (CM-21-014): from it on, that Offer is no longer pending on any replica. A replica applies no further state change from the entry; the case's attributed_to and every participant's CASE_OWNER role are unchanged.The Offer was ledgered and broadcast (CM-21-005), so every replica holds a transfer in flight. A Reject that leaves no entry never closes it, and the offerer, who is no longer a recipient of the Reject (CM-21-010), never learns of the decline. A participant's Reject is a case event like its Accept; it is ledgered for the same reason (CLP-02-012). See #3748. |
Refines: CM-21-010 Refines: CLP-02-012 |
CM-21-013 |
MUST | The CASE_MANAGER MUST act on Reject(Offer(VulnerabilityCase)) only when the sender is the transferee named in an ownership-transfer Offer it recorded for that case and that is still open, as CM-21-014 defines and reads from the ledger. Any other sender MUST be refused, with no ledger commit, no outbound activity, and the Offer still pending.The same standing CM-21-011 requires for the Accept. Without it any actor that knows an Offer id could close a transfer it is not party to, and the closed Offer could no longer be accepted by the transferee it names. |
Refines: HP-01-006 Refines: CM-21-011 |
CM-21-014 |
MUST | The CASE_MANAGER MUST refuse an ownership-transfer proposal (CM-21-005) for a case while an ownership-transfer Offer for that case is open, making no Offer from it. Whether an Offer is open MUST be read from the case ledger (CLP-11-001): it is open until the ledger holds an Accept (CM-21-007), Reject (CM-21-012) or withdrawal (CM-21-015) entry for it, or until the end_time the ledgered Offer carries has passed (CM-21-017). Once it is closed, the CASE_MANAGER MUST admit a new proposal for the case, to any transferee, including the one that declined, with no waiting period and nothing to clear first. A redelivered proposal whose Offer the ledger already records is not a new proposal: it changes nothing (HP-01-003).Two pending Offers would let two transferees accept the same case, and CM-21-001 allows one owner. Reading "open" from the ledger rather than from local offer records keeps the CASE_MANAGER and every replica in agreement (CLP-07-014), and no memory of who declined is needed because a decline does not bar a later offer. Withdrawal and expiry exist so that a transferee who never answers cannot block every later transfer of the case. |
Refines: CM-21-001 Refines: CM-21-005 Refines: CLP-11-001 |
CM-21-015 |
MUST | The CASE_OWNER that proposed an ownership transfer MAY withdraw the Offer the CASE_MANAGER made from its proposal while that Offer is open (CM-21-014), by sending Undo of its own proposal, Undo(Offer(Offer(VulnerabilityCase))), addressed to the CASE_MANAGER. The CASE_MANAGER MUST resolve the Offer it made from the proposal by the id it derived for it (CM-21-005). When it admits the Undo, it MUST commit a CaseLedgerEntry recording the withdrawal and MUST broadcast Announce(CaseLedgerEntry) to all case participants; the entry closes the Offer. The CASE_MANAGER MUST refuse an Undo from any other sender, or for an Offer that is already closed, with no ledger commit and no outbound activity.Without a withdrawal, one open Offer per case (CM-21-014) lets a transferee who never answers block every later transfer of the case. Ledgering the withdrawal tells the transferee and every replica that the Offer can no longer be accepted, the same way the Reject entry does (CM-21-012). The owner names its own proposal, the one activity of the transfer it holds, so it needs nothing it learned from the ledger to withdraw (ADR-0148). |
Refines: CM-21-014 Refines: CLP-02-012 |
CM-21-016 |
MUST | An ownership-transfer Offer(VulnerabilityCase) MAY carry an activity-level end_time; the CASE_MANAGER copies it from the inner Offer of the owner's proposal (CM-21-005). When present, it MUST be read as the deadline to answer the Offer: the instant after which the Offer can no longer be accepted or rejected. Unlike an embargo Invite's deadline (CM-28-001), it is binding: the CASE_MANAGER MUST refuse an Accept or Reject of the Offer that it processes after end_time, with no role change and no ledger entry for the answer. An Offer without end_time stays open until it is answered or withdrawn. end_time MUST be normalised to UTC at the wire edge (CS-13-001, ADR-0032).An embargo deadline is a strong suggestion, so participants may still answer late. A transfer deadline has to hold, so that the offerer, the transferee and every other participant know who decides for the case once it passes. end_time keeps the meaning CM-28-001 gives it on an Invite — the deadline to answer — so it adds no new meaning to the field. |
Refines: CM-21-011 Refines: CM-21-013 Refines: CS-13-001 |
CM-21-017 |
MUST | Only the CASE_MANAGER evaluates ownership-transfer Offer expiry, and it MUST do so lazily, from the ledgered Offer's end_time and its own clock, whenever it processes an Accept, Reject or Undo of the Offer or a new proposal for the case; no scheduler is required. An Offer whose end_time has passed is closed (CM-21-014): a late Accept or Reject is refused (CM-21-016), an Undo is refused because the Offer is closed (CM-21-015), and a new proposal is admitted. The expiry is an observation, not an act: the CASE_MANAGER MUST NOT commit an entry or send an activity recording it, since anything that holds the case and its ledger can read the deadline from the Offer and no receiver changes on it (ADR-0152). The check runs behind the CASE_MANAGER role gate (BT-17-001); a replica never evaluates one.Expiry evaluated in one place keeps the decision about a late answer with the actor that judges answers. Lazy evaluation needs no timer, and the next act on the case is the first moment the expiry matters. An expiry entry would record a fact every holder of the ledger can already derive, and would change nothing at any replica, so it is noise on the wire and in the ledger (ADR-0152). Unlike an embargo Invite's expiry, which moves a consent row (CM-28-009), an Offer's expiry changes no case-file record. |
Refines: CM-21-014 Refines: CM-21-016 |
CM-21-019 |
MUST | The CASE_MANAGER MUST admit an ownership-transfer proposal (CM-21-005) only from the actor holding CASE_OWNER on the case when it processes the proposal, and MUST admit an ownership-transfer Offer(VulnerabilityCase) from no sender: it makes that Offer itself. A participant replica MUST admit no proposal, and MUST take the Offer only from the case's CASE_MANAGER (PCR-03-001); a replica that holds no such case, or whose case names no CASE_MANAGER, has no CASE_MANAGER to recognise and MUST refuse it. Any other sender MUST be refused before anything is written: no ledger commit, no Offer made, and the case's attributed_to and every participant's CASE_OWNER role unchanged. A redelivery of a proposal the CASE_MANAGER already made its Offer from, on the same sender's behalf, keeps the standing it was admitted with (HP-01-003).CM-21-011 admits an Accept from the transferee of a recorded Offer, so whoever can get an Offer recorded chooses the next owner. Without a sender rule any actor that knows a case id could propose the case to itself and accept it, even from off the roster. The Case Owner is the one actor with the authority to hand the case on (CM-02-004). The redelivery clause keeps a late copy of an answered proposal a duplicate rather than a refusal once its sender has handed ownership on. See #4540. |
Refines: HP-01-006 Refines: CM-21-005 Refines: CM-21-011 |
CASE_MANAGER-Authoritative Case Creation
Requirements on the CASE_MANAGER's obligations when accepting a CaseProposal, per ADR-0041. These replace the receiver-authored case-creation described in ADR-0015 and govern how the CASE_MANAGER initializes CaseStatus, participants, and embargo in its DataLayer before emitting Create(VulnerabilityCase).
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-22-001 |
MUST | The CASE_MANAGER MUST create the VulnerabilityCase object in its own store when accepting a Create(CaseProposal), with attributed_to equal to the proposing actor's ID (the CASE_OWNER, CM-02-008), and MUST be the actor of the Create(VulnerabilityCase) it emits. It MUST address that activity to the report receiver (the CASE_OWNER) only; the report receiver introduces the CASE_MANAGER to the reporter (CBT-01-001, ADR-0132).ADR-0041 supersedes ADR-0015. The CASE_MANAGER is the case's authoritative creator, which AS2 records as the actor of Create(VulnerabilityCase) (ADR-0023); attributed_to records the case owner, which ownership checks read and ownership transfer rewrites (MSM-07-008, CM-21-002). |
Implements: CP-09-001 Supersedes: CM-12-001 — In multi-actor deployments, where the CASE_MANAGER creates the case (ADR-0041) Supersedes: CM-14-001 — In multi-actor deployments, where the CASE_MANAGER creates the case (ADR-0041) |
CM-22-002 |
MUST | After creating the VulnerabilityCase, the CASE_MANAGER MUST create CaseParticipant records for at minimum: (a) the report receiver with CASE_OWNER and RM.RECEIVED, and (b) the reporter with RM.ACCEPTED. These records MUST be written before Create(VulnerabilityCase) is emitted to the report receiver.The Create(VulnerabilityCase) payload embeds participants inline so the receiver's store_embedded_participants can seed its local replica. Absent participant records at emission time produce an empty replica and break role-guard checks (CP-09-002, CP-09-004). |
Refines: CM-22-001 Implements: CP-09-002 |
CM-22-003 |
MUST | The CASE_MANAGER MUST commit all canonical ledger entries for case initialization (create_case, create_embargo_event, add_report_to_case, participant-record and participant-status entries, add_case_status_to_case) natively, in its own ledger. These entries MUST NOT be authored by the report receiver and back-filled into the CASE_MANAGER's ledger by any mechanism._validate_canonical_entry (in sync/nodes/canonical_entry.py) rejects entries where the actor_id does not match _CASE_AUTHORED_SIGNATURES. Receiver-authored entries fail this check (the root cause of Issue #1767). Native CASE_MANAGER authorship is the only compliant path. |
Refines: CM-22-001 |
CM-22-004 |
MUST | The CASE_MANAGER MUST initialize the default embargo for the new case before emitting Create(VulnerabilityCase) to the report receiver (the CASE_OWNER).Embargo state is protocol-significant. Deferring initialization to the report receiver or to a later message would produce a window in which embargo-gated operations are undefined (ADR-0041, CP-09-003). The CASE_MANAGER is the only actor that can initialize it authoritatively at case creation; the actor default it applies is the CASE_OWNER's policy, carried on the proposal (CP-01-009, CP-01-010). |
Refines: CM-22-001 Implements: CP-09-003 |
RM Case Closure via Leave(VulnerabilityCase)
Requirements for the canonical RM case closure path: Leave(VulnerabilityCase) is the sole mechanism for advancing a participant's RM state to CLOSED. Add(ParticipantStatus, rmState=CLOSED) is not a valid closure trigger. See ADR-0050 (Leave is canonical for RM closure) and ADR-0051 (CASE_MANAGER RM lifecycle). Decided: the maintainer decided the closure semantics in CM-23-002, CM-23-013 and CM-23-014 (what the owner's close does to pending Invites and to later Leaves) on 2026-10-12. ADR-0085 sets the owner-close write boundary. ADR-0131 sets the CM-23-013 reply to late traffic: by default it is dropped, and only a late Leave may get an as:Reject (#4065). The CM-23-014 settle step is #4066 (PR #4489).
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-23-001 |
MUST | RM case closure MUST be triggered by Leave(VulnerabilityCase), and the CASE_MANAGER's ledger commits are the authority for it. A participant-sent Add(ParticipantStatus, rmState=CLOSED) MUST NOT be used as a closure trigger. The CASE_MANAGER MUST commit a close_case entry recording the received Leave, and then, in the order it writes them, one add_participant_status_to_participant entry for each RM transition it writes for the leaving participant (CM-23-012; V → D → C is two entries). These status entries are the Leave's recorded consequence, written by the CASE_MANAGER as its own RM writes for the leaving participant (CLP-07-001, CLP-07-002, CLP-07-003). Each replica still receiving case content (CM-23-004) advances the leaving participant's RM state to RM.CLOSED only by applying those status entries, in ledger order, from the Announce(CaseLedgerEntry) fan-out. A replica MUST NOT work out a closure path from its own stored state, and applying the close_case entry MUST NOT change any participant's RM state. A status entry that cannot be committed leaves its RM transition unwritten: the CASE_MANAGER MUST take back each transition whose entry did not commit and MUST NOT report the Leave as applied, for the leaving participant and for its own closure alike (CM-23-005, CLP-07-013). The Leave round-trip makes the CASE_MANAGER the authority for closure and prevents ghost-departures from lost messages: a participant cannot silently reach RM.CLOSED on peers without the CASE_MANAGER having committed and broadcast a ledger entry. The ledger records each move as the CASE_MANAGER makes it, so a replica applies moves and cannot invent a step no entry backs (#4091). See ADR-0050. |
|
CM-23-002 |
MUST | When the CASE_MANAGER receives Leave(VulnerabilityCase) from the Case Owner, it MUST execute the following sequence: (0) commit the close_case entry recording the Leave (CLP-10-006), then | |
| expire every embargo Invite still awaiting a reply (CM-23-014), | |||
| (1) advance the Case Owner participant to RM.CLOSED, committing one status | |||
| entry per RM transition it writes, in order (CM-23-001), | |||
| (2) advance the CASE_MANAGER's own CaseParticipant to RM.CLOSED, committing | |||
| one status entry per RM transition it writes, in order (CM-23-005), | |||
| fanning each entry out to every active participant whose closure the ledger does not already record (CM-10-004, CM-23-004). The CASE_MANAGER's own status entry reaching RM.CLOSED, the last entry of step (2), or of step (1) when the Case Owner is the CASE_MANAGER itself, is the owner-close write boundary (ADR-0085): once the ledger holds it the case is fully closed, and no later entry records an external participant's act (CM-23-013). The CASE_MANAGER MUST NOT commit any separate entry marking the closure: it records nothing a replica does not already hold (ADR-0152). Every check of the boundary MUST use one shared predicate keyed on that entry. The boundary entry is NOT a log-complete marker: consumers MUST NOT treat it as the highest logIndex of the case or as proof that a replica holds the complete ledger. The Case Owner's departure is the authoritative termination event for a case. ADR-0085's boundary refuses external participants' appends, not the CASE_MANAGER's own housekeeping, so a consumer that stopped reading at the boundary could miss an entry the CASE_MANAGER writes later. The guarantee is the boundary, not chain finality. The CASE_MANAGER's own RM.CLOSED is the last thing the closure changes, so its entry marks the boundary without a marker entry that every holder of the ledger could derive (ADR-0152). See ADR-0050, ADR-0051, ADR-0085. |
Refines: CM-23-001 | ||
CM-23-003 |
MUST | When the CASE_MANAGER receives Leave(VulnerabilityCase) from a non-owner participant, it MUST advance only that participant's RM state to RM.CLOSED and commit a CaseLedgerEntry recording the departure (close_case), then one status entry per RM transition it writes for that participant, in order (CM-23-001). The close_case entry is committed first, before any of those writes (CLP-10-006). The case MUST remain open for the remaining participants. This applies only while the case is open; a non-owner Leave that arrives after owner close is governed by CM-23-013. Non-owner departure is a participant-scoped event, not a case termination event. Treating a non-owner Leave identically to an owner Leave would prematurely close the case and violate the Case Owner's authority over the case lifecycle. See ADR-0050. |
Refines: CM-23-001 |
CM-23-004 |
MUST | The CASE_MANAGER MUST NOT send a participant whose closure an earlier ledger entry already records (its status entry with rmState=CLOSED) any case content: no CaseLedgerEntry announcements, no Announce(VulnerabilityCase), no status broadcasts and no embargo announcements. The entries that record a participant's own closure, the close_case entry and the status entries that follow it (CM-23-001), are not withheld from that participant, so the leaving participant's replica receives every step, the one that reaches RM.CLOSED included. Those entries are identified by their subject being the recipient, not by the recipient's status, which is already RM.CLOSED when the last of them is fanned out. They never widen entitlement (CM-10-004): a participant that is inert or removed when it leaves is not sent them. The only other message a closed participant receives is the direct embargo notice to a signatory that CM-31-009 requires, which carries no other case content. A participant that has left stops at the entries that record its departure: it does not receive later entries, the CASE_MANAGER's own closure included. A closing participant waits for those entries and MUST NOT write its own RM.CLOSED to its replica in place of them. The CASE_MANAGER's own store, not any participant's replica, is where every participant's closure is verified. RM.CLOSED is the participant's own declaration that it has stopped paying attention to the case, made while the case may still be open, and its case replication process is assumed to have exited, so nothing is there to receive a later entry (the same assumption governs the CASE_MANAGER once the case is fully closed, CM-23-013). It receives no further content, so it does not go on seeing embargoed material after it has left, and continuing to send it entries wastes bandwidth and may trigger erroneous processing on stale replicas. The entries that record its own closure come before it exits, so it still receives them (a replica is advanced only by the ledger, CM-23-001). A closed replica therefore does not converge on later closures; that is intended. Resolves CONCERN-4100. |
Refines: CM-23-001 Refines: CM-10-004 Refines: CM-31-009 |
CM-23-005 |
MUST | The CASE_MANAGER MUST track its own RM lifecycle via a CaseParticipant record with ParticipantStatus entries that advance through RM.RECEIVED, RM.VALID, and RM.ACCEPTED during case initialization, and RM.CLOSED on case termination (owner Leave). Each transition MUST be recorded as a CaseLedgerEntry. Uniform RM lifecycle tracking across all participants (including the coordinator) enables case-level RM state to be read consistently from the ledger without special-casing the CASE_MANAGER. See ADR-0051. |
Refines: CM-23-002 |
CM-23-006 |
MUST | For the CASE_MANAGER's CaseParticipant, RM.RECEIVED MUST denote that a CaseProposal has been received and is being evaluated.The RM state machine models a lifecycle that applies to any actor processing a case, but its transition labels map to case-level events for the coordinator rather than to report-validation events as for other participants. See ADR-0051. |
Refines: CM-23-005 |
CM-23-007 |
MUST | During case initialization (after accepting a CaseProposal), the Case Actor MUST emit CaseLedgerEntry records for each of its own RM transitions: (a) CASE_MANAGER→RM.RECEIVED when the CaseProposal arrives, (b) CASE_MANAGER→RM.VALID when the proposal is validated and case creation | |
| begins, | |||
| (c) CASE_MANAGER→RM.ACCEPTED when the VulnerabilityCase is successfully | |||
| created and initialized. Ledger entries for the CASE_MANAGER's own RM transitions create a traceable, timestamped protocol-significant history of case initialization, consistent with CM-02-009 (CASE_MANAGER applies its own timestamp to every state-changing event). See ADR-0051. |
Refines: CM-23-005 Refines: CM-02-009 |
||
CM-23-008 |
MUST | For the CASE_MANAGER's CaseParticipant, RM.VALID MUST denote that the CaseProposal is correctly formatted and actionable by this CASE_MANAGER, and that case creation has begun.RM.VALID is the CASE_MANAGER's signal that the proposal passed structural validation and that initialization is in progress; it distinguishes a validated-but-incomplete case from one still under evaluation. See ADR-0051. |
Refines: CM-23-006 |
CM-23-009 |
MUST | For the CASE_MANAGER's CaseParticipant, RM.ACCEPTED MUST denote that the case has been successfully created and is being coordinated.RM.ACCEPTED marks the end of the initialization phase and the start of active coordination; it is the CASE_MANAGER's counterpart to a reporter's or vendor's RM.ACCEPTED (engagement decision). See ADR-0051. |
Refines: CM-23-006 |
CM-23-010 |
MUST | For the CASE_MANAGER's CaseParticipant, RM.CLOSED MUST denote that the Case Owner has left the case and the case is fully closed.RM.CLOSED on the CASE_MANAGER's own participant record is the terminal state; it signals that no further ledger entries will be authored for this case. See ADR-0051. |
Refines: CM-23-006 |
CM-23-011 |
MUST | When the CASE_MANAGER receives Leave(VulnerabilityCase) from the Case Owner while the case still holds an active embargo (the current EM state is EM.ACTIVE or EM.REVISE), it MUST decline the closure with an as:Reject response ("received and understood but declined", MSM-05-001) and MUST NOT execute the CM-23-002 closure sequence (no participant advances to RM.CLOSED, the CASE_MANAGER included, so the case does not become fully closed). The decline MUST suppress the receipt commit itself: a declined owner-close records NO close_case CaseLedgerEntry at all. Gating the receive-and-commit path behind the decline is required, not optional, because that receipt commit opens the closure — it is committed before any role-specific effect (CLP-10-006), and the status entries that follow it (CM-23-001) are what advance the departing participant to RM.CLOSED on each replica (CLP-10-001, CM-23-003). Skipping only the local CM-23-002 steps while still committing the receipt would leave a recorded departure the case then declined, contradicting "no participant advances to RM.CLOSED" (ISSUE-3137). The Case Owner MUST terminate the active embargo first, then re-issue the close. Declining the close does NOT itself terminate the embargo — the embargo remains active until terminated through the normal EM path. This is the terminal, case-global counterpart to the participant-scoped VP-13-005 (participants SHOULD NOT close while embargoed) and VP-13-009 (a participant close while others remain engaged does not auto-terminate the embargo): owner-close ends the case for everyone, so the soft participant-level guidance is raised to a hard case-actor refusal.Owner-close is a hard, terminal write boundary (ADR-0085): the CASE_MANAGER then accepts no further external ledger writes. VP-13-009 keeps an embargo alive across a participant close only while others remain engaged; owner-close leaves none and no authority to lift it, so an embargo left active could never be discharged. Refusing the premature close forces an explicit terminate-then-close ordering. Leave is understood but declined — hence as:Reject, not Create(ProcessingFault) (MSM-05-001). |
Refines: CM-23-002 Refines: VP-13-005 |
CM-23-012 |
MUST | Leave(VulnerabilityCase) advances only the leaving actor's own RM state to RM.CLOSED, regardless of which RM rung that actor currently holds (RECEIVED, INVALID, VALID, ACCEPTED, or DEFERRED). Each closure is written as transitions the RM transition function permits: a Leave from RM.VALID is recorded as V → D → C (RMB-14-005), and a Leave from RM.START, by an actor that has not yet recorded receiving the case it is leaving, is recorded as S → R → C, because S → C is not a transition. Case closure MUST NOT advance any non-leaving ("bystander") participant's RM state: on owner Leave, only the Case Owner and the CASE_MANAGER advance to RM.CLOSED (CM-23-002); every other participant MUST retain its last RM state. The characterization of owner-close as ending "the case for everyone" (CM-23-011) refers to the case-level write boundary — the CASE_MANAGER accepts no further external ledger entries (ADR-0085) — and MUST NOT be read as a directive to force each remaining participant's RM dimension to RM.CLOSED. A participant's RM state is the self-declaratory record of how that participant handled the report (ADR-0121). A Leave is that participant's own authoritative closure act, so advancing the leaving actor to RM.CLOSED regardless of rung is legitimate self-declaration, not an externally imposed jump. Every closure is an ordinary transition: the RM transition function closes from Received, Invalid, Accepted and Deferred (ADR-0114), and a Leave from Valid passes through Deferred first, so no leave/close write site overrides the transition table (RMB-14-005). A bystander that never sent Leave has made no closure declaration; forcing its RM to RM.CLOSED would fabricate a declaration it never made. Such a participant simply remains at its last rung when the case ends around it (the library closes before every book is returned). The demos' "all participants RM.CLOSED" milestone (DEMOMA-07-003) is reached because every participant closes its own handling through the protocol before the Case Owner closes the case (CM-23-013), not because closure pushes them there. |
Refines: CM-23-001 Refines: CM-23-002 |
CM-23-013 |
MUST | Once the CASE_MANAGER has committed its own status entry reaching RM.CLOSED (CM-23-002 step 2), the case is fully closed, and the CASE_MANAGER MUST behave toward the case as if its process had exited: for any inbound activity for the case — a non-owner participant's Leave(VulnerabilityCase) included — it MUST NOT commit any CaseLedgerEntry, the receipt entry included, and MUST NOT change any case or participant state. It MAY reply with an as:Reject ("received and understood but declined", MSM-05-001), or MAY drop the activity with no reply at all. A participant MUST NOT rely on receiving a reply to an activity it sends after the case has closed. The sender's RM state on the canonical ledger stays at its last rung (CM-23-012). The boundary covers every activity class — RM (R), EM, CS and general-inquiry (GI) traffic such as Add(Note) alike — and is a hard kill switch, not a selective gate (ADR-0085). The CASE_MANAGER MAY keep answering a replica's resync request (Reject(CaseLedgerEntry)) by replaying entries the ledger already holds, because a replay appends nothing; it MUST NOT append a new entry on behalf of an external participant.A deployment's process for a closed case can exit once closure has fanned out. The prototype's CASE_MANAGER behaves the same way: nothing reaches the ledger, and a reply is a courtesy. The receipt commit is suppressed because it opens the closure whose status entries would close the sender on the replicas (CLP-10-006, CM-23-001). A participant that wants its departure recorded leaves first (CM-23-015). Resync is exempt: a replay appends nothing.* |
Refines: CM-23-003 Refines: CM-23-002 Depends on: MSM-05-001 |
CM-23-014 |
MUST | As part of the CM-23-002 owner-close sequence, after the close_case entry and before it writes any closure step, the CASE_MANAGER MUST expire every embargo Invite still awaiting a reply on the case (every consent row that is INVITED moves to TIMED_OUT), without waiting for its RSVP deadline. It MUST commit each expiry as its own Update(EmbargoConsent) carrying the TIMED_OUT row (CM-28-009, CM-28-014). A pending case Invite carries no RSVP deadline and so needs no settling: an answer to it after close is an external append, refused at the ADR-0085 boundary.After close a pending Invite can only expire. Waiting for its RSVP deadline would keep the CASE_MANAGER running for a closed case, possibly for years, only to write one more entry after the write boundary. Settling first leaves no timer, so the CASE_MANAGER can stop once the closure has fanned out. This keeps the boundary clean in practice; it does not promise chain finality (CM-23-002). |
Refines: CM-23-002 Depends on: CM-28-009 Depends on: CM-28-014 |
CM-23-015 |
SHOULD | The Case Owner SHOULD leave a case last, or nearly last, after the other participants have left, so that their departures are recorded before the case is fully closed; a departure sent after closure is not recorded (CM-23-013). The owner's Leave closes the case for everyone (CM-23-002), and nothing a participant sends afterwards reaches the ledger. A participant that has not left by then keeps its last RM state on every replica that receives it, which is harmless but leaves the ledger without a record of its departure. Leaving last is the owner's normal course, not a hard ordering: a participant that never leaves cannot hold a case open. Every demo scenario closes through close_case_owner_last (vultron/demo/helpers/closure.py), which sends the owner's Leave only once the CASE_MANAGER has recorded each other departure. |
Refines: CM-23-002 Depends on: CM-23-013 |
CM-23-016 |
MUST | A participant's case replica is the case as it stood when the replica was seeded plus the ledger entries since. A replica MUST apply the entries in ledger order by copying the objects they carry, so that two replicas at the same ledger position hold identical case files. Applying an entry MUST NOT mint an id, read a clock, read local policy or state outside the case, or run lifecycle logic, for example by walking the RM transition table from its own stored state to a target state. The CASE_MANAGER is the single mover (ADR-0108) and the ledger records each change as it makes it (CLP-07-013). A replica that re-derives a path from its own, possibly lagging, state can write an RM rung the ledger never recorded (#4091), and one that builds a record itself gives it a different id and time from the CASE_MANAGER's (#4425), so replicas diverge from the CASE_MANAGER. CM-23-001 states the rule for closure; this states it for every replicated change (ADR-0124). |
Extends: CM-23-001 |
CM-23-018 |
MUST | The CASE_MANAGER MUST refuse every activity for a case from a sender whose closure that case's ledger records (its status entry with rmState=CLOSED), with no ledger entry committed and nothing sent: the refusal is the sender guard's, so it gets no refusal notice (CLP-05-001, HP-01-006). There are two exceptions. First, it MUST answer that sender's Reject(CaseLedgerEntry) with a replay of only the entries that record its own closure (CM-23-004): that resync request is admitted by the active check of SYNC-03-005, not refused as a closed sender's (CM-23-020). Second, a repeated act that would close that sender, who is already closed (a Leave(VulnerabilityCase), or a Reject of its stub or full-case Invite), MUST be reported SKIPPED, with no ledger entry committed and nothing sent, whatever its activity id: its effect is already in place (ID-05-003). The Case Owner's Leave(VulnerabilityCase) is not such a repeat while the case is not fully closed: its effect includes the case's closure (CM-23-002), which is not in place while the case is open, so it is processed to complete it. A failed owner Leave keeps none of the closure (CLP-07-015), so this owner's closure was recorded by an act other than its Leave. A Close(Report) closes no sender on a case, so that sender's is refused like every other sender's (CM-23-019).RM.CLOSED is terminal (ADR-0085). A closed participant is sent no later case content (CM-23-004), so its act would be recorded in a case it cannot see, and CM-23-013 stops writes only once the case is fully closed. The resync request stays open: its replay appends nothing and is capped. A second closing act changes nothing, whichever act closed the sender first, and a repeat is decided by effect, not by activity id. Resolves CONCERN-4556 and #4660. |
Refines: CM-23-004 Refines: HP-01-006 Refines: CM-10-004 Refines: SYNC-03-005 |
CM-23-019 |
MUST | Once a report belongs to a case, only that case's Case Owner holds the report, and the Case Owner closes the case by Leave(VulnerabilityCase) (CM-23-001, CM-23-002), never by Close(Report). The CASE_MANAGER, and any participant holding a replica of the case, MUST refuse a Close(Report) for a report that belongs to the case, from every sender: from the Case Owner it is the wrong closing act, and from any other sender it is a message about a report that sender does not hold. The refusal is the sender guard's, since no sender is entitled to send it to a case actor, so it MUST write no RM state, commit no ledger entry and send nothing, a refusal notice included (CLP-02-005, HP-01-006). An actor MUST NOT send Close(Report) for a report that belongs to a case it holds: its closing trigger MUST fail before anything is written or sent. A Close(Report) for a report that belongs to no case is unaffected.A Close(Report) moved its sender to RM.CLOSED without the Leave round-trip, so a Case Owner could close itself while its case stayed open, keep acting as owner, and leave the case never fully closed (#4746). Turning the owner's Close(Report) into a full close would give the case two closing messages, and refusing only a closed owner's later acts would leave the wrong message admitted (ADR-0144). |
Refines: CM-23-001 Refines: CM-23-018 |
CASE_MANAGER-Delegated Activity Authorship Contract
Requirements for how trigger use cases assign actor and attributed_to when the CASE_MANAGER emits an Activity on behalf of a requesting participant (delegated message). The reference implementation is delegated_authorship() (vultron/core/behaviors/delegated_authorship.py). Codified from CONCERN-2170.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-24-001 |
MUST | When the CASE_MANAGER emits a protocol Activity on behalf of a requesting participant (delegated message), the actor field of the emitted Activity MUST be set to the CASE_MANAGER's actor ID, not the requesting participant's actor ID.Receiving actors authenticate messages by matching the actor field against known case participants. An Activity whose actor names a non-CASE_MANAGER participant is not recognised as a CASE_MANAGER-authorised delegation and may be rejected or misrouted. Bug ISSUE-2142 (ownership-transfer offer delivery timeout) is direct evidence: the Coordinator rejected an Offer whose actor named the Finder. |
Refines: CM-02-009 |
CM-24-002 |
MUST | When a delegated Activity is emitted by the CASE_MANAGER, the attributed_to field MUST be set to the requesting participant's actor ID so that receivers can recover the originating identity.attributed_to is the AS2-standard field for recording who delegated an action. Without it, the intent of the message cannot be attributed to the correct participant, and audit trails are incomplete. |
Refines: CM-24-001 |
CM-24-006 |
MUST | A case MUST have a participant holding CASE_MANAGER from case creation onward: both creation paths register one (a dedicated CASE_MANAGER actor holding COORDINATOR + CASE_MANAGER under ADR-0041, or the creating actor as CASE_OWNER + CASE_MANAGER under CM-02-015), and role delegation transfers it without a gap. An actor that finds no CASE_MANAGER for a case MUST fail the act, and MUST NOT send the activity directly to another participant instead (CM-24-008).An earlier requirement (retired; plan/retired-specs/CM-24-003.md) codified a "no dedicated CASE_MANAGER, send directly" fallback that no creation path can produce; it survived in the delegated-context helper, in two nodes that returned SUCCESS with a warning when the resolver found nobody, and in tests that asserted the fallback. An arm for an unreachable state is where a corrupt roster hides (#3918, ADR-0113 as amended). |
Refines: CM-24-001 Depends on: CM-22-002 |
CM-24-007 |
MUST | When the CASE_MANAGER records an activity and then forwards it on its sender's behalf (CM-33-001), the forward MUST carry the same id on every attempt, derived from the recorded activity, and the record and the forward MUST commit as one act (CLP-07-015), so a failure to build or send the forward keeps neither. That failure MUST NOT be reported as a refusal of the sender (HP-01-003). A redelivery of an activity whose first delivery failed so MUST be processed as a new delivery. A redelivery of a recorded activity MUST send nothing. The record precedes the forward (CLP-10-006), so a forward that failed after a kept record left a recorded request its addressee never got and cannot answer (#4814). With the record and the forward in one act, no recorded request lacks its forward, so no redelivery has a forward to finish (#4983). One id makes a resend safe: the outbox holds one pending entry per id (OX-06-002), and the addressee, which commits nothing, rebuilds its answer under the same id. |
Refines: CM-24-004 Depends on: OX-06-002 Depends on: CLP-10-006 |
Observer Participant Semantics
Requirements for the CVDRole.OBSERVER role (formerly CVDRole.OTHER): what an Observer IS, how it is admitted, what content it receives, and how the RM lifecycle applies. Codified by ADR-0057 (docs/adr/0057-observer-participant-role.md). Source: CONCERN-2093.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-25-001 |
MUST | An Observer (OBSERVER) is a Tier-1 case participant: it MUST hold a CaseParticipant record, run the standard RM lifecycle, and satisfy the same embargo-consent gate (MV-10-005) as any other participant. OBSERVER is the base role — the lowest non-null privilege set in the Vultron protocol. All other CVD roles are additive to it.Defining Observer as a full participant (not a passive consumer or an external T0 actor) ensures it is subject to the same admission and embargo-consent rules as Vendors and Coordinators. The "base role" framing establishes OBSERVER as the correct fallback when no other role applies, rather than leaving CVDRole.OTHER as an unspecified catch-all. See ADR-0057. |
|
CM-25-002 |
MUST | An Observer MUST be admitted to a case via the standard Invite/Accept flow defined in CM-17.Requiring a non-empty case_roles value is load-bearing: the participant record created with the stub Invite (CM-11-006) carries the roles that Invite names. A missing or empty list would create an Observer participant with no role — indistinguishable from a defect. |
Refines: CM-17-003 Refines: CM-16-003 |
CM-25-003 |
MUST | An Observer MUST receive full case content once it is an active participant (CM-10-004). That check is the sole gate on case content for an Observer. No additional or reduced delivery tier applies. An Observer that accepts the stub Invite consents to the active embargo (MV-10-006). The absence of a remediation function does not change what the Observer is entitled to receive once it is active. Introducing a separate reduced delivery tier for Observers is deferred: notes/selective-disclosure.md describes the potential future mechanism but it is not yet implemented. |
Depends on: CM-10-004 Depends on: MV-10-005 Depends on: MV-10-006 |
CM-25-004 |
SHOULD | After accepting a case invitation and receiving the full case replica, an Observer SHOULD run the standard RM triage cycle (CM-11-002): RM.VALID or RM.INVALID, then RM.ACCEPTED or RM.DEFERRED. For an Observer, RM.ACCEPTED means "I am actively engaged with / monitoring this case" — it does NOT imply commitment to develop or deploy a fix. The RM triage cycle represents an Observer's assessment of whether the case is relevant to them and whether they intend to remain engaged. An Observer that determines the case is outside their scope should transition to RM.INVALID or RM.DEFERRED, not silently stay at RM.RECEIVED. This preserves the RM machine's semantics as a participation-intent signal. |
Depends on: CM-11-002 |
CM-25-005 |
MUST NOT | A participant whose only case role is OBSERVER MUST NOT emit VFD state-transition messages (CV, CF, or CD). An Observer has no vendor-fix-deployment obligations and no authority to advance the VFD dimension on behalf of a product or service. An Observer joined the case to monitor it, not to own a fix path. Permitting VFD emissions from a role with no fix obligations would corrupt the VFD state machine for the case (e.g., a monitoring actor falsely claiming fix deployment). |
Refines: CM-03-005 |
CM-25-006 |
MUST | The Invite activity sent by the CASE_MANAGER for an Observer MUST carry case_roles naming OBSERVER only.The participant record created with the stub Invite (CM-11-006) carries the roles that Invite names; omitting CVDRole.OBSERVER would create a participant with an empty role list, indistinguishable from the caseRoles=[] defect (issue #1288). |
Refines: CM-25-002 |
CM-25-007 |
MUST NOT | An Observer invitation MUST NOT carry an empty case-roles list: an empty list names no role, so the invitee's participant record would violate the CM-16-003 non-empty invariant.[] provides no role information to the CASE_MANAGER's participant-creation path on the stub Invite (CM-11-006), producing a participant record that violates the non-empty role invariant enforced by CM-16-003. |
Refines: CM-25-006 |
Role Stacking — Union of Permissions
Requirement governing how conflicting obligations are resolved when a participant holds multiple CVD roles simultaneously. Codified by ADR-0057. Source: CONCERN-2093.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-26-001 |
MUST | A participant's permitted and obligated actions are determined by the union of what their role list permits. A MUST NOT restriction attached to one role is superseded when any other role in the participant's case_roles list carries a MAY, SHOULD, or MUST for the same action class.CVD roles represent orthogonal functions a participant may perform. When a participant holds more than one function (e.g., a security researcher who is also a Deployer for an affected product), restricting them based on their least-capable role would incorrectly limit their protocol participation. The union model prevents role restrictions from blocking obligations that exist under a co-role. |
Refines: CM-25-005 |
Embargo Invite RSVP Deadlines
Requirements for the optional RSVP-by deadline carried on an embargo invitation, the authority relationship between that wire value and the CM-18-002 pocket-veto policy window, and how an expiry is recorded. Codified by ADR-0065. Source: IDEA-2066.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-28-001 |
MUST | An Invite(EmbargoEvent) activity MAY carry an activity-level end_time. When present, the Invite's end_time MUST be interpreted as the RSVP-by deadline for that invitation — the instant after which the invitation is no longer open. It MUST NOT be interpreted as the embargo's expiry.as_Object.end_time is inherited by as_Activity and therefore by every Vultron activity, so no vocabulary extension is required. On an Activity, AS2 end_time already means "valid until", making the RSVP reading idiomatic rather than a repurposing. Placing the deadline on the activity rather than on the embargo object is what allows the same embargo to be offered to several actors with different deadlines, and to one actor twice. See ADR-0065. |
|
CM-28-002 |
MUST | When the Invite's end_time is present it MUST be authoritative for that invitation's expiry, superseding the receiver's local policy window. When the Invite's end_time is absent, the CM-18-002 policy window MUST apply, preserving the pre-existing behaviour. Either way the resulting deadline is subject to the EP-07-006 ceiling.An explicit wire value lets both parties compute the same lapse instant from the same field, with no additional message exchange. A receiver-local-only window leaves the two parties unable to agree on whether an invitation is still open — the coordination ambiguity that motivated IDEA-2066. See ADR-0065. |
Refines: CM-18-002 Depends on: EP-07-006 |
CM-28-003 |
MUST | The actor holding CASE_MANAGER MUST be the enforcement authority for embargo invite expiry.Centralising enforcement at the CASE_MANAGER matches the case-communication model (PCR-08) and the CASE_MANAGER's role as canonical ledger author (ADR-0019, ADR-0021). See ADR-0065. |
|
CM-28-004 |
MUST | An expired invitation MUST be recorded as the consent row's TIMED_OUT state via the TIME_OUT trigger (INVITED → TIMED_OUT), never as DECLINED. DECLINED MUST be reached only by an explicit refusal, so the consent row distinguishes "never responded" from "explicitly declined".A participant that did not answer made no decision, and code, logs, demos and replicas read the consent row, not ledger provenance (ADR-0118). An expired participant is re-invitable (CM-18-003), excluded from embargoed content like any non-signatory (CM-10-004), and made a signatory by a late Accept the CASE_MANAGER honours (EMB-17-002). ADR-0118 reverses ADR-0065's choice to record a lapsed invitation as DECLINED. |
Refines: CM-18-003 |
CM-28-005 |
MUST | An expired embargo invitation MUST be distinguishable from an explicit refusal both in the participant's consent row (TIMED_OUT versus DECLINED, CM-28-004) and in the canonical case ledger (CM-28-009).The canonical ledger is the project's history mechanism (ADR-0019), so expiry-versus-refusal is recoverable from it; since ADR-0118 the consent row carries the distinction too, so a reader that consults only the participant record also sees it. See ADR-0065, ADR-0118. |
|
CM-28-009 |
MUST | When an invitation's RSVP deadline passes unanswered, the CASE_MANAGER MUST record the expiry as its own Update(EmbargoConsent) carrying the invitee's TIMED_OUT row in full, committed as sent. It MUST NOT record the expiry as an activity attributed to the invitee, which sent nothing. The entry differs from the invitee's Reject(Invite(EmbargoEvent)), whose row is DECLINED.The expiry is the occasion and the Update is the CASE_MANAGER's act on it, a real AS2 activity (ADR-0140). The TIMED_OUT row it carries makes expiry-versus-refusal distinguishable in the ledger (CM-28-005), and it is how a replica learns the TIMED_OUT state without computing it (CM-28-014). A second, invitee-attributed entry would record an act nobody sent. See ADR-0065, ADR-0118. |
Refines: CM-28-005 |
CM-28-011 |
MUST NOT | The CM-18-002 policy window MUST NOT be applied so as to place the effective RSVP deadline after the embargo's own end_time. Where the window would overshoot, the deadline is the embargo's end_time.The policy window is an absolute duration (default 7 days) measured from the invitation's published timestamp, with no reference to the embargo it concerns. Applied to a short embargo it produces a pocket veto that fires after the embargo has already ended, recording an expiry for a decision that no longer exists. EP-07-006 states the general rule; this requirement binds the specific fallback path CM-18-002 defines. See ADR-0096. |
Refines: CM-18-002 |
CM-28-012 |
MUST | When the CASE_MANAGER relays an Invite(EmbargoEvent) to a participant (EP-09-002) it MUST set the Invite's end_time to that Invite's published time plus the configured RSVP window, subject to the EP-07-002 minimum and the EP-07-006 ceiling. The Invites of one relay round are emitted in sequence and need not share one deadline.No trigger put a deadline on the wire, so every receiving store fell to the EP-07-001 fallback and derived a deadline from its own ActorConfig window, and two replicas could disagree about when the same invitation closed. The relaying CASE_MANAGER is the enforcement authority (CM-28-003); it states the deadline once and everyone reads the same number off the same object, which is also what ASK-03-004 requires of an ask (#3918, ADR-0113 as amended). |
Refines: CM-28-002 Depends on: EP-09-002 Depends on: EP-07-002 Depends on: EP-07-006 |
CM-28-013 |
MUST | The CASE_MANAGER MUST record the RSVP deadline on the invitee's INVITED consent row for the invited embargo when it commits the Invite emission, as its own Update(EmbargoConsent) entry carrying that row and committed right after the Invite's (CLP-07-013), and every replica MUST record the same deadline by copying that entry (EP-09-007, ADR-0124). A participant MUST NOT derive or store an RSVP deadline on receipt of an Invite (EP-09-003).The deadline is case state about the invitee; like every other case state it is written once by the CASE_MANAGER and reaches replicas through the ledger (ADR-0108). Writing it at receipt was the one-store shape ADR-0113 retired for EM (#3918). |
Depends on: CM-28-012 Depends on: EP-09-007 Refines: EP-09-003 |
CM-28-014 |
MUST | Only the CASE_MANAGER evaluates invite expiry (CM-28-003). Its Update(EmbargoConsent) carrying the invitee's TIMED_OUT row (CM-28-009) MUST be committed by the CASE_MANAGER before the row is written (ADR-0124, CLP-07-013), and reaches replicas as that ledger entry (CM-28-016). A replica learns an expiry from that entry, copying the row, and never computes one.The expiry commit ran unconditionally in whichever store processed a late Accept, so a non-manager could author a ledger entry, and no replay node existed for the semantic type, so an expiry recorded by the manager reached no replica (#3918). |
Refines: CM-28-009 Depends on: RSH-08-004 Depends on: BT-17-001 |
Sender Entitlement for Case Content Changes
Who may remove a note from a case or attach a report to it. Neither message has a trigger today; these entries name the sender a receiver accepts (HP-01-006).
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-30-001 |
MUST | The CASE_MANAGER MUST act on Remove(Note)[target=VulnerabilityCase] only when the sender is the note's author and an active participant (CM-10-004), or the case's Case Owner; a participant replica MUST apply the removal only from the CASE_MANAGER (PCR-03-001). Any other sender MUST be refused with the note left in the case.The handler removes any note for any sender. An author may withdraw its own note, and the Case Owner may remove anyone's; nobody else has a reason to. See #3733. |
Refines: HP-01-006 |
CM-30-002 |
MUST | The CASE_MANAGER MUST act on Add(VulnerabilityReport)[target=VulnerabilityCase] only when the sender is the case's Case Owner; a participant replica MUST apply the addition only from the CASE_MANAGER (PCR-03-001). Any other sender MUST be refused with the case's reports unchanged.The handler appends any report id to any case for any sender. See #3733. |
Refines: HP-01-006 |
Participant Removal and Reinstatement
Removing a participant withdraws its entitlement to case content and keeps its record. The Case Owner requests removal and reinstatement; the CASE_MANAGER applies them, records each as one ledger entry, and tells the affected participant directly (ADR-0116).
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-31-001 |
MUST | Removing a participant MUST record a removal fact on its CaseParticipant and MUST NOT delete the record from case_participants. The participant's status history and the ledger entries it authored MUST remain, and a removed participant MUST be inert (CM-10-004).Deleting the record erases who took part in the case and leaves ledger entries naming an actor the roster no longer holds. Removal is a decision about entitlement to case content, not about membership, so it is a fact the active check reads (ADR-0116). |
Refines: CM-10-004 |
CM-31-003 |
MUST | VulnerabilityCase MUST carry a computed activeParticipants collection on the wire holding exactly the participants the CM-31-002 check finds active, computed from the participant records (CM-31-014). A receiver reading a case MUST recompute it rather than refuse it as an unknown key; a supplied value that contradicts the recomputed one is refused (ARCH-23-005).case_participants stays the full roster that role resolution and actor_participant_index read (CM-19-001). Publishing the active set as its own collection makes Remove(CaseParticipant) literally true: the participant leaves that collection. Core types refuse unknown keys on input, so a computed field in the dump refuses the case's own serialized form unless input handles it. |
|
CM-31-004 |
MUST | Remove(CaseParticipant, target=VulnerabilityCase) is the Case Owner's request to the CASE_MANAGER to remove a participant from active participation. The CASE_MANAGER MUST refuse it when the sender does not hold CASE_OWNER, when the named participant holds CASE_MANAGER or CASE_OWNER, or when it names no participant of the case, and MUST report a removal of an already-removed participant as skipped. The CASE_MANAGER MUST NOT remove a participant on its own initiative.The Case Owner decides who takes part in the case; the CASE_MANAGER applies case changes and decides none (ADR-0108). The CASE_MANAGER role is never unfilled (CM-24-006), and the Case Owner leaves only by transferring ownership (CM-21). A participant that wants to stop taking part sends Leave(VulnerabilityCase) (CM-23), not Remove. |
Depends on: PCR-03-006 |
CM-31-005 |
MUST | An accepted removal MUST be recorded by the Case Owner's received Remove(CaseParticipant) as its entry, committed on receipt (CLP-10-006): Remove(CaseParticipant) MUST be a ledgered activity, and the CASE_MANAGER MUST NOT commit a second entry of its own for the same removal.The Remove states the change, so it is the change's entry (CLP-07-013), authored by the actor that made it, and no second entry records it again. The ledger therefore shows the removal as the Case Owner's decision rather than the CASE_MANAGER acting alone (ADR-0108). Without the canonical signature a removal cannot be committed, so it never reaches a replica. |
Depends on: CLP-10-006 |
CM-31-006 |
MUST | After committing a removal the CASE_MANAGER MUST send the removed participant a direct Remove(CaseParticipant) naming it, with actor the CASE_MANAGER and attributedTo the Case Owner (CM-24), and MUST NOT ledger that notice. The removal entry's fan-out MUST include the removed participant, and MUST be the last canonical entry it is sent, by fan-out or by replay, until it is reinstated.Learning of removal only from silence is indistinguishable from a network fault, and the removed party needs to know it is still bound and how it will hear of the embargo's end. The notice is delivery, not a second record of the move; the ledger entry keeps the removed party's replica continuous up to the removal. |
Depends on: CM-31-005 |
CM-31-007 |
MUST | A replica MUST apply a removal from its ledger entry, recording the same removal fact the CASE_MANAGER recorded. A replica MUST NOT apply a removal from a directly received Remove(CaseParticipant) (RSH-08-003).Replicas take case state from the ledger. The direct notice tells the removed party what happened; the entry is what changes every replica, the removed party's included. |
Depends on: RSH-08-004 |
CM-31-008 |
MUST | Removal MUST NOT change the participant's embargo consent rows. A removed signatory remains bound by the embargo it accepted. Releasing a removed participant from the embargo would reward the conduct that got it removed. Consent is the participant's own statement (CM-18-005); removal is a separate fact. |
Extends: CM-18-005 |
CM-31-009 |
MUST | When the active embargo is terminated (including by the case becoming public) or replaced by a shorter revision, the CASE_MANAGER MUST send every signatory participant the ledger fan-out does not reach — removed (CM-31-001), or at RM CLOSED and skipped by the CM-23-004 fan-out — a direct notice outside the ledger stream: Remove(EmbargoEvent) for a termination, Announce(EmbargoEvent) with the new terms for a shorter revision. The notice MUST carry no other case content. No notice is owed for a longer revision or for expiry on the agreed date.A participant that stopped receiving case content while bound stays bound, and without this it never learns that the obligation ended early or shortened. A longer revision does not bind a participant that never agreed to it, and expiry on the agreed date is already known to it. A past-dated Announce(EmbargoEvent) is not used: its meaning would depend on the receiver's clock, and an early end is an event, not a date that passed. "Shorter" and "longer" follow the EP-05-001 split: a revision that ends at the same time as the embargo it replaces is on the carry-over arm, so the bound participant is bound by it and is sent it like a shorter one. |
Refines: CM-10-007 |
CM-31-010 |
MUST | A participant whose ledger stream is paused — withheld under CM-10-005, removed (CM-31-001), or skipped at RM CLOSED (CM-23-004) — MUST apply a CM-31-009 notice sent by the CASE_MANAGER for the case, and MUST ignore one from any other sender. Replaying the corresponding ledger entry after the participant becomes active again MUST change nothing further.A paused replica cannot take the change from the ledger, so the notice is its only channel. The exception is to the channel, not the authority: the notice still originates from the CASE_MANAGER (PCR-03-001). |
Refines: RSH-08-003 |
CM-31-011 |
MUST | Add(CaseParticipant, target=VulnerabilityCase) is the Case Owner's request to the CASE_MANAGER to reinstate a removed participant. It MUST follow the CM-31-004 through CM-31-007 pipeline (Case Owner only, one ledger entry, a direct notice, replicas applying it from the entry), MUST clear the removal fact, and MUST NOT require the participant to accept again. The CASE_MANAGER MUST refuse an Add naming a participant that is not removed or that never joined.Owners make mistakes and disputes resolve, so removal is reversible. The participant never withdrew, so it does not rejoin; Leave remains open to it. Joining requires accepting a stub Invite (ADR-0114), and Add must not become a way around that. A participant reinstated into a case whose embargo it has not accepted stays inert, and its CM-10-006 backfill waits for its consent. |
Depends on: CM-31-004 |
CM-31-012 |
MUST NOT | The CASE_MANAGER MUST NOT emit Add(CaseParticipant), or commit an add_case_participant ledger entry, as a consequence of an invitee accepting the stub Invite. The invitee's Accept message is already a ledger entry (accept_invite_actor_to_case) and is the entry for the consent row it signs and the joined mark: a replica applies those two writes the same way the CASE_MANAGER does, with the record's updated taken from the Accept's published as received, and fails when it holds no record for the invitee. For a participant with the VENDOR role only, VF Vf is the CASE_MANAGER's own add_participant_status_to_participant entry (CM-11-009), committed after the case is announced to the invitee and the prior ledger backfilled (#2898). A replica seeded with the case MUST NOT move a record it holds backwards when it replays an older entry over the seed: joined is only set, and a consent row changes only by a legal move (CM-18-003) (ADR-0114, ADR-0124).CM-17-004 does not call for it, the Accept message is already the entry for its effects, and a replica usually refuses the direct message for want of the participant record it names. It also records one move twice, and Add(CaseParticipant) now means reinstatement only (CM-31-011). |
Refines: CM-17-004 |
CM-31-013 |
MUST | A removed participant MUST NOT be sent a stub Invite, a full-case Invite, or an embargo Invite; the CM-31-009 notices are its only exception. When a participant is reinstated into a case with an active embargo it has not accepted, the CASE_MANAGER MUST send it that embargo's Invite. Invites reach inert participants because they ask them to join or consent (CM-10-007). A removed participant was taken out by the Case Owner's decision; inviting it to vote on embargo revisions would undo that decision. A reinstated participant that is not a signatory stays inert until it consents, so it must be asked. |
Refines: CM-10-007 |
Case Split — Child Case Creation, Links, Inheritance, and Sibling Notices
How the parent case's Case Owner splits off a child case through the parent's CASE_MANAGER, what the child records and inherits, how its participants stay apart from the parent, and how the cases of one split family tell each other about embargo changes (ADR-0125, VP-10).
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-32-001 |
MUST | A case split MUST be requested by the parent case's Case Owner sending Create(CaseProposal) to the parent's CASE_MANAGER, with the activity's context naming the parent case. The CASE_MANAGER MUST act on a split proposal only when its sender holds CASE_OWNER on the parent case; a split proposal from any other sender MUST be refused, with no case created and the parent unchanged.The parent's Owner decides what the parent's coordination becomes, as it does for an ownership transfer or a merge (ADR-0053, ADR-0105). The parent context is what tells a split apart from a report-born proposal, whose sender entitlement differs (HP-01-006). |
Refines: HP-01-006 Refines: CP-05-002 |
CM-32-002 |
MUST | The CASE_MANAGER MUST answer a split proposal as it answers any Create(CaseProposal) (CP-05): Accept(CaseProposal) naming the child case in result, then Create(VulnerabilityCase) for the child, both addressed to the Owner; or Reject(CaseProposal). A redelivered split proposal with the same proposal identifier MUST NOT create a second child case (CP-05-006).Reusing the proposal answer gives the split the admission decision, the one-act creation and the duplicate handling the creation flow already has, rather than a second creation path to keep in step with it. |
Depends on: CP-05-003 Depends on: CP-05-006 |
CM-32-003 |
MUST | The parent's CASE_MANAGER MUST create the child case in its own store and MUST be the child's CASE_MANAGER. The child's attributed_to MUST be the Case Owner that requested the split, and its genesis hash MUST be derived from the child's own ID, creation time and Owner (CLP-08-002), never from the parent's.The split creates a case like any other (CM-22-001, ADR-0041), so the child has a CASE_MANAGER from creation (CM-24-006) and a ledger of its own that shares no chain with the parent's. Hosting the whole family at one CASE_MANAGER (CM-32-016) is what lets siblings be read from the parent (CM-32-006). |
Refines: CM-22-001 Depends on: CLP-08-002 |
CM-32-004 |
MUST | A child case MUST be created with parent_cases holding exactly the parent case's ID, and with child_cases and sibling_cases empty. The cross-references MUST be case IDs, never embedded cases.The child must know the case its embargo came from, so its CASE_MANAGER can route VP-10 notices. IDs, not embedded cases, because related cases are the exception ADR-0017 made to the full-object rule. |
|
CM-32-005 |
MUST | After committing the child case's creation entry, the CASE_MANAGER MUST add the child's ID to the parent case's child_cases and commit that change as one entry in the parent's ledger. A parent replica MUST take the child link from that entry. A split MUST NOT write sibling_cases on any case.Ordering the parent entry after the child's creation means the parent never names a case that does not exist. One write per split keeps the links consistent: no existing sibling changes when a new child appears, and sibling_cases stays free for the merge spec (ADR-0105). |
Depends on: CLP-02-001 |
CM-32-006 |
MUST | A split family MUST be read as a parent case together with the cases its child_cases lists, and a child's siblings as the other cases in its parent's child_cases. A case that is both a child and a parent belongs to both families, and a notice it owes under CM-32-013 or CM-32-014 goes to every other member case of each. Only a split writes child_cases (CM-32-005): a case merge MUST NOT record a merged case there, so that merged cases never join a split family.The parent stays open after a split, so it is coordinated alongside its children and counts as their sibling for VP-10-002 and VP-10-003. Reading siblings from the parent needs no stored sibling list, because one CASE_MANAGER hosts the parent and every child (CM-32-003, CM-32-016). Keeping merges out of child_cases leaves the merge spec the other two link fields (ADR-0105). |
Refines: VP-10-002 Refines: VP-10-003 |
CM-32-007 |
MUST | When the parent case has an active embargo at the split, the child MUST be created with a new EmbargoEvent of its own, scoped to the child, carrying the active embargo's end time and active from creation, with the child Owner's consent row for that embargo written AGREED, so the Owner reads as its signatory (ADR-0122). The parent embargo's consent rows MUST NOT carry over, and the parent's open proposals and revisions MUST NOT be inherited.VP-10-005 transfers the embargo; ADR-0125 limits the transfer to its terms. A new embargo object keeps consent per (participant, embargo) (ADR-0122): a child participant consents to the child's embargo, and its answer says nothing about the parent's. |
Refines: VP-10-005 Supersedes: CM-14-010 |
CM-32-008 |
MUST NOT | When the parent case has no active embargo at the split, the child MUST NOT be given an embargo, and the creation-time default embargo initialization (CM-14-010, EP-04-012) MUST NOT run for it. The child inherits the parent's embargo state, not a default: a parent whose embargo has ended or was never agreed must not gain an embargoed child the parent's participants never agreed to. |
Refines: VP-10-005 Supersedes: CM-14-010 |
CM-32-009 |
MUST NOT | A child case MUST NOT receive any of the parent case's reports, notes, ledger entries, participants, case statuses, vulnerability records or case references. Its ledger MUST begin at its own genesis hash. The split exists to keep the child's participants apart from the parent's content. Creating the child empty and adding only what the splitter wrote makes isolation the default; copying and pruning would disclose whatever the pruning missed. |
|
CM-32-010 |
MUST | A split proposal MUST carry a new VulnerabilityReport attributed to the splitter, which becomes the child's only report, and the stub summary the child's stub Invites carry (CM-17-010). The CASE_MANAGER MUST refuse a split proposal whose report is one of the parent case's reports, or that carries no stub summary.Every case has a report (CM-05-002). A report the splitter writes for the child is the child's fresh description, and refusing a parent report keeps the parent's description out of the child (CM-32-009). The splitter, not the CASE_MANAGER, decides how much an invitee not yet bound by the embargo may learn from the stub (CM-17-010). |
Depends on: CM-05-002 Depends on: CM-17-010 |
CM-32-011 |
MUST | Child case initialization MUST run the case initialization sequence (CM-14-001) with its embargo step (CM-14-010) replaced by CM-32-007 or CM-32-008 and its reporter steps (CM-14-011, CM-14-012) skipped, and MUST seat only the CASE_MANAGER and the Owner, the Owner holding CASE_OWNER at RM.RECEIVED (CM-14-009). Authoring the child's report MUST NOT give the splitter the Reporter role. These two are the participants seated by case initialization for CM-10-004, and a participant MUST NOT be invited to the child before its creation entry is committed (CM-14-007).The child has no outside reporter; the Owner wrote its description. Every other participant joins the child by accepting its stub Invite (ADR-0114). |
Supersedes: CM-14-001 Supersedes: CM-14-011 Supersedes: CM-14-012 Refines: CM-10-004 |
CM-32-012 |
MUST NOT | A child-case activity MUST NOT add a participant to the parent case's roster, and the parent's recipient selection (CM-10-007) MUST NOT return a participant of the child that is not separately a participant of the parent. Such a participant MUST receive no parent ledger entry. Joining the child is joining the child only. A participant in both cases, such as the Owner, is a participant of each separately, and parent content reaches it only as a parent participant. |
Refines: CM-10-004 Refines: CM-10-007 |
CM-32-013 |
MUST | When the CASE_MANAGER of a child case commits an embargo proposal or revision whose end time is earlier than the end time the child inherited, it MUST, before the proposal can be activated, commit a split-family embargo notice to the ledger of every other member case of the child's split family (CM-32-006), carrying the child's ID, the proposed end time and the proposer's rationale, and MUST commit the rationale to the child's ledger. The rationale is the proposing activity's content, and the CASE_MANAGER MUST refuse such a proposal whose content is empty.This is how VP-10-002 is carried out. An earlier end in one child can reveal the vulnerability in the products the others cover (VP-10, split discussion), so the other cases hear of it while it is still a proposal, and the reason is on record. The activity's content carries it because embargo proposals have no rationale field of their own. |
Implements: VP-10-002 |
CM-32-014 |
MUST | When the active embargo of any member of a split family changes — a revision is activated or the embargo is terminated — the family's CASE_MANAGER MUST commit a split-family embargo notice to the ledger of every other member case of the family (CM-32-006), carrying the changed member's ID, its new end time or the fact of termination, and the rationale when the change's proposal gave one. This is how VP-10-003 is carried out. Participants address case messages only to their CASE_MANAGER (PCR-08-001), and every member case has the same CASE_MANAGER (CM-32-016), so a change agreed in one case reaches the others' participants through their own ledgers rather than from participant to participant. |
Implements: VP-10-003 |
CM-32-015 |
MUST | A split-family embargo notice MUST be committed to a member case's ledger only by that case's CASE_MANAGER, so that it reaches the case's active participants through the shared recipient selection (CM-10-004). The notice MUST NOT change the receiving case's EM state or any consent row, and MUST carry no content of the case it reports on beyond that case's ID, its new end time or the fact of termination, and the rationale. A notice informs and proposes nothing: a case's embargo changes only through its own negotiation (ADR-0125). Committing it to the receiving ledger delivers it through the shared recipient selection, so the participants of one case learn of another's change without ever being addressed by it, and learn nothing else of that case. |
Implements: VP-10-003 Refines: CM-10-004 |
CM-32-016 |
MUST NOT | While a case is a member of a split family, its CASE_MANAGER role MUST NOT be delegated to an actor other than the CASE_MANAGER of the family's other member cases; such a delegation MUST be refused with the case's CASE_MANAGER unchanged. Siblings are read from the parent (CM-32-006), and notices are ledger commits by the one CASE_MANAGER hosting the family (CM-32-013, CM-32-014). A member moved to another CASE_MANAGER could neither find its siblings nor reach them. ADR-0125 leaves a family spread across CASE_MANAGERs to a later decision. |
Extends: CM-24-006 |
Role Delegation Offers
How a role offer, Offer(CaseParticipantRole) (ADR-0039), and its answer travel through the CASE_MANAGER, which records both. The offer adds a role to an actor that is already a participant; the roles an Invite carries at joining are CM-17. The routing follows PCR-08 and the ownership-transfer model (CM-21).
| ID | Priority | Requirement | Related |
|---|---|---|---|
CM-33-001 |
MUST | A Case Owner offering a role MUST address Offer(CaseParticipantRole) to the case's CASE_MANAGER only, never to the target. When the CASE_MANAGER admits the Offer it MUST commit a CaseLedgerEntry for it and then forward it to the target, the forward carrying inReplyTo set to the recorded Offer's id, with actor the CASE_MANAGER and attributed_to the Case Owner (CM-24-001, CM-24-002). The CASE_MANAGER MUST NOT grant the role on receiving the Offer: the grant is the committed Accept (CM-02-016).A participant addresses case-scoped messages only to the CASE_MANAGER (PCR-08-001). Sent straight to the target, the Offer bypasses the CASE_MANAGER, so the answer it draws can be neither checked against a recorded Offer nor ledgered. See #4672. |
Implements: PCR-08-001 Refines: CM-24-005 |
CM-33-002 |
MUST | The CASE_MANAGER MUST act on a received Offer(CaseParticipantRole) only when the sender is the case's Case Owner. Any other sender MUST be refused, with no ledger entry and no forward. A CASE_MANAGER MAY also offer a role on its own case where its policy delegates role offers to it; it then commits and sends the Offer to the target itself, under the checks of CM-33-003. An implementation MAY omit that CASE_MANAGER-originated path.Handing out a role decides who runs part of the case, which is the Case Owner's decision, as ownership transfer is (CM-02-005). The reference implementation builds the Case Owner path only. See #4672. |
Refines: HP-01-006 Refines: CM-02-005 |
CM-33-003 |
MUST | The CASE_MANAGER MUST refuse a role Offer that carries no readable CVD role, that offers CASE_OWNER or CASE_MANAGER, whose target is not a participant in the case, whose target already holds the offered role, or whose target is the CASE_MANAGER itself. A refused Offer MUST leave no ledger entry and MUST NOT be forwarded; a missing role MUST NOT be read as any default role.A grant only adds a role, so an accepted CASE_OWNER or CASE_MANAGER offer would leave the case with two holders of a role it has exactly one of (CM-21-001, CM-02-001). Ownership moves by ownership transfer (CM-21); a CASE_MANAGER handoff moves ledger authority and is not designed (#3362). The CASE_MANAGER takes on no other role by Offer: the forward of CM-33-001 would be addressed to itself (BT-17-008), and the answer of CM-33-004 would go to itself. The received use case once read a missing role as CASE_MANAGER. See #4672. |
Constrains: CM-02-016 Refines: CM-02-001 |
CM-33-004 |
MUST | The target of a role Offer MUST act on it only when the CASE_MANAGER sent it, and MUST address its Accept or Reject to the CASE_MANAGER only, never to the Case Owner that made the Offer. A role Offer sent to the target by any other actor MUST be refused, with no answer sent.A replica takes case changes from the CASE_MANAGER alone (PCR-03-001), and the answer is a case-scoped message (PCR-08-001). An answer that reaches only the Case Owner is never ledgered. See #4672. |
Implements: PCR-08-001 Refines: PCR-03-001 |
CM-33-005 |
MUST | The CASE_MANAGER MUST act on Accept(Offer(CaseParticipantRole)) or Reject(Offer(CaseParticipantRole)) only when the sender is the target named in a role Offer it recorded for the case and that Offer is open. An Offer is open until the ledger holds an Accept or Reject entry for it (CLP-11-001). An answer naming the forward MUST be resolved to the recorded Offer through the forward's inReplyTo. Any other sender, or an answer to an Offer that is not open, MUST be refused with no ledger entry and no role change; a redelivery of the answer that closed the Offer MUST be reported SKIPPED (HP-01-003).The answer decides whether a role is granted, so only the actor offered the role may give it, and only once. The check is against the Offer the CASE_MANAGER recorded, never the copy embedded in the answer (as CM-11-017 and CM-21-011 do). See #4672. |
Refines: HP-01-006 Depends on: CM-33-001 |
CM-33-006 |
MUST | When the CASE_MANAGER admits an answer to a role Offer under CM-33-005, it MUST commit a CaseLedgerEntry for it (accept_case_participant_role or reject_case_participant_role) and MUST broadcast Announce(CaseLedgerEntry) to the case's participants. On an Accept it MUST grant the role on its own replica as CM-02-016 requires; a Reject changes no role.A participant's answer to an Offer is a case event (CLP-02-012), and the coverage test exempted both answers until they reached a commit (CLP-09-003). See #4672. |
Implements: CLP-02-012 Implements: PCR-08-003 Depends on: CM-33-005 |
CaseProposal Protocol
Requirements for the CaseProposal protocol flow, which separates the act of requesting case initialization from the act of creating the case. Covers the wire vocabulary type, the Create/Accept/Reject message flow, and the corresponding MessageSemantics values and received-side use cases.
File ID: CP
Wire Vocabulary
| ID | Priority | Requirement | Related |
|---|---|---|---|
CP-01-006 |
MAY | as_CaseProposal MAY include an optional summary field with a human-readable description of the proposal. |
|
CP-01-009 |
MAY | The actor of Create(as_CaseProposal) MAY be the proposing actor's full actor profile inline, or a reference to that profile which the CASE_MANAGER dereferences by an API call (AKM-05-003, AKM-05-004).The proposing actor becomes the CASE_OWNER (CP-09-001), and its published embargo policy lives on its profile (EP-01-001), in its own store, which the CASE_MANAGER cannot read (PCR-01-003). That policy is the CASE_OWNER's actor default at case creation (EP-04-003), so it has to reach case creation; whether the profile travels inline or is fetched is an implementation choice. This codebase requires the inline form (CP-01-010), a prototype shortcut, not a protocol rule. |
Depends on: EP-04-003 Depends on: EP-01-001 Depends on: AKM-05-003 |
CP-01-011 |
MUST | as_CaseProposal MUST carry, as proposerCaseRoles, the CVD roles the proposing actor holds in a case it owns, copied from its own ActorConfig.default_case_roles (CFG-07-002). An absent or empty proposerCaseRoles states no role beyond CASE_OWNER.The CASE_MANAGER seats the proposer with these roles (CP-09-007) and has no other source for them: it may run in a different container, and it cannot read another actor's configuration or store (ADR-0073). Carrying them follows the rule CP-01-010 applies to the proposer's embargo policy. |
Refines: CP-01-001 Depends on: CFG-07-002 |
CP-01-012 |
MUST | A Create(as_CaseProposal) whose proposerCaseRoles names CASE_MANAGER MUST be refused at the parse edge with a reason naming the fault.Only the CaseActor holds CASE_MANAGER. A proposer that could state it would seat itself as the manager of the case it proposes. |
Extends: CP-01-011 |
Protocol Flow — Sending
| ID | Priority | Requirement | Related |
|---|---|---|---|
CP-04-001 |
MUST | A report receiver that wishes to request case initialization at a dedicated case-actor service MUST send Create(as_CaseProposal) to the case-actor service's inbox, with actor set to the receiver's own URI.Using Create with the report receiver as actor accurately declares that the receiver created the proposal object, not the case itself. Sending Create(VulnerabilityCase) from the receiver would violate AS2 semantics because the receiver is not the authoritative creator of the case. |
Implements: CP-01-001 |
CP-04-003 |
MUST | Create(as_CaseProposal) MUST be addressed to the case-actor container's identity — {case_actor_service_url}/actors/case-actor — and MUST NOT be addressed to an identity derived per case or per report.A derived identity is unhostable by construction: the sender computes it, so no container has registered it, and delivery answers a permanent 404 (ISSUE-1872). It also cannot be provisioned in advance, because it is not computable until the report exists — by which time the proposal has already been sent. A container identity comes from configuration and can be provisioned once, at seed time. Nothing is lost: which case the proposal concerns is recoverable from its report_id, and every later case-scoped message carries activity.context. |
Refines: CP-04-001 Refines: BT-10-002 |
Protocol Flow — Receiving (Case-Actor Side)
| ID | Priority | Requirement | Related |
|---|---|---|---|
CP-05-001 |
MUST | A CreateCaseProposalReceivedUseCase MUST be implemented on the case-actor side to handle incoming Create(as_CaseProposal) activities. |
|
CP-05-002 |
MUST | CreateCaseProposalReceivedUseCase MUST evaluate the proposal and either accept or reject it, emitting an Accept(CaseProposal) or Reject(CaseProposal) activity accordingly. |
|
CP-05-003 |
MUST | When accepting a proposal, the case-actor service MUST send two activities in sequence: first Accept(as_CaseProposal) (with the as_CaseProposal, inline or by reference, as object_), then Create(VulnerabilityCase) (with actor set to the case-actor URI, context set to the URI of the new VulnerabilityCase, and in_reply_to set to the URI of the Accept(CaseProposal) activity).Accept(CaseProposal) acknowledges the report receiver's proposal. Create(VulnerabilityCase) with actor=case-actor preserves AS2 "I created this" semantics: only the authoritative creator MUST be the actor of a Create activity. context MUST be the case URI (consistent with all other case-scoped activities; required for inbox deferral routing). in_reply_to carries the causal antecedent (the Accept URI) in the AS2-correct field for responses. See ADR-0045. |
|
CP-05-004 |
MUST | When rejecting a proposal, the case-actor service MUST send Reject(as_CaseProposal) with the as_CaseProposal, inline or by reference, as object_. |
|
CP-05-005 |
MUST | The case-actor service MUST queue Accept(as_CaseProposal) and Create(VulnerabilityCase) for an accepted proposal together or not at all, and MUST NOT send the Accept without the Create. Once queued, each one MUST be retried until it is delivered or its delivery attempts are exhausted (OX-13). The Accept is irrevocable once sent: a retry or a re-send MUST carry the same Accept under its original identifier, never a second one (CP-05-006).The two sends are not atomic on the network, so they are made atomic where they are queued: an Accept with no Create behind it tells the report receiver of a case nothing announces. Once both are queued, the outbox retries each one, so a failed Create needs no retry of its own. A second Accept reads as a second, independent decision. |
|
CP-05-006 |
MUST | The CASE_MANAGER MUST handle a duplicate Create(CaseProposal) idempotently: if the same proposal (the same proposal identifier) has already been accepted, it MUST re-send the stored Accept(CaseProposal) unchanged, with its original identifier, and MUST NOT create a second VulnerabilityCase. When that case is fully closed (its ledger holds the CASE_MANAGER's own RM.CLOSED status entry, CM-23-013), it MUST instead answer with Reject(CaseProposal) and MUST NOT commit any CaseLedgerEntry for the case or change its state (CM-23-013).A duplicate proposal is the only signal the case-actor gets that its acceptance was lost, so answering it is the recovery path CONCERN-2367 found missing. Re-sending the frozen original (VM-08-003) reads as the first acceptance arriving late; a new Accept is indistinguishable from a second, independent decision, which CP-05-005's irrevocability rule precludes. |
Refines: ASK-08-001 Depends on: CM-23-013 |
CP-05-007 |
MUST | A report receiver that has sent Create(CaseProposal) and received neither Accept(CaseProposal) nor Reject(CaseProposal) by the proposal's deadline MUST treat the proposal as expired and MUST NOT continue waiting indefinitely.CONCERN-2367 found that a silently lost Accept stalls the handshake permanently, because no party holds a timeout. The proposal is an ask (ASK-01-001), so it carries a deadline (ASK-03-004) and sits in the receiver's outstanding-ask register (ASK-04-001), which is actor-scoped and therefore works before a case exists. Re-proposal is a later decision, not an automatic retry (ASK-05-004) — and it is also what lets the case-actor recover (CP-05-006). |
Depends on: ASK-05-001 |
CP-05-008 |
MUST | A Create(as_CaseProposal) under a new proposal identifier is a new request even when it names a report that already has a case. The case-actor service MUST subject it to the admission decision (CP-05-002) and MUST answer it with its own Accept(as_CaseProposal) or Reject(as_CaseProposal). When the proposer is that case's owner it MUST reuse the existing case and MUST NOT create a second VulnerabilityCase, unless that case is fully closed (its ledger holds the CASE_MANAGER's own RM.CLOSED status entry), in which case it MUST answer with Reject(as_CaseProposal) and MUST NOT commit any CaseLedgerEntry for the case or change its state (CM-23-013). For any other proposer it MUST create a separate case and MUST NOT add that proposer to the existing case.The report identifier is chosen by the sender. A reuse keyed on the report alone lets a proposer join another party's case by naming its report, and collapses the one-case-per-recipient rule (CBT-06-002) into a shared case. The proposer is the second half of the key that makes the reuse safe: the same proposer re-proposing after its first proposal expired (CP-05-007) converges on its own case, and a different proposer gets its own. |
Refines: CP-05-002 Depends on: CM-23-013 |
CP-05-010 |
MUST | The CASE_MANAGER MUST act on a Create(CaseProposal) whose proposal identifier it has already recorded, by admitting, declining or storing that proposal, only when the sender is the proposer it recorded for that identifier. A delivery of that identifier from any other sender MUST be refused, with no case created, no proposal-keyed record written and nothing sent. A proposal identifier it has not recorded is a new proposal, which any actor may send (CP-05-002).The proposal identifier is chosen by the sender, and every proposal-keyed record is keyed on it alone. Without this rule, another actor that reuses an admitted identifier is treated as a redelivery: it gets a stray case, or a closed case's decline is recorded on the original proposer's identifier (#4719). |
Refines: HP-01-006 Refines: CP-05-006 |
Protocol Flow — Receiving (Report-Receiver Side)
| ID | Priority | Requirement | Related |
|---|---|---|---|
CP-06-001 |
MUST | An AcceptCaseProposalReceivedUseCase MUST be implemented on the report-receiver side to handle incoming Accept(as_CaseProposal) activities. |
|
CP-06-002 |
MUST | A RejectCaseProposalReceivedUseCase MUST be implemented on the report-receiver side to handle incoming Reject(as_CaseProposal) activities. |
|
CP-06-003 |
MUST | AcceptCaseProposalReceivedUseCase MUST update the report receiver's local state to reflect that the case-actor service has accepted the proposal (e.g., record the case-actor URI and await the subsequent Create(VulnerabilityCase)). |
|
CP-06-004 |
MUST | RejectCaseProposalReceivedUseCase MUST update the report receiver's local state to reflect the rejection (e.g., mark the pending case as rejected and surface the rejection reason if present). |
|
CP-06-005 |
MUST | The vendor MUST record the actor it addressed a CaseProposal to, and MUST act on Accept(CaseProposal) or Reject(CaseProposal) only when the sender is that actor. Once a case is established for the report, a reply MUST NOT replace the trusted CASE_MANAGER. A reply from any other sender MUST be refused with the report's case link unchanged.The accept handler sets the trusted CASE_MANAGER to whoever replied, and routing and Announce trust both read that field; any actor that knows a report id can redirect the vendor's case traffic to itself. See #3733. |
Refines: HP-01-006 Refines: CP-06-004 |
CASE_MANAGER-Authoritative Initialization on Accept(CaseProposal)
Requirements for the CASE_MANAGER to perform all case initialization natively when accepting a CaseProposal, per ADR-0041. These replace the receiver-side case-creation and prologue back-fill that ADR-0015 introduced.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CP-09-001 |
MUST | When the CASE_MANAGER accepts a Create(as_CaseProposal), it MUST create the VulnerabilityCase object with attributed_to set to the proposing actor (the report receiver, who becomes the CASE_OWNER), and MUST itself be the actor of the Create(VulnerabilityCase) it emits. It MUST NOT set attributed_to to its own actor ID.VulnerabilityCase.attributed_to records the case owner (CM-02-008), and ownership checks and ownership transfer read and write it as such (MSM-07-008, CM-21-002). AS2's "I created this" is carried by the actor of Create(X) (ADR-0023), not by the created object's attributedTo, so the CASE_MANAGER creating the case and the report receiver owning it are both recorded without conflict (ADR-0041). |
Implements: CP-05-001 Refines: CM-02-008 |
CP-09-002 |
MUST | When the CASE_MANAGER creates the VulnerabilityCase on proposal acceptance, it MUST add the report receiver as a CASE_OWNER participant at RM.RECEIVED and the reporter as a participant at RM.ACCEPTED in the CASE_MANAGER's DataLayer.The receiver's replica is seeded from the embedded participant objects in Create(VulnerabilityCase). If participants are absent at emission time the replica starts with no participant records and downstream operations that require them (e.g. embargo consent, role guards) fail. Hard-coding CVDRole.VENDOR for the receiver additionally mislabels coordinators, which makes VFD fix-lifecycle checks demand a fix they never produce. |
Refines: CP-09-001 |
CP-09-006 |
MUST | The report receiver and reporter participant records MUST be created in the CASE_MANAGER's DataLayer before the CASE_MANAGER emits Create(VulnerabilityCase) to the receiver.The receiver's replica is seeded from participant objects embedded in Create(VulnerabilityCase); participants absent at emission time leave the replica without the records required for role guards and embargo consent. |
Refines: CP-09-002 |
CP-09-007 |
MUST | Any additional CVD roles the report receiver holds MUST be the ones its proposal states in proposerCaseRoles (CP-01-011), which come from the receiver's own ActorConfig.default_case_roles (CFG-07-002). The CASE_MANAGER MUST NOT take them from its own configuration.Explicit role configuration is the only reliable source; a report receiver may be a coordinator rather than a vendor, so role inference is unsound. The configuration is the receiver's, and the CASE_MANAGER may run in another container, so its own configuration says nothing about the receiver (ADR-0133). |
Refines: CP-09-002 Depends on: CP-01-011 |
CP-09-008 |
MUST NOT | Additional CVD roles for the report receiver MUST NOT be inferred. Inferring roles (e.g. hard-coding CVDRole.VENDOR) mislabels coordinators and causes VFD fix-lifecycle checks to demand a fix they never produce. |
Refines: CP-09-002 |
CP-09-003 |
MUST | The CASE_MANAGER MUST initialize the default embargo for the new case before emitting Create(VulnerabilityCase).Embargo state is protocol-significant. Deferring initialization to the report receiver or to a later message would produce a window in which embargo-gated operations are undefined (ADR-0041). |
Refines: CP-09-001 |
CP-09-004 |
MUST | The Create(VulnerabilityCase) activity emitted by the CASE_MANAGER MUST carry the case's participant records, inline or by reference, so that the report receiver can seed its replica's participants (CP-09-002). |
Refines: CP-09-002 |
CP-09-009 |
MUST | When the CASE_MANAGER accepts a Create(as_CaseProposal), it MUST enqueue Accept(as_CaseProposal) and Create(VulnerabilityCase) before it fans out any Announce(CaseLedgerEntry) for the new case. The canonical ledger entries that record the initialization are committed after the Create(VulnerabilityCase) is queued, so that every recipient holds the case object before its first ledger entry arrives.The outbox is FIFO (OX-01-002), so enqueue order is wire order. A ledger commit ahead of the Create hands each recipient entries for a case it does not hold yet: each takes the SYNC-15 pre-genesis path (buffer, Reject, from-genesis replay) while the Create waits behind the whole fan-out. That made pre-genesis recovery the normal path of every case creation, and the Create's queue position timed out the replica gates in the rv and rcv-reject scenarios (#3033) and congested the CaseActor outbox in rcvcv (#2898). This is CM-14-011's rule applied to the CASE_MANAGER-authoritative path: the receiver's system must hold the case object before any fan-out that references it. It is also SYNC-09-002's order: the entries record a case whose creation was sent, and only the Announce(CaseLedgerEntry) fan-out follows its commit. The create-case trigger takes the same order. On both paths the case, its records, the queued Accept and Create and every creation entry are written in one DataLayer transaction (ADR-0146), so any creation entry that fails rolls all of them back before delivery is told of either activity (DL-02-004, CLP-07-013). Holding the Create back until after the commit would put the genesis entry's fan-out ahead of it, which is the pre-genesis path this rule exists to avoid. |
Refines: CP-05-003 Depends on: CM-14-011 Depends on: OX-01-002 |
Code Style Specifications
Defines code formatting, import organization, and coding style standards for Python code in the Vultron project.
File ID: CS
Port and Adapter Data Exchange
Domain Model Naming
Datetime and Timezone Conventions
| ID | Priority | Requirement | Related |
|---|---|---|---|
CS-13-005 |
MUST | A wire timestamp this node generates MUST serialize with a UTC offset (Z or +00:00). A timestamp carried from a peer MUST serialize with the explicit offset it arrived with, which need not be UTC (CLP-15-007).UTC is the form every node can write without configuration. A carried timestamp is part of an activity recorded verbatim (CLP-07-011) and compared byte for byte across replicas, so converting +05:00 to Z would keep the instant and break the record. #4164 owns pinning both halves (resolves CONCERN-3808). That every wire timestamp carries an explicit offset at all is the ActivityStreams rule CS-13-006 holds, split from this spec (MS-12-010). |
Depends on: CLP-15-007 Depends on: CLP-07-011 |
Configuration Management
Requirements for unified, structured application configuration in Vultron. Configuration uses YAML files backed by Pydantic-based models loaded via pydantic-settings with environment variable override support.
File ID: CFG
Actor Configuration
CVD Case State Behavioral Requirements
Event-Condition-Action behavioral requirements for CVD Case State (CS) transitions. Each group is triggered by either a received CS message or a local CS state transition, and specifies typed preconditions, ordered steps, and postconditions that implementations must satisfy. CS cascade chains that trigger embargo teardown reference EM behavior directly and are developed in concert with the EM behavioral requirements.
File ID: CSB
Receive CV (Vendor Aware)
Behaviors triggered when a participant receives a CV (Vendor Aware) message, updating the sender's CS state model to reflect vendor awareness of the vulnerability.
Trigger: Message Received — CV
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-01-001 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) receiving CV MUST update their model of the sending Participant's CS state to Vendor Aware (q^cs ∈ Vfd···) and emit CK to acknowledge receipt. CV announces a Vendor-specific CS transition. The receiver does not change their own CS state; they update their awareness model for the sender. The CK acknowledges receipt per VP-12 messaging norms. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Update model of sender's CS state to Vendor Aware (Vfd···) — receiver's own CS state is unchanged → Receiver's awareness model for sender reflects CS=Vfd 2. [receiver] Emit CK to acknowledge receipt of CV → CK queued for sender Postconditions - Sender's CS state recorded as Vfd; CK emitted; receiver CS unchanged |
Satisfies: MSM-03-001 — CV shorthand wire-type mapping |
Receive CF (Fix Ready)
Behaviors triggered when a participant receives a CF (Fix Ready) message, updating the sender's CS state model to reflect that a fix is ready.
Trigger: Message Received — CF
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-02-001 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) receiving CF MUST update their model of the sending Participant's CS state to Fix Ready (q^cs ∈ VFd···) and emit CK to acknowledge receipt. CF announces a Vendor-specific CS transition. The receiver does not change their own CS state; they update their awareness model for the sender. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Update model of sender's CS state to Fix Ready (VFd···) — receiver's own CS state is unchanged → Receiver's awareness model for sender reflects CS=VFd 2. [receiver] Emit CK to acknowledge receipt of CF → CK queued for sender Postconditions - Sender's CS state recorded as VFd; CK emitted; receiver CS unchanged |
Satisfies: MSM-03-002 — CF shorthand wire-type mapping |
Receive CD (Fix Deployed)
Behaviors triggered when a participant receives a CD (Fix Deployed) message, updating the sender's CS state model to reflect that a fix has been deployed.
Trigger: Message Received — CD
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-03-001 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) receiving CD MUST update their model of the sending Participant's CS state to Fix Deployed (q^cs ∈ VFD···) and emit CK to acknowledge receipt. CD announces a Vendor-specific CS transition. The receiver does not change their own CS state; they update their awareness model for the sender. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Update model of sender's CS state to Fix Deployed (VFD···) — receiver's own CS state is unchanged → Receiver's awareness model for sender reflects CS=VFD 2. [receiver] Emit CK to acknowledge receipt of CD → CK queued for sender Postconditions - Sender's CS state recorded as VFD; CK emitted; receiver CS unchanged |
Satisfies: MSM-03-003 — CD shorthand wire-type mapping |
Receive CP (Public Aware)
Behaviors triggered when a participant receives a CP (Public Aware) message, recording public disclosure of the vulnerability and initiating embargo teardown if an active embargo exists.
Trigger: Message Received — CP
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-04-001 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public (q^cs ∈ ···p··) and EM None or Exited (q^em ∈ {N, X}) receiving CP MUST transition their local CS to Public Aware (q^cs ···p·· → ···P··) and emit CK to acknowledge receipt. CP is a global (participant-agnostic) CS transition. All receivers must update their own CS state. When EM is None or Exited, no embargo teardown is needed. ECA DetailsPreconditions - EM state is None or Exited; CS matches pattern ...p.. - EM state: NONE, EXITED- CS pattern: ...p..Steps 1. [receiver] Transition local CS to Public Aware (···P··) → Receiver CS public bit set to P 2. [receiver] Emit CK to acknowledge receipt of CP → CK queued for sender Postconditions - Receiver CS is public-aware; CK emitted |
Satisfies: MSM-03-004 — CP shorthand wire-type mapping |
CSB-04-002 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public (q^cs ∈ ···p··) and EM Proposed (q^em ∈ P) receiving CP MUST transition their local CS to Public Aware. Public awareness terminates a pending embargo proposal per VP-09-001 and VP-11-002. The embargo cannot be negotiated once the information is public. ECA DetailsPreconditions - EM state is Proposed; CS matches pattern ...p.. - EM state: PROPOSED- CS pattern: ...p..Steps 1. [receiver] Transition local CS to Public Aware (···P··) → Receiver CS public bit set to P 2. [receiver] Transition EM from PROPOSED to NONE (embargo proposal abandoned) → Receiver EM state is NONE 3. [receiver] Emit CK to acknowledge receipt of CP → CK queued for sender Postconditions - Receiver CS is public-aware; EM is NONE; CK emitted |
Satisfies: VP-09-001 — Embargoes SHALL terminate immediately when information becomes public Satisfies: MSM-03-004 — CP shorthand wire-type mapping |
CSB-04-003 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public (q^cs ∈ ···p··) and EM Active or Revise (q^em ∈ {A, R}) receiving CP MUST transition their local CS to Public Aware. VP-09-001 and VP-11-002 require immediate embargo termination when public awareness occurs. The ordering — CS transition recorded before embargo teardown — satisfies constraint C-14 from the behavioral spec design note: record CS state change first, then initiate teardown. ECA DetailsPreconditions - EM state is Active or Revise; CS matches pattern ...p.. - EM state: ACTIVE, REVISE- CS pattern: ...p..Steps 1. [receiver] Transition local CS to Public Aware (···P··) → Receiver CS public bit set to P 2. [receiver] Emit ET to initiate embargo termination → ET queued for participants; EM begins transitioning to EXITED 3. [receiver] Emit CK to acknowledge receipt of CP → CK queued for sender Postconditions - Receiver CS is public-aware; ET emitted; EM is EXITED; CK emitted |
Satisfies: VP-09-001 — Embargoes SHALL terminate immediately when information becomes public Satisfies: VP-11-002 — SHALL terminate embargo when information about vulnerability or exploit is public Satisfies: VP-11-003 — SHALL initiate embargo termination upon becoming aware of publicly available information Satisfies: MSM-03-004 — CP shorthand wire-type mapping |
CSB-04-004 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) already in CS Public Aware (q^cs ∈ ···P··) receiving a duplicate CP MUST emit CK without any CS or EM state transition. All C messages warrant acknowledgment. Duplicate CP does not re-trigger CS or EM transitions. ECA Details- CS matches pattern ...P.. - CS pattern: ...P..Steps 1. [receiver] Emit CK to acknowledge receipt of CP (no state change) → CK queued for sender; CS and EM states unchanged Postconditions* - CK emitted; no state change |
Satisfies: MSM-03-004 — CP shorthand wire-type mapping |
CSB-04-005 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public (q^cs ∈ ···p··) and EM Proposed (q^em ∈ P) receiving CP MUST transition EM to None (q^em P → N). Public awareness makes further embargo negotiation moot; the pending proposal is abandoned per VP-09-001. |
|
CSB-04-006 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public (q^cs ∈ ···p··) and EM Proposed (q^em ∈ P) receiving CP MUST emit CK to acknowledge receipt. All C messages warrant acknowledgment per VP-12 messaging norms.* |
|
CSB-04-007 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public (q^cs ∈ ···p··) and EM Active or Revise (q^em ∈ {A, R}) receiving CP MUST initiate embargo termination by emitting ET. VP-09-001 and VP-11-002 require immediate embargo termination when public awareness occurs. |
|
CSB-04-008 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public (q^cs ∈ ···p··) and EM Active or Revise (q^em ∈ {A, R}) receiving CP MUST emit CK to acknowledge receipt. All C messages warrant acknowledgment per VP-12 messaging norms.* |
Receive CX (Exploit Public)
Behaviors triggered when a participant receives a CX (Exploit Public) message, recording public availability of an exploit and initiating embargo teardown if an active embargo exists.
Trigger: Message Received — CX
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-05-001 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and not yet exploit-public (q^cs ∈ ···px·) and EM None or Exited (q^em ∈ {N, X}) receiving CX MUST transition their local CS to both Public Aware and Exploit Public (q^cs ···px· → ···PX·) and emit CK to acknowledge receipt. CX in state ···px· implies the pX→PX invariant (constraint C-15): exploit public without prior public awareness is an ephemeral state that immediately resolves to both P and X. The CS transition is atomic. ECA DetailsPreconditions - EM state is None or Exited; CS matches pattern ...px. - EM state: NONE, EXITED- CS pattern: ...px.Steps 1. [receiver] Transition local CS to Public Aware and Exploit Public (···PX·) → Receiver CS public bit P and exploit bit X both set 2. [receiver] Emit CK to acknowledge receipt of CX → CK queued for sender Postconditions - Receiver CS is ···PX·; CK emitted |
Satisfies: MSM-03-005 — CX shorthand wire-type mapping |
CSB-05-002 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and not yet exploit-public (q^cs ∈ ···px·) and EM Proposed (q^em ∈ P) receiving CX MUST transition their local CS to ···PX·. Exploit publication terminates a pending embargo proposal per VP-09-001. The pX→PX invariant applies (constraint C-15). ECA DetailsPreconditions - EM state is Proposed; CS matches pattern ...px. - EM state: PROPOSED- CS pattern: ...px.Steps 1. [receiver] Transition local CS to Public Aware and Exploit Public (···PX·) → Receiver CS public bit P and exploit bit X both set 2. [receiver] Transition EM from PROPOSED to NONE (embargo proposal abandoned) → Receiver EM state is NONE 3. [receiver] Emit CK to acknowledge receipt of CX → CK queued for sender Postconditions - Receiver CS is ···PX·; EM is NONE; CK emitted |
Satisfies: VP-09-001 — Embargoes SHALL terminate immediately when information becomes public Satisfies: MSM-03-005 — CX shorthand wire-type mapping |
CSB-05-003 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and not yet exploit-public (q^cs ∈ ···px·) and EM Active or Revise (q^em ∈ {A, R}) receiving CX MUST transition their local CS to ···PX·. VP-09-001 and VP-11-002 require immediate embargo termination when exploit publication occurs. The pX→PX invariant applies. C-14 ordering: record CS transition first, then initiate teardown. ECA DetailsPreconditions - EM state is Active or Revise; CS matches pattern ...px. - EM state: ACTIVE, REVISE- CS pattern: ...px.Steps 1. [receiver] Transition local CS to Public Aware and Exploit Public (···PX·) → Receiver CS public bit P and exploit bit X both set 2. [receiver] Emit ET to initiate embargo termination → ET queued for participants; EM begins transitioning to EXITED 3. [receiver] Emit CK to acknowledge receipt of CX → CK queued for sender Postconditions - Receiver CS is ···PX·; ET emitted; EM is EXITED; CK emitted |
Satisfies: VP-09-001 — Embargoes SHALL terminate immediately when exploit publication occurs Satisfies: VP-11-002 — SHALL terminate embargo when exploit is public Satisfies: MSM-03-005 — CX shorthand wire-type mapping |
CSB-05-004 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS already public but not yet exploit-public (q^cs ∈ ···Px·) receiving CX MUST transition their local CS exploit bit to X (q^cs ···Px· → ···PX·) and emit CK. When public awareness already exists, CX transitions only the exploit bit. No new embargo teardown is triggered since the embargo should already have been terminated when P was reached. ECA DetailsPreconditions - CS matches pattern ...Px. - CS pattern: ...Px.Steps 1. [receiver] Transition local CS exploit bit to X (···PX·) → Receiver CS exploit bit X set; public bit P remains set 2. [receiver] Emit CK to acknowledge receipt of CX → CK queued for sender Postconditions - Receiver CS is ···PX·; CK emitted |
Satisfies: MSM-03-005 — CX shorthand wire-type mapping |
CSB-05-005 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS already exploit-public (q^cs ∈ ···PX·) receiving a duplicate CX MUST emit CK without any CS or EM state transition. All C messages warrant acknowledgment. Duplicate CX does not re-trigger CS or EM transitions. ECA Details- CS matches pattern ...PX. - CS pattern: ...PX.Steps 1. [receiver] Emit CK to acknowledge receipt of CX (no state change) → CK queued for sender; CS and EM states unchanged Postconditions* - CK emitted; no state change |
Satisfies: MSM-03-005 — CX shorthand wire-type mapping |
CSB-05-006 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and not yet exploit-public (q^cs ∈ ···px·) and EM Proposed (q^em ∈ P) receiving CX MUST transition EM to None (q^em P → N). Exploit publication makes further embargo negotiation moot; the pending proposal is abandoned per VP-09-001. |
|
CSB-05-007 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and not yet exploit-public (q^cs ∈ ···px·) and EM Proposed (q^em ∈ P) receiving CX MUST emit CK to acknowledge receipt. All C messages warrant acknowledgment per VP-12 messaging norms.* |
|
CSB-05-008 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and not yet exploit-public (q^cs ∈ ···px·) and EM Active or Revise (q^em ∈ {A, R}) receiving CX MUST initiate embargo termination by emitting ET. VP-09-001 and VP-11-002 require immediate embargo termination when exploit publication occurs. |
|
CSB-05-009 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and not yet exploit-public (q^cs ∈ ···px·) and EM Active or Revise (q^em ∈ {A, R}) receiving CX MUST emit CK to acknowledge receipt. All C messages warrant acknowledgment per VP-12 messaging norms.* |
|
CSB-05-010 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and not yet exploit-public (q^cs ∈ ···px·) and EM Active or Revise (q^em ∈ {A, R}) receiving CX MUST record the CS transition to ···PX· before embargo teardown is initiated (constraint C-14). C-14 ordering requires the CS state change to be recorded first so that the canonical ledger correctly reflects cause before effect. |
Receive CA (Attacks Observed)
Behaviors triggered when a participant receives a CA (Attacks Observed) message, recording active exploitation of the vulnerability and initiating embargo teardown if an active embargo exists.
Trigger: Message Received — CA
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-06-001 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and attacks not yet observed (q^cs ∈ ···p·a) and EM None or Exited (q^em ∈ {N, X}) receiving CA MUST transition their local CS to both Public Aware and Attacks Observed (q^cs ···p·a → ···P·A) and emit CK. CA in state ···p·a implies constraint C-16: attack observation without prior public awareness triggers the CS→P transition alongside CS→A. When EM is None or Exited, no embargo teardown is needed. ECA DetailsPreconditions - EM state is None or Exited; CS matches pattern ...p.a - EM state: NONE, EXITED- CS pattern: ...p.aSteps 1. [receiver] Transition local CS to Public Aware and Attacks Observed (···P·A) → Receiver CS public bit P and attacks bit A both set 2. [receiver] Emit CK to acknowledge receipt of CA → CK queued for sender Postconditions - Receiver CS is ···P·A; CK emitted |
Satisfies: MSM-03-006 — CA shorthand wire-type mapping |
CSB-06-002 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and attacks not yet observed (q^cs ∈ ···p·a) and EM Proposed (q^em ∈ P) receiving CA MUST transition their local CS to ···P·A. Attacks observed alongside previously non-public CS triggers CS→P, which terminates a pending embargo proposal. Constraint C-16 applies. ECA DetailsPreconditions - EM state is Proposed; CS matches pattern ...p.a - EM state: PROPOSED- CS pattern: ...p.aSteps 1. [receiver] Transition local CS to Public Aware and Attacks Observed (···P·A) → Receiver CS public bit P and attacks bit A both set 2. [receiver] Transition EM from PROPOSED to NONE (embargo proposal abandoned) → Receiver EM state is NONE 3. [receiver] Emit CK to acknowledge receipt of CA → CK queued for sender Postconditions - Receiver CS is ···P·A; EM is NONE; CK emitted |
Satisfies: VP-09-001 — Embargoes SHALL terminate immediately when information becomes public Satisfies: MSM-03-006 — CA shorthand wire-type mapping |
CSB-06-003 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and attacks not yet observed (q^cs ∈ ···p·a) and EM Active or Revise (q^em ∈ {A, R}) receiving CA MUST transition their local CS to ···P·A. Attacks observed without prior public awareness triggers CS→P (constraint C-16) which in turn requires embargo termination per VP-09-001. The C-14 ordering: record CS transition first, then initiate teardown. ECA DetailsPreconditions - EM state is Active or Revise; CS matches pattern ...p.a - EM state: ACTIVE, REVISE- CS pattern: ...p.aSteps 1. [receiver] Transition local CS to Public Aware and Attacks Observed (···P·A) → Receiver CS public bit P and attacks bit A both set 2. [receiver] Emit ET to initiate embargo termination → ET queued for participants; EM begins transitioning to EXITED 3. [receiver] Emit CK to acknowledge receipt of CA → CK queued for sender Postconditions - Receiver CS is ···P·A; ET emitted; EM is EXITED; CK emitted |
Satisfies: VP-09-001 — Embargoes SHALL terminate immediately when information becomes public Satisfies: VP-11-006 — SHOULD terminate embargo when attacks are known to have occurred Satisfies: MSM-03-006 — CA shorthand wire-type mapping |
CSB-06-004 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS already public but attacks not yet observed (q^cs ∈ ···P·a) receiving CA MUST transition their local CS attacks bit to A (q^cs ···P·a → ···P·A) and emit CK. When public awareness already exists, CA transitions only the attacks bit. The public bit is already set; no new P transition is needed. ECA DetailsPreconditions - CS matches pattern ...P.a - CS pattern: ...P.aSteps 1. [receiver] Transition local CS attacks bit to A (···P·A) → Receiver CS attacks bit A set; public bit P remains set 2. [receiver] Emit CK to acknowledge receipt of CA → CK queued for sender Postconditions - Receiver CS is ···P·A; CK emitted |
Satisfies: MSM-03-006 — CA shorthand wire-type mapping |
CSB-06-005 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS attacks already observed (q^cs ∈ ···P·A) receiving a duplicate CA MUST emit CK without any CS or EM state transition. All C messages warrant acknowledgment. Duplicate CA does not re-trigger CS or EM transitions. ECA Details- CS matches pattern ...P.A - CS pattern: ...P.ASteps 1. [receiver] Emit CK to acknowledge receipt of CA (no state change) → CK queued for sender; CS and EM states unchanged Postconditions* - CK emitted; no state change |
Satisfies: MSM-03-006 — CA shorthand wire-type mapping |
CSB-06-006 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and attacks not yet observed (q^cs ∈ ···p·a) and EM Proposed (q^em ∈ P) receiving CA MUST transition EM to None (q^em P → N). The CS→P transition triggered by attacks observation makes further embargo negotiation moot; the pending proposal is abandoned per VP-09-001. |
|
CSB-06-007 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and attacks not yet observed (q^cs ∈ ···p·a) and EM Proposed (q^em ∈ P) receiving CA MUST emit CK to acknowledge receipt. All C messages warrant acknowledgment per VP-12 messaging norms.* |
|
CSB-06-008 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and attacks not yet observed (q^cs ∈ ···p·a) and EM Active or Revise (q^em ∈ {A, R}) receiving CA MUST initiate embargo termination by emitting ET. VP-09-001 requires immediate embargo termination when public awareness occurs; constraint C-16 triggers CS→P on attacks observation. |
|
CSB-06-009 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and attacks not yet observed (q^cs ∈ ···p·a) and EM Active or Revise (q^em ∈ {A, R}) receiving CA MUST emit CK to acknowledge receipt. All C messages warrant acknowledgment per VP-12 messaging norms.* |
|
CSB-06-010 |
MUST | A Participant in a non-Closed RM state (q^rm ∉ C) with CS not yet public and attacks not yet observed (q^cs ∈ ···p·a) and EM Active or Revise (q^em ∈ {A, R}) receiving CA MUST record the CS transition to ···P·A before embargo teardown is initiated (constraint C-14). C-14 ordering requires the CS state change to be recorded first so that the canonical ledger correctly reflects cause before effect. |
Receive CE (CS Error)
Behaviors triggered when a participant receives a CE (CVD Case State Error) signal, which has no dedicated semantic dispatch value and is handled outside the standard AS2 pattern-matching pipeline.
Trigger: Message Received — CE
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-07-001 |
SHOULD | A Participant in a non-Closed RM state (q^rm ∉ C) receiving CE SHOULD emit CK to acknowledge receipt and SHOULD emit GI to inquire about the nature of the CS error. CE signals a protocol error in the CS process. Acknowledging with CK and following up with GI allows the sender to explain the error and enables recovery. CE does not trigger a CS state transition. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Emit CK to acknowledge receipt of CE → CK queued for sender 2. [receiver] Emit GI to inquire about the nature of the error → GI queued for sender Postconditions - CK and GI emitted; CS state unchanged |
Satisfies: MSM-03-007 — CE shorthand wire-type mapping |
Receive CK (CS Acknowledgment)
Behaviors triggered when a participant receives a CK (CVD Case State Acknowledgment) passthrough signal, which has no dedicated semantic dispatch value and requires no state transition.
Trigger: Message Received — CK
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-08-001 |
MAY | A Participant receiving CK MAY process it as a passthrough acknowledgment with no state transition and no required response. CK is a pure acknowledgment message. It confirms the sender received a prior C message. No state change or response is required from the receiver.* |
Satisfies: MSM-03-008 — CK shorthand wire-type mapping |
Enter CS Vendor Aware (V)
Trigger: State Entered — CS.V
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-09-001 |
SHOULD | A Vendor Participant entering CS Vendor Aware (q^cs ∈ Vfdpxa) with EM None (q^em ∈ N) SHOULD begin embargo negotiations promptly. VP-14-005 recommends starting the EM process as soon as possible once a Vendor is aware. Constraint C-32: EM SHOULD begin promptly on Vfdpxa entry when no embargo exists. ECA DetailsPreconditions - EM state is None; Participant holds the Vendor role; CS matches pattern Vfd... - EM state: NONE- Role: vendor- CS pattern: Vfd... |
Satisfies: VP-14-005 — Once a case has reached Vendor Aware, if the EM process has not started, it SHOULD begin as soon as possible |
CSB-09-002 |
SHOULD | A Participant entering CS Vendor Aware SHOULD emit CV to announce the transition to other case Participants. State-change notifications support shared situation awareness. CV informs other Participants that a Vendor is now aware, enabling them to coordinate embargo and fix timing accordingly. ECA DetailsPreconditions - CS matches pattern Vfd... - CS pattern: Vfd...Steps 1. [participant] Emit CV to announce Vendor Aware CS transition → CV queued for case participants Postconditions - CV emitted; other Participants informed of V transition |
Satisfies: VP-12-001 — CV messages SHOULD be sent only by Participants with direct knowledge of the notification |
Enter CS Fix Ready (F)
Trigger: State Entered — CS.F
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-10-001 |
SHOULD NOT | A Participant entering CS Fix Ready (q^cs ∈ VFd···) SHOULD NOT start new embargo negotiations. If an embargo proposal is pending (q^em ∈ P), the Participant SHOULD reject it. VP-14-007 discourages starting or completing new embargo negotiations once a fix is ready. Constraint C-34: fix readiness is late in the process for beginning embargo negotiations. ECA DetailsPreconditions - CS matches pattern VFd... - CS pattern: VFd... |
Satisfies: VP-14-007 — Once a case has reached Fix Ready, new embargo negotiations SHOULD NOT start and pending embargoes SHOULD be rejected |
CSB-10-002 |
SHOULD | A Participant entering CS Fix Ready SHOULD emit CF to announce the transition to other case Participants. State-change notifications support shared situation awareness. CF informs other Participants that a fix is available, enabling them to coordinate embargo extension or termination decisions. ECA DetailsPreconditions - CS matches pattern VFd... - CS pattern: VFd...Steps 1. [participant] Emit CF to announce Fix Ready CS transition → CF queued for case participants Postconditions - CF emitted; other Participants informed of F transition |
Enter CS Fix Deployed (D)
Trigger: State Entered — CS.D
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-11-001 |
SHOULD | A Participant entering CS Fix Deployed (q^cs ∈ VFD···) with an active embargo (q^em ∈ {A, R}) SHOULD terminate the embargo by emitting ET. VP-14-010 recommends terminating any existing embargo once the fix has been deployed. Constraint C-35: embargo termination SHOULD follow fix deployment. ECA DetailsPreconditions - EM state is Active or Revise; CS matches pattern VFD... - EM state: ACTIVE, REVISE- CS pattern: VFD...Steps 1. [participant] Emit ET to terminate the active embargo → ET queued for participants; EM begins transitioning to EXITED Postconditions - ET emitted; EM is EXITED |
Satisfies: VP-14-010 — By the time a fix has been deployed, any existing embargo SHOULD terminate |
CSB-11-002 |
SHOULD | A Participant entering CS Fix Deployed SHOULD emit CD to announce the transition to other case Participants. State-change notifications support shared situation awareness. CD informs other Participants that the fix is deployed, allowing them to coordinate publication and embargo termination decisions. ECA DetailsPreconditions - CS matches pattern VFD... - CS pattern: VFD...Steps 1. [participant] Emit CD to announce Fix Deployed CS transition → CD queued for case participants Postconditions - CD emitted; other Participants informed of D transition |
Enter CS Public Aware (P)
Trigger: State Entered — CS.P
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-12-001 |
MUST | A Participant entering CS Public Aware (q^cs → ···P··) with an active embargo (q^em ∈ {A, R}) SHALL terminate the embargo immediately by emitting ET. The CS state transition MUST be recorded before the embargo teardown is initiated (constraint C-14). VP-09-001 and VP-14-001 require immediate embargo termination upon public disclosure. Constraint C-14 requires observably correct ordering: the CS state change is recorded first, then the teardown begins. This ensures the canonical ledger correctly reflects cause (P transition) before effect (embargo termination). ECA DetailsPreconditions - EM state is Active or Revise; CS matches pattern ...P.. - EM state: ACTIVE, REVISE- CS pattern: ...P..Steps 1. [participant] Record CS Public Aware transition in local state → Receiver CS public bit set to P 2. [participant] Emit ET to terminate the embargo → ET queued for participants; EM begins transitioning to EXITED Postconditions - CS public bit is P; ET emitted; EM is EXITED; CS transition recorded before teardown (C-14) |
Satisfies: VP-09-001 — Embargoes SHALL terminate immediately when information becomes public Satisfies: VP-14-001 — Once Public Awareness, any existing embargo SHALL terminate |
CSB-12-002 |
MUST NOT | A Participant entering CS Public Aware MUST NOT seek new embargoes or accept new embargo proposals for this case. VP-14-001 prohibits seeking new embargoes once public awareness has occurred. This applies to all future EP messages for this case. ECA DetailsPreconditions - CS matches pattern ...P.. - CS pattern: ...P.. |
Satisfies: VP-14-001 — Once Public Awareness, new embargoes SHALL NOT be sought |
CSB-12-003 |
SHOULD | A Participant entering CS Public Aware SHOULD emit CP to announce the transition to other case Participants if CP was not already the triggering event (i.e., the transition was triggered locally or by a different message type). State-change notifications support shared situation awareness. CP informs other Participants that the vulnerability is now public. This item applies when the P transition is triggered by a local event (e.g., own publication) rather than by receiving CP from another Participant. ECA DetailsPreconditions - CS matches pattern ...P.. - CS pattern: ...P..Steps 1. [participant] Emit CP to announce Public Aware CS transition → CP queued for case participants Postconditions - CP emitted; other Participants informed of P transition |
Enter CS Exploit Public (X)
Trigger: State Entered — CS.X
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-13-001 |
MUST | A Participant entering CS in state ···px· (public not yet set, exploit now set) MUST also trigger CS→P (the pX→PX invariant) before or simultaneously with the exploit transition. This compound transition MUST be treated as atomic: both P and X bits are set together. The ephemeral state ···pX· (exploit public without public awareness) cannot exist as a stable state. Constraint C-15 requires the transition to be ···px· → ···PX·, not ···px· → ···pX· → ···PX·. Observable consequence: CX emitted from ···px· state must be accompanied by CP. The precondition uses the post-transition pattern ···pX· (X uppercase, P lowercase) since state_entered fires after the X bit is set. ECA DetailsPreconditions - CS matches pattern ...pX. - CS pattern: ...pX.Steps 1. [participant] Transition CS to ···PX· (set both public bit P and exploit bit X atomically) → Receiver CS public bit P and exploit bit X both set 2. [participant] Emit CX to announce exploit public transition → CX queued for case participants 3. [participant] Emit CP to announce public aware transition (C-15 constraint) → CP queued for case participants Postconditions - CS is ···PX·; CX and CP both emitted; pX→PX invariant satisfied |
|
CSB-13-002 |
MUST | A Participant entering CS Exploit Public (q^cs → ···PX·) with an active embargo (q^em ∈ {A, R}) SHALL terminate the embargo immediately by emitting ET. The CS state transition MUST be recorded before the embargo teardown is initiated (constraint C-14). VP-09-001 and VP-14-002 require immediate embargo termination upon exploit publication. The C-14 ordering applies: CS state recorded before teardown begins. ECA DetailsPreconditions - EM state is Active or Revise; CS matches pattern ...PX. - EM state: ACTIVE, REVISE- CS pattern: ...PX.Steps 1. [participant] Record CS Exploit Public transition in local state → Receiver CS exploit bit set to X 2. [participant] Emit ET to terminate the embargo → ET queued for participants; EM begins transitioning to EXITED Postconditions - CS exploit bit is X; ET emitted; EM is EXITED; CS transition recorded before teardown (C-14) |
Satisfies: VP-09-001 — Embargoes SHALL terminate immediately when exploit becomes public Satisfies: VP-14-002 — Once Exploit Publication, any existing embargo SHALL terminate |
CSB-13-003 |
MUST NOT | A Participant entering CS Exploit Public MUST NOT seek new embargoes or accept new embargo proposals for this case. VP-14-002 prohibits seeking new embargoes once exploit publication has occurred. ECA DetailsPreconditions - CS matches pattern ....X. - CS pattern: ....X. |
Satisfies: VP-14-002 — Once Exploit Publication, new embargoes SHALL NOT be sought |
Enter CS Attacks Observed (A)
Trigger: State Entered — CS.A
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-14-001 |
SHOULD | A Participant entering CS Attacks Observed (q^cs → ·····A) with an active embargo (q^em ∈ {A, R}) SHOULD terminate the embargo by emitting ET. The CS state transition SHOULD be recorded before the embargo teardown is initiated (constraint C-14). VP-09-003 and VP-14-013 recommend early embargo termination when attacks are observed. Unlike P and X, the attacks case is SHOULD (not SHALL) to allow for judgment in cases where other factors may apply. ECA DetailsPreconditions - EM state is Active or Revise; CS matches pattern .....A - EM state: ACTIVE, REVISE- CS pattern: .....ASteps 1. [participant] Record CS Attacks Observed transition in local state → Receiver CS attacks bit set to A 2. [participant] Emit ET to terminate the embargo → ET queued for participants; EM begins transitioning to EXITED Postconditions - CS attacks bit is A; ET emitted; EM is EXITED |
Satisfies: VP-09-003 — Embargoes SHOULD terminate early when there is evidence of active exploitation Satisfies: VP-11-006 — SHOULD terminate embargo when attacks are known to have occurred Satisfies: VP-14-013 — Once Attacks observed, any existing embargo SHOULD terminate |
CSB-14-002 |
SHOULD | A Participant entering CS Attacks Observed with CS not yet public (q^cs ∈ ···p·A) SHOULD also trigger CS→P and emit CP to announce public awareness. Constraint C-16: attacks observed on an otherwise non-public case SHOULD trigger the P transition. Active attacks constitute de-facto public awareness of the vulnerability's existence. ECA DetailsPreconditions - CS matches pattern ...p.A - CS pattern: ...p.ASteps 1. [participant] Transition CS to Public Aware and Attacks Observed (···P·A) → Receiver CS public bit P and attacks bit A both set 2. [participant] Emit CP to announce Public Aware transition → CP queued for case participants 3. [participant] Emit CA to announce Attacks Observed transition → CA queued for case participants Postconditions - CS is ···P·A; CP and CA emitted |
|
CSB-14-003 |
MUST NOT | A Participant entering CS Attacks Observed MUST NOT seek new embargoes for this case. VP-14-003 prohibits seeking new embargoes once attacks have been observed. ECA DetailsPreconditions - CS matches pattern .....A - CS pattern: .....A |
Satisfies: VP-14-003 — Once Attacks observed, new embargoes SHALL NOT be sought |
CSB-14-004 |
SHOULD | A Participant entering CS Attacks Observed SHOULD emit CA to announce the transition to other case Participants. State-change notifications support shared situation awareness. CA informs other Participants that active exploitation has been observed, enabling them to re-evaluate embargo and disclosure decisions. VP-12-002 recommends that fix development accelerate and embargo teardown begin. ECA DetailsPreconditions - CS matches pattern .....A - CS pattern: .....ASteps 1. [participant] Emit CA to announce Attacks Observed CS transition → CA queued for case participants Postconditions - CA emitted; other Participants informed of A transition |
Satisfies: VP-12-002 — Once attacks have been observed, fix development SHOULD accelerate and embargo teardown process SHOULD begin |
Trigger-Side VFD Role Preconditions
Precondition requirements for self-reported CS trigger endpoints (notify-fix-ready, notify-fix-deployed, notify-published). These complement the receive-side CSB-02, CSB-03, and CSB-04 groups by specifying which CVD roles may initiate each VFD transition locally and what state each trigger endpoint MUST set.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-15-001 |
MUST | An Actor self-reporting a fix-ready transition (f→F, vf.state=VF) via the trigger path MUST hold CVDRole.VENDOR in the case; the transition MUST be blocked (FAILURE) when CVDRole.VENDOR is absent. Under the structural VFD split (Concern #2595), this requirement is enforced at object-construction time: only VENDOR participants have a non-None vf dimension, making non-VENDOR actors structurally incapable of advancing the vendor path.Fix readiness (F event) represents that the actor has produced a fix. Only actors who develop or maintain the affected product (Vendors) can produce a fix; Deployers operate existing deployments and do not produce fixes. Finders, Reporters, and Coordinators likewise MUST NOT self-report fix readiness. The structural split (ParticipantStatus.vf is None for non-VENDOR actors) makes this a data-model invariant rather than a BT guard check. ECA DetailsPreconditions - Actor requests f→F (vf.state=VF) via the trigger path Steps 1. [trigger] Check that the requesting actor holds CVDRole.VENDOR in the case participant record (equivalently, that vf is non-None) → Role check passes for VENDOR; FAILURE returned otherwise 2. [trigger] If role check passes, advance vf dimension to vf.state=VF → ParticipantStatus updated; activity emitted to Case Manager Postconditions - Only VENDOR actors may record vf.state=VF; all others receive FAILURE |
|
CSB-15-002 |
MUST | An Actor self-reporting a fix-deployed transition (d→D, d.state=D) via the trigger path MUST hold CVDRole.DEPLOYER in the case; the transition MUST be blocked (FAILURE) when CVDRole.DEPLOYER is absent. Under the structural VFD split (Concern #2595), this requirement is enforced at object-construction time: only DEPLOYER participants have a non-None d dimension, making non-DEPLOYER actors structurally incapable of advancing the deployer path.Fix deployment (D event) represents applying a fix in one's own environment. Only actors who are responsible for operating deployed instances (Deployers) can perform this action. A shipping-product Vendor who does not control downstream deployment holds only CVDRole.VENDOR, not CVDRole.DEPLOYER, and has no d dimension. The structural split makes this a data-model invariant rather than a BT guard check.ECA DetailsPreconditions - Actor requests d→D (d.state=D) via the trigger path Steps 1. [trigger] Check that the requesting actor holds CVDRole.DEPLOYER in the case participant record (equivalently, that d is non-None) → Role check passes for DEPLOYER; FAILURE returned for VENDOR-only or any other non-DEPLOYER actor 2. [trigger] If role check passes, advance d dimension to d.state=D → ParticipantStatus updated; activity emitted to Case Manager Postconditions - Only DEPLOYER actors may record d.state=D; all others receive FAILURE |
|
CSB-15-003 |
MUST | The notify-published trigger endpoint MUST set only pxa_state=Pxa in the ParticipantStatus record. The D event (fix deployed) is participant-specific and distinct from the P event (public awareness); notify-published MUST NOT also advance the d dimension. Conflating P and D transitions (as notify-published did by setting both d.state=D and pxa_state=Pxa) causes non-deployer actors (Finders, Reporters, Coordinators) to incorrectly acquire deployment state when they announce public disclosure. Under the structural VFD split (Concern #2595), the d dimension is None for non-DEPLOYER participants, making this invariant structural rather than a guard check. The pxa (P event) is participant-agnostic; the D event is only valid for DEPLOYER-role actors who have separately completed deployment. ECA DetailsPreconditions - Actor calls the notify-published trigger endpoint Steps 1. [trigger] Set pxa_state=Pxa in the ParticipantStatus record → Public awareness recorded 2. [trigger] Leave vf and d dimensions unchanged (do not advance d.state=D) → vf and d reflect prior state; D event not triggered by P event Postconditions - ParticipantStatus records pxa_state=Pxa; vf and d dimensions are not modified by this trigger; actors wishing to report fix deployment must call notify-fix-deployed separately (and must hold CVDRole.DEPLOYER per CSB-15-002) |
|
CSB-15-004 |
MUST | A participant that holds the DEPLOYER role but not the VENDOR role MUST be permitted to make the d→D transition only when at least one participant holding the VENDOR role in the same case has reached F (fix ready). Such a participant has no vendor-fix (VF) state of its own and is at d before the transition; the transition moves it to D, recording deployment of a fix another participant produced. Under the structural VFD split (Concern #2595), a DEPLOYER-only participant has vf=None (structural absence) and d=DDimension (deployer path only). The causal gate — requiring at least one VENDOR participant to have vf.state=VF — captures the real-world dependency: a Deployer can only apply a fix that exists. The previous model required checking the DEPLOYER-only participant''s own vfd_state, which was permanently stuck at vfd because V and F transitions did not apply. The split resolves this cleanly: the deployer''s d dimension starts at CS_d.d and advances to CS_d.D when the causal gate passes; the vf dimension is structurally absent throughout. Note: this constraint is generic — "some VENDOR must be at VF" — because the system does not yet track which specific vendor a given deployer depends on. Finer-grained vendor-deployer dependency tracking is a separate Concern. ECA DetailsPreconditions - Actor holds CVDRole.DEPLOYER but not CVDRole.VENDOR in the case (vf=None, d=DDimension(state=CS_d.d)); at least one other participant holds CVDRole.VENDOR and has vf.state=VF Steps 1. [trigger] Check that the requesting actor holds CVDRole.DEPLOYER and not CVDRole.VENDOR (equivalently, d is non-None and vf is None); check that at least one participant in the case holds CVDRole.VENDOR and has vf.state=VF (causal gate) → All checks pass; d→D transition allowed 2. [trigger] Advance d dimension to d.state=D → d.state=D (prior state was CS_d.d); vf remains None; activity emitted to Case Manager Postconditions - DEPLOYER-only participant records d.state=D (vf=None throughout); at least one VENDOR participant has vf.state=VF in the same case |
Refines: CSB-15-002 — CSB-15-002 gates d→D on CVDRole.DEPLOYER; CSB-15-004 adds the causal precondition requiring at least one VENDOR to have vf.state=VF first |
CS Write-Boundary Transition Validation
Requirements for CS state write paths (VFD and PXA) to validate source→target transition validity before persisting a ParticipantStatus snapshot. Complements CSB-15 (trigger-side role preconditions) and SDO-02-002 (dimension object transition contract) by requiring that the BT write node itself is fail-closed regardless of upstream guard coverage.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-16-001 |
MUST | A BT node or helper that advances a participant''s vendor-path (vf) or deployer-path (d) dimension in a ParticipantStatus snapshot MUST validate that the (current → target) transition is permitted by the respective sub-machine (CS_vf for vendor path; CS_d for deployer path) before persisting. A same-state write is permitted (status confirmation). A backward move or illegal jump MUST cause the node to return Status.FAILURE without writing to the DataLayer. Upstream BT guards are necessary but insufficient: a guard that is too weak, missing, or bypassed allows an invalid state jump to reach the write node unchecked. Validating at the write boundary makes illegal transitions fail-closed regardless of guard coverage. Source: concern #1896, which surfaced when the CheckCSFixNotYetDeployed guard was too weak in issue #1825. Under the structural VFD split (Concern #2595), the validation applies independently to each dimension: is_valid_vf_transition() for the vendor path and is_valid_d_transition() for the deployer path. A node that writes both dimensions (VENDOR+DEPLOYER participants) must validate each independently. Scope: applies to local write nodes only. The received-wire path (add_participant_status_tree) uses a weaker monotonic-forward check intentionally — remote peers may advance through multiple steps between status messages; strict adjacency applies only to local writes. ECA DetailsPreconditions - A non-None target vf state (for VENDOR path) or d state (for DEPLOYER path) is passed to a ParticipantStatus write node Steps 1. [write_node] Read the participant''s current vf state (if VENDOR) or d state (if DEPLOYER) from the DataLayer → Current dimension state available; absent dimension is None (structural) 2. [write_node] For each non-None dimension: if target == current, proceed. If is_valid_vf_transition(current, target) [vendor] or is_valid_d_transition(current, target) [deployer] is True, proceed. Otherwise return Status.FAILURE. → Only valid transitions or same-state writes reach the DataLayer Postconditions - No ParticipantStatus with an illegal vf or d state jump is persisted |
Implements: SDO-02-004 Refines: BTND-10-001 |
CSB-16-002 |
MUST | A BT node or helper that writes a PXA state (CS_pxa) to a CaseStatus or ParticipantStatus snapshot MUST validate that the (current_pxa_state → target_pxa_state) transition is permitted by the PXA state machine before persisting. A same-state write is permitted. A backward move MUST cause the node to return Status.FAILURE without writing to the DataLayer. PXA state transitions are forward-only (p→P, x→X, a→A). An invalid backward PXA write would produce a misleading public-disclosure regression in the case record. The same fail-closed boundary that protects VFD applies equally to PXA. Source: concern #1896. ECA DetailsPreconditions - A non-None CS_pxa target state is passed to a ParticipantStatus write node Steps 1. [write_node] Read the participant's current PXA state → Current CS_pxa state available 2. [write_node] If target == current, proceed. If is_valid_pxa_transition(current, target) is True, proceed. Otherwise return Status.FAILURE. → Only valid transitions or same-state writes reach the DataLayer Postconditions - No ParticipantStatus with an illegal PXA regression is persisted |
Implements: SDO-02-004 Refines: BTND-10-001 |
CS Compound State and History Validity
CS-internal validity invariants over the compound (VFD x PXA) case state and over whole case histories. Where CSB-16 validates a single write at the persistence boundary, CSB-17 defines which compound states exist, which single-event transitions between them are causally possible, and which orderings of the six CS events a real case could have produced. Mined from the legacy vultron.core.case_states hypercube model and re-expressed against the current CS enums; see ADR-0060.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-17-001 |
MUST | An implementation MUST treat exactly 32 compound CS states as valid: the cross product of the 4 valid VFD states (vfd, Vfd, VFd, VFD) and the 8 PXA states. The VFD dimension is a linear chain, not a free product of three bits: readiness presupposes awareness and deployment presupposes readiness. Enumerating only the reachable combinations makes the two impossible combinations unrepresentable rather than merely rejected at runtime, which is why CS_vfd has four members and not eight.ECA DetailsPostconditions - The CS enum has exactly 32 members, one per valid compound state |
Refines: SM-09-002 — SM-09-002 states the vF/fD prohibition as a rejection rule; this refines it to a structural guarantee by enumerating only the 32 reachable combinations, so the impossible ones are unrepresentable. |
CSB-17-002 |
MUST | A CS transition MUST change exactly one of the six CS dimensions, and MUST change it from the not-yet-happened value to the happened value. CS events are irreversible and are observed one at a time; a compound state change of Hamming distance greater than one conflates two distinct events and loses their ordering, which is exactly the information history validity depends on. These two conditions alone admit 72 transitions; applying the ephemeral-state rule of CSB-17-003 leaves exactly 58 valid CS transitions. ECA DetailsPreconditions - A (source, destination) pair of compound CS states Steps 1. [validator] Identify the single dimension that differs between source and destination; reject if more than one differs. If none differs, the write is a same-state re-assertion, which CSB-16-001 and CSB-16-002 permit as a status confirmation: accept it only when the caller has opted in (the allow_null parameter), and reject it otherwise, because a transition validator's default answer for "no change" must be "that is not a transition". → A single CS event accounts for the change, or the caller has explicitly asked for same-state writes to pass 2. [validator] Delegate to the changed dimension's own transition table (is_valid_vfd_transition or is_valid_pxa_transition). → Monotonicity and VFD prerequisite ordering are enforced 3. [validator] Apply the ephemeral-state rule of CSB-17-003 to the source state. → A transition out of a vP or pX state fires only the event that state requires Postconditions - Only Hamming-distance-1 monotone transitions are accepted |
Constrains: CSB-16-001 — Adds the one-dimension-at-a-time and monotonicity constraints that a per-dimension write validator cannot see. The same-state write CSB-16 permits is preserved via the opt-in described in step 1. Constrains: CSB-16-002 Depends on: CSB-17-003 — The 58-transition count holds only with the ephemeral rule applied. |
CSB-17-003 |
MUST | An implementation MUST treat the compound states vP (public aware while the vendor is unaware) and pX (exploit public while the public is unaware) as ephemeral. Neither condition can persist in reality: public awareness reaches the vendor, and a published exploit makes the public aware. Modelling these as ephemeral rather than invalid preserves the causal ordering of the events while forbidding the case from progressing in any other direction first. The pX ephemeral rule is the pX to PX invariant of CSB-13-001 generalised from the entry cascade to every successor of a pX state. ECA DetailsPreconditions - The case is in one of the 12 ephemeral compound CS states Postconditions - Each ephemeral state has exactly one valid successor state |
Extends: CSB-13-001 Refines: SM-09-001 — SM-09-001 governs the persistence boundary: an incoming status write carrying vP or pX MUST be promoted to the forced form before it is stored. CSB-17-003 governs trajectory validation, where the same two conditions are modelled as ephemeral states with exactly one legal successor. The two are consistent because they apply at different boundaries: no vP or pX state is ever persisted, but such a state is still a legal intermediate point in a replayed history, and CSB-17-004 therefore permits a history prefix to end in one. |
CSB-17-004 |
SHOULD | A consumer validating a reported CS history SHOULD validate it causally by replaying the event sequence through CSB-17-002 and CSB-17-003 from the initial state vfdpxa, rather than checking states point-in-time or comparing wall-clock timestamps. A complete history contains all six CS events exactly once; there are exactly 70 valid complete histories out of 720 orderings. Validity of a case trajectory is a property of the whole ordered sequence, not of any single state: VFDPXA and VFDXAP visit only valid states, but only the first is a possible case history. Wall-clock timestamps cannot substitute, because they may be missing, skewed, or recorded by different participants. SHOULD rather than MUST because a consumer holding only a current state, with no event log to replay, cannot do better. ECA DetailsPreconditions - An ordered sequence of CS events, possibly incomplete Steps 1. [validator] Reject any event that occurs more than once in the sequence → Each CS event happens at most once 2. [validator] Replay the sequence from vfdpxa, requiring each step to satisfy CSB-17-002 and CSB-17-003 given every event that preceded it. → Causally impossible orderings are rejected Postconditions - A complete valid history terminates in VFDPXA |
|
CSB-17-005 |
MUST | An incomplete CS history MUST be validated as a prefix. Real cases are usually still in progress, so requiring six events would reject every live case. Ending in an ephemeral state is unavoidable at the prefix level: CSB-17-003's forced successor has not happened yet when the prefix is cut, which is different from persisting an ephemeral state — SM-09-001 still forbids that. Pinning acceptance to CSB-17-004's prefix closure keeps both directions honest: a validator that merely never rejects a genuine prefix could still accept dead ends. ECA DetailsPreconditions - An ordered sequence of CS events with fewer than six entries Steps 1. [validator] Apply CSB-17-004's replay to the sequence without requiring that it terminate in VFDPXA. → The prefix is accepted iff it is a prefix of at least one valid complete history Postconditions - A prefix ending in an ephemeral state is accepted, and its forced successor per CSB-17-003 is the only legal continuation |
Refines: CSB-17-004 Depends on: CSB-17-003 |
CSB-17-006 |
MUST | An implementation MUST NOT allow a fix to be ready while the vendor is unaware (the vF compound state is impossible). Fix readiness presupposes vendor awareness; the vF combination is structurally excluded from the 32 valid compound CS states. |
|
CSB-17-007 |
MUST | An implementation MUST NOT allow a fix to be deployed that was never ready (the fD compound state is impossible). Fix deployment presupposes fix readiness; the fD combination is structurally excluded from the 32 valid compound CS states. |
|
CSB-17-008 |
MUST | A CS transition that changes two or more dimensions at once, that reverses a dimension, or that skips a VFD prerequisite MUST be rejected. Each of the three prohibited forms conflates or reorders distinct causal events; rejecting them enforces the one-event-at-a-time and monotonicity invariants. |
|
CSB-17-009 |
MUST | From a vP state the next CS event MUST be V. The forced-successor rule is the operational consequence of CSB-17-003's ephemeral classification: each ephemeral state has exactly one valid continuation, enforcing the vP→VP invariant. |
|
CSB-17-012 |
MUST | From a pX state the next CS event MUST be P. The forced-successor rule for pX states enforces the pX→PX invariant; pX is ephemeral and P is the only valid continuation. |
Refines: CSB-17-003 |
CSB-17-013 |
MUST | Any CS event other than the forced successor MUST be rejected when the current state is ephemeral (vP or pX). Accepting any other event would break the vP→VP and pX→PX invariants that the forced-successor rule exists to enforce. |
Refines: CSB-17-003 |
CSB-17-010 |
MUST | Prefix validation MUST NOT require all six CS events to be present; a prefix MAY end in an ephemeral state. Real cases are still in progress at validation time; requiring all six events would reject every live case, and an ephemeral-ending prefix is valid as long as its forced successor has not yet been observed. |
|
CSB-17-011 |
MUST | Every accepted incomplete CS history prefix MUST have at least one valid completion among the 70 valid complete histories of CSB-17-004. Pinning acceptance to the prefix closure of CSB-17-004 prevents a validator from accepting dead-end prefixes that cannot lead to any valid complete history. |
Cross-Machine State Entailments
Invariants that span two state machines simultaneously. These are causal entailments, not local transition constraints: they capture states that are logically contradictory given the sequence of events that produced them. CSB-18-001 is enforced at emit time via ValidateTriggerTransitionsNode and on the receive path via composite_state_violations() (composite_state_invariants.py). CSB-18-002 through CSB-18-004 (PXA↔EM) are enforced on the receive path via ThreatTerminationBranchNode in add_participant_status_tree; violation_pxa_em_entailment() (composite_state_invariants.py) is the post-cascade diagnostic caller via PxaEmInvariantDiagnosticNode (CONCERN-3008, issue #3115).
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-18-001 |
MUST NOT | A participant's composite state MUST NOT have the VFD F-bit set (q^cs ∈ VFd··· or VFD···) while the participant's RM state is outside {ACCEPTED, DEFERRED, CLOSED}. A fix cannot be ready before the vulnerability has been accepted for action. The F-bit records that a vendor-fix is available. A vendor can only have a fix ready if they have accepted the report for action (RM ∈ {ACCEPTED, DEFERRED, CLOSED}). RM states RECEIVED/INVALID/VALID precede acceptance, so F-bit + pre-acceptance RM is a causal impossibility. The underlying constraint is a history property — rm_em_cs.md requires that the actor "passed through q^rm = Accepted at some point" — while a ParticipantStatus carries only the current RM value. The check therefore approximates it with RM_STATES_CONSISTENT_WITH_FIX, which is exactly the set of states reachable from RM.ACCEPTED. That is sound but not complete: DEFERRED and CLOSED are each also reachable without visiting ACCEPTED (VALID->DEFERRED, INVALID->CLOSED), so neither proves acceptance. They are included deliberately, because excluding them would refuse the legitimate batched update in which a peer advances through ACCEPTED and reports fix readiness in one message (CSB-16-001). Narrowing to {ACCEPTED} is the only complete alternative over a single snapshot and would refuse far more real traffic than it caught; a complete check needs the participant's RM history (ISSUE-3015). Implemented in composite_state_violations() (composite_state_invariants.py), which composes violation_rm_vf_entailment() and violation_rm_d_entailment() — the per-dimension form the rule takes after ADR-0075 split the compound CS_vfd into independent CS_vf and CS_d. Enforced on both protocol paths: the emit path refuses the whole trigger (ValidateTriggerTransitionsNode._validate_entailments) and the receive path refuses the whole received status (RSH-09-004, ISSUE-2906). Source: ISSUE-2372. ECA DetailsPreconditions - Any participant state snapshot is available for inspection |
Constrains: CSB-16-001 |
CSB-18-002 |
MUST NOT | A participant's composite state MUST NOT have the PXA P-bit set (q^cs ∈ ···P··) while an active embargo exists (q^em ∈ {ACTIVE, REVISE}). Public awareness of a vulnerability while an embargo is active is causally impossible in a well-functioning coordination — the P event causes embargo termination, so EM cannot remain ACTIVE once P has occurred. The P event (public disclosure) causes embargo termination per the protocol state machine. An embargo that persists after P has been recorded is a causal contradiction: the P event entails embargo termination, not mere transition ambiguity. This is a causal entailment, not a timing race — it is NOT enforced at emit time because the sequence of events is what makes it contradictory. Enforced on the receive path via ThreatTerminationBranchNode (add_participant_status_tree.py, RSH-03-001) when the authorization gate permits teardown. Diagnostic check: violation_pxa_em_entailment() (composite_state_invariants.py); post-cascade caller: PxaEmInvariantDiagnosticNode (CONCERN-3008, issue #3115). Source: ISSUE-2372. ECA DetailsPreconditions - A PXA state and an EM state are both available for the same case |
|
CSB-18-003 |
MUST NOT | A participant's composite state MUST NOT have the PXA X-bit set (q^cs ∈ ···X·) while an active embargo exists (q^em ∈ {ACTIVE, REVISE}). Public exploit availability while an embargo is active is causally impossible — the X event entails embargo termination. Same causal logic as CSB-18-002. The X event (exploit public) triggers embargo termination; EM cannot remain ACTIVE after X has been recorded. This is a causal entailment, not enforced at emit. Enforced on the receive path via ThreatTerminationBranchNode (RSH-03-002). Diagnostic check: violation_pxa_em_entailment() (composite_state_invariants.py); post-cascade caller: PxaEmInvariantDiagnosticNode (CONCERN-3008, issue #3115). ECA DetailsPreconditions - A PXA state and an EM state are both available for the same case |
Refines: CSB-18-002 |
CSB-18-004 |
MUST NOT | A participant's composite state MUST NOT have the PXA A-bit set (q^cs ∈ ···A) while an active embargo exists (q^em ∈ {ACTIVE, REVISE}). Observed attacks while an embargo is active is causally impossible — the A event entails embargo termination. Same causal logic as CSB-18-002 and CSB-18-003. The A event (attacks observed) triggers embargo termination; EM cannot remain ACTIVE after A has been recorded. This is a causal entailment, not enforced at emit. Enforced on the receive path via ThreatTerminationBranchNode (RSH-03-003). Diagnostic check: violation_pxa_em_entailment() (composite_state_invariants.py); post-cascade caller: PxaEmInvariantDiagnosticNode (CONCERN-3008, issue #3115). ECA DetailsPreconditions - A PXA state and an EM state are both available for the same case |
Refines: CSB-18-002 |
CS Ordering Invariants Under Out-of-Order Ledger Delivery
Normative requirements for how CS ordering invariants interact with the ADR-0037 out-of-order ledger hold. Clarifies when invariant guards are evaluated relative to the hold and the drain.
| ID | Priority | Requirement | Related |
|---|---|---|---|
CSB-19-001 |
MUST | CS ordering invariants (ephemeral-state guard, history-prefix validity, CP-before-ET ordering) MUST be evaluated at drain time — that is, against the causal-order replay of the held Announces (ADR-0037, SYNC-14-009) — not at the time of original out-of-order receipt. An out-of-order Announce(CaseLedgerEntry) carrying a CS update is held by BufferOutOfOrderEntryNode before any domain effects are applied; the CS state machine is not touched at hold time. When the gap fills, the drain replays each held Announce through the full announce receive BT in log_index order (ADR-0037, CLP-14-001), so CS guards see the entries in correct causal sequence. Evaluating guards at original receipt would produce false violations whenever the predecessor CS entry has not yet arrived. |
Depends on: CSB-12-001 |
CSB-19-002 |
MUST NOT | A held out-of-order CS ledger entry MUST NOT trigger an ephemeral-state violation or history-prefix validation failure at the time it is held. Such checks MUST be deferred until the entry is replayed in causal order during the drain. An ephemeral-state entry (e.g., a pX-state CaseStatus arriving before its P-state predecessor) is structurally valid in isolation; the violation would only be real if the predecessor never arrives. Flagging it at hold time produces a false positive and may cause the entry to be incorrectly rejected, breaking convergence. |
Refines: CSB-19-001 |
CSB-19-003 |
MUST | The CP-before-ET ordering constraint (CSB-12-001: CS→P MUST be recorded before ET is emitted) is preserved under out-of-order ledger delivery because the CASE_MANAGER commits entries in causal order (CLP-14-001). The P-state ledger entry always carries a lower log_index than the ET entry that follows it; the drain therefore always processes P before ET when both are held. CSB-12-001 is enforced at the CASE_MANAGER write-side: the CASE_MANAGER commits the P-state entry before committing the ET entry. This means the P-state entry's log_index is strictly lower than the ET entry's log_index. The ADR-0037 drain replays in log_index order, so the constraint cannot be violated by reordered delivery alone. No receiver-side re-enforcement of the temporal constraint is required; the causal order in the ledger is sufficient. |
Refines: CSB-19-001 Depends on: CSB-12-001 Depends on: CLP-14-001 |
Duration
Requirements for a canonical representation of time durations independent of start/end timestamps. Applicable wherever a duration must be exchanged in JSON, such as embargo policy records and embargo initialization at case creation.
File ID: DUR
Representation Format
Allowed Grammar (Restricted ISO 8601 Subset)
Canonical Form
Validation Requirements
Python / Pydantic v2 Mapping
| ID | Priority | Requirement | Related |
|---|---|---|---|
DUR-05-001 |
SHOULD | Duration fields SHOULD use datetime.timedelta as the internal Python representation |
Interval Composition
Embargo Duration Semantics
| ID | Priority | Requirement | Related |
|---|---|---|---|
DUR-07-001 |
MUST | Specific embargo end times on cases MUST be declared as event intervals with unambiguous start and end timestamps, not as durations alone | |
DUR-07-002 |
SHOULD | At case creation (RM.RECEIVED), if no embargo end time is established, the case creation process SHOULD apply the actor's default embargo duration (from the actor profile) to the case creation timestamp to establish the initial embargo end time | |
DUR-07-003 |
MUST | When a default embargo duration is applied at case creation, this application MUST be logged at INFO level to ensure visibility in logs. The resulting CaseStatus.em.state MUST be EM.ACTIVE (not EM.PROPOSED); see specs/embargo-policy.yaml EP-04-001. |
|
DUR-07-004 |
MUST | An embargo end time MUST be established before the case transitions to RM.VALID | Depends on: DUR-07-002 |
Embargo Management Behavioral Requirements
Event-Condition-Action behavioral requirements for Embargo Management (EM). Each group is triggered by either a received EM message or a local EM state transition, and specifies typed preconditions, ordered steps, and postconditions that implementations must satisfy.
File ID: EMB
Receive EP (Embargo Proposed)
Behaviors triggered when a participant receives an EP (Embargo Proposed) message, transitioning EM state to Proposed and acknowledging receipt. Written in the voice of the formal protocol, in which every Participant is a peer. In the CASE_MANAGER topology (PCR-08-001) a participant addresses its proposal to the CASE_MANAGER alone, so the Participant receiving EP is the CASE_MANAGER on receipt; every other participant reaches the same EM state through ledger replay, and is asked for its answer by the Invite the CASE_MANAGER relays to it. "Emit EK" is discharged by the CASE_MANAGER's commit and announcement of the received activity; no EK message exists (EP-09-009). See EP-09 and ADR-0113 as amended.
Trigger: Message Received — EP
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-01-001 |
MUST | A Participant in EM None (q^em ∈ N) with CS not yet public/exploit/attack (q^cs ∈ ···pxa) receiving EP MUST transition their EM state to Proposed (q^em N → P) and emit EK to acknowledge receipt. EP is the EM message that initiates embargo negotiation. When no embargo exists and the case is still pre-public, the receiver transitions to Proposed to signal they are evaluating the proposal. ECA DetailsPreconditions - EM state is None; CS matches pattern ...pxa - EM state: NONE- CS pattern: ...pxaSteps 1. [receiver] Transition EM state from NONE to PROPOSED → Receiver EM state is PROPOSED 2. [receiver] Emit EK to acknowledge receipt of EP → EK queued for sender Postconditions - Receiver EM state is PROPOSED; EK emitted to sender |
Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies*: MSM-02-001 — EP shorthand wire-type mapping |
EMB-01-002 |
MUST NOT | A Participant MUST NOT accept a new embargo proposal (process EP) when information about the vulnerability is public (q^cs ∈ ···P··), exploit is public (q^cs ∈ ····X·), or attacks have been observed (q^cs ∈ ·····A). The Participant MUST respond with ER to reject the proposal. The ER duty binds only the proposal's addressee: a receiver the proposal is not addressed to (HP-01-012) holds an unaddressed copy, refuses it (HP-01-005) and answers nothing (ADR-0118); cc addresses nobody (ADR-0109). The ER names the Invite by id and needs no terms, so an Invite that names its terms by URI only is answered too (ADR-0123). A re-delivery of an Invite already answered with ER is a duplicate: it is skipped and queues no second ER (HP-01-003). An ER recorded as sent but no longer pending is queued again under its own id, so a failed queue write does not lose it (ID-04-005).Embargo negotiation is not viable once public disclosure has occurred. VP-06-001 and VP-11-001 prohibit new embargo negotiations in these states. ER signals the rejection to the proposer so they can update their own EM state. ECA DetailsPreconditions - CS matches pattern ...P.. - CS pattern: ...P..- CS matches pattern ....X. - CS pattern: ....X.- CS matches pattern .....A - CS pattern: .....ASteps 1. [receiver] Emit ER to reject the embargo proposal → ER queued for sender; receiver EM state returns to NONE if previously PROPOSED Postconditions - ER emitted; no EM state transition to PROPOSED |
Satisfies: VP-06-001 — MUST NOT propose or accept embargo when public/exploit/attacks Satisfies: VP-11-001 — SHALL NOT negotiate embargoes where vulnerability or exploit is public or attacks occurred Satisfies: MSM-02-001 — EP shorthand wire-type mapping |
EMB-01-003 |
MUST | A Participant already in EM Proposed (q^em ∈ P) with CS not yet public/exploit/attack (q^cs ∈ ···pxa) receiving a second or counter EP MUST emit EK to acknowledge and MAY evaluate the counter-proposal terms without an additional state transition. Multiple EP messages can arrive during negotiation. When already in Proposed, no additional state transition is needed. The receiver evaluates the counter-proposal and may accept, counter, or reject via EA, EV, or ER. ECA DetailsPreconditions - EM state is Proposed; CS matches pattern ...pxa - EM state: PROPOSED- CS pattern: ...pxaSteps 1. [receiver] Emit EK to acknowledge receipt of counter-proposal EP → EK queued for sender; EM state remains PROPOSED Postconditions - EK emitted; EM state remains PROPOSED; counter-proposal terms available for evaluation |
Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies*: MSM-02-001 — EP shorthand wire-type mapping |
EMB-01-004 |
SHOULD NOT | A Participant in RM Invalid (q^rm ∈ I) SHOULD NOT propose or accept an embargo when receiving EP. VP-13-003 discourages starting embargo management from an Invalid RM state. The report has been assessed as invalid; committing to embargo terms under those conditions is premature and likely to be wasted effort. ECA DetailsPreconditions - Participant is in RM Invalid - RM state: INVALID |
Satisfies: VP-13-003 — SHOULD NOT begin EM with a proposal from a Participant in RM Invalid Satisfies: MSM-02-001 — EP shorthand wire-type mapping |
EMB-01-005 |
SHOULD NOT | A Participant in an inactive RM state (q^rm ∈ {D, C}) SHOULD NOT begin embargo management by accepting an incoming EP. VP-13-002 discourages starting embargo management in inactive RM states. Deferred or Closed cases indicate the participant has deprioritized or completed their engagement; starting embargo negotiation in these states is unlikely to lead to productive coordinated disclosure. ECA DetailsPreconditions - Participant is in RM Deferred/Closed - RM state: DEFERRED, CLOSED |
Satisfies: VP-13-002 — SHOULD NOT begin EM in an inactive RM state Satisfies: MSM-02-001 — EP shorthand wire-type mapping |
Receive EA (Embargo Accepted)
Behaviors triggered when a participant receives an EA (Embargo Accepted) message, transitioning EM state to Active and acknowledging receipt.
Trigger: Message Received — EA
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-02-001 |
MUST | A Participant in EM Proposed (q^em ∈ P) with CS not yet public/exploit/attack (q^cs ∈ ···pxa) receiving EA MUST transition their EM state to Active (q^em P → A) when the accepted proposal was the last one open, MUST move to Revise (q^em P → R) while another proposal stays open (EMB-18-005), and MUST emit EK to acknowledge receipt. EA is the message that moves a negotiated embargo from Proposed to Active. The receiver records that the embargo is now in effect. Any other open proposal stays open as a revision of it (ADR-0130). ECA DetailsPreconditions - EM state is Proposed; CS matches pattern ...pxa - EM state: PROPOSED- CS pattern: ...pxaSteps 1. [receiver] Transition EM state from PROPOSED to ACTIVE → Receiver EM state is ACTIVE 2. [receiver] Emit EK to acknowledge receipt of EA → EK queued for sender Postconditions - Receiver EM state is ACTIVE, or REVISE while another proposal is open; EK emitted to sender |
Satisfies: VP-05-003 — An embargo SHALL begin at the moment it is accepted Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies*: MSM-02-002 — EA shorthand wire-type mapping |
EMB-02-002 |
MUST NOT | A Participant MUST NOT process EA to transition EM to Active when the case is already public (q^cs ∈ ···P··), exploit is public (q^cs ∈ ····X·), or attacks have been observed (q^cs ∈ ·····A). The Participant MUST instead emit ER to signal the embargo is not viable. Where the case's EM is moved only by the case owner's Accept(EmbargoEvent, target=Case) (MSM-07-008), the CASE_MANAGER refuses that activation before committing it to the case ledger, and sends no ER for it.VP-06-001 prohibits accepting embargoes once public disclosure has occurred. In the CASE_MANAGER topology an ER is a Reject(Invite(EmbargoEvent)), which records its sender's own consent (MSM-07-004, ADR-0122); sent by the CASE_MANAGER in answer to the owner's activation it would record the manager's refusal, not the owner's, so the activation is refused instead. VP-13-004 adds that EM SHOULD NOT proceed from Proposed to Active when RM is Invalid or Closed, but the public/exploit/attack constraint is absolute.ECA DetailsPreconditions - CS matches pattern ...P.. - CS pattern: ...P..- CS matches pattern ....X. - CS pattern: ....X.- CS matches pattern .....A - CS pattern: .....ASteps 1. [receiver] Emit ER to reject the acceptance (embargo not viable) → ER queued for sender; EM state transitions toward NONE Postconditions - ER emitted; EM state not transitioned to ACTIVE |
Satisfies: VP-06-001 — MUST NOT accept a new embargo when public/exploit/attacks Satisfies: VP-11-001 — SHALL NOT negotiate embargoes where vulnerability or exploit is public Satisfies: MSM-02-002 — EA shorthand wire-type mapping |
EMB-02-003 |
SHOULD NOT | A Participant in RM Invalid or Closed (q^rm ∈ {I, C}) SHOULD NOT allow EM to proceed from Proposed to Active upon receiving EA. VP-13-004 discourages proceeding to Active when the report has been assessed as invalid or closed. The embargo would apply to a case the participant is not actively working. ECA DetailsPreconditions - Participant is in RM Invalid/Closed - RM state: INVALID, CLOSED |
Satisfies: VP-13-004 — SHOULD NOT proceed from EM Proposed to EM Accepted when RM is Invalid or Closed Satisfies: MSM-02-002 — EA shorthand wire-type mapping |
Receive EV (Embargo Revision Proposed)
Behaviors triggered when a participant receives an EV (Embargo Revision Proposed) message while an embargo is active, transitioning EM state to Revise and acknowledging receipt. Written in the voice of the formal protocol, in which every Participant is a peer. In the CASE_MANAGER topology (PCR-08-001) a participant addresses its proposal to the CASE_MANAGER alone, so the Participant receiving EV is the CASE_MANAGER on receipt; every other participant reaches the same EM state through ledger replay, and is asked for its answer by the Invite the CASE_MANAGER relays to it. "Emit EK" is discharged by the CASE_MANAGER's commit and announcement of the received activity; no EK message exists (EP-09-009). See EP-09 and ADR-0113.
Trigger: Message Received — EV
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-03-001 |
MUST | A Participant in EM Active (q^em ∈ A) with CS not yet public/exploit/attack (q^cs ∈ ···pxa) receiving EV MUST transition their EM state to Revise (q^em A → R). "Emit EK" is discharged by the CASE_MANAGER's commit of the received activity, fanned out as Announce(CaseLedgerEntry); no EK message is emitted (EP-09-009, MSM-02-009).EV signals a proposal to revise an active embargo's terms. The receiver enters Revise state to indicate a pending revision. The original embargo remains in force during revision per VP-04-005. ECA DetailsPreconditions - EM state is Active; CS matches pattern ...pxa - EM state: ACTIVE- CS pattern: ...pxaSteps 1. [receiver] Transition EM state from ACTIVE to REVISE → Receiver EM state is REVISE 2. [receiver] CASE_MANAGER commits the received EV and announces the ledger entry (discharges EK) → Ledger entry committed and fanned out; no EK message exists Postconditions - Receiver EM state is REVISE; original embargo remains in force; EK discharged by the ledger commit |
Satisfies: VP-04-005 — Once accepted, revisions MAY be proposed Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies*: MSM-02-003 — EV shorthand wire-type mapping |
EMB-03-002 |
MUST | A Participant already in EM Revise (q^em ∈ R) with CS not yet public/exploit/attack (q^cs ∈ ···pxa) receiving a counter EV (counter-revision) MUST emit EK to acknowledge without an additional state transition. Multiple revision proposals can arrive. When already in Revise, no further state transition is needed; the receiver evaluates the counter-revision and responds with EJ or EC. ECA DetailsPreconditions - EM state is Revise; CS matches pattern ...pxa - EM state: REVISE- CS pattern: ...pxaSteps 1. [receiver] Emit EK to acknowledge counter-revision EV → EK queued for sender; EM state remains REVISE Postconditions - EK emitted; EM state remains REVISE |
Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies*: MSM-02-003 — EV shorthand wire-type mapping |
EMB-03-003 |
MUST | A Participant that is neither the case owner nor the CASE_MANAGER, in EM Active or Revise (q^em ∈ {A, R}) with CS in a public/exploit/attack state (q^cs ∉ ···pxa), receiving EV MUST answer it with ER to the CASE_MANAGER, as EMB-01-002 requires for a proposal, and MUST NOT adopt the proposal, emit ET or change its own EM state. Termination is initiated only by the case owner, or by the CASE_MANAGER when delegated (EP-09-003, EP-09-008, CM-24, ADR-0113, ADR-0118), and it is the public, exploit or attack fact that causes it, never a revision proposal. Revision negotiation is not viable once public disclosure has occurred, so the participant refuses the revision. A proposal is at most the occasion on which a participant notices the case is public; making it the cause would tie termination to someone proposing terms. Ending the embargo is the case owner's decision (or the delegated CASE_MANAGER's), and only the CASE_MANAGER writes shared EM state (EP-09-008, ADR-0113); a participant answers only the Invite addressed to it (EP-09-003). VP-11-001 and VP-11-002 are met by the owner's or CASE_MANAGER's termination, which in the CASE_MANAGER topology is the CS public-event cascade, not by each participant emitting ET (ADR-0118). ECA DetailsPreconditions - EM state is Active or Revise; CS matches pattern ...P.. - EM state: ACTIVE, REVISE- CS pattern: ...P..- EM state is Active or Revise; CS matches pattern ....X. - EM state: ACTIVE, REVISE- CS pattern: ....X.- EM state is Active or Revise; CS matches pattern .....A - EM state: ACTIVE, REVISE- CS pattern: .....ASteps 1. [receiver] Emit ER to the CASE_MANAGER to reject the revision → ER queued for the CASE_MANAGER; no ET is emitted; the receiver's EM state is unchanged Postconditions - ER queued for the CASE_MANAGER; no revision proposal adopted; the receiver emits no ET and its EM state is unchanged |
Satisfies: VP-11-002 — SHALL terminate embargo when public/exploit Satisfies: MSM-02-003 — EV shorthand wire-type mapping |
Receive EJ (Embargo Revision Rejected)
Behaviors triggered when a participant receives an EJ (Embargo Revision Rejected) message, reverting EM state from Revise to Active and acknowledging receipt.
Trigger: Message Received — EJ
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-04-001 |
MUST | A Participant in EM Revise (q^em ∈ R) with CS not yet public/exploit/attack (q^cs ∈ ···pxa) receiving EJ MUST transition their EM state back to Active (q^em R → A) when the rejected revision was the last proposal open, and MUST remain in Revise (q^em R → R) while another proposal is open (EMB-18-005). "Emit EK" is discharged by the CASE_MANAGER's commit of the received activity, fanned out as Announce(CaseLedgerEntry); no EK message is emitted (EP-09-009, MSM-02-009).EJ signals that a revision proposal has been rejected and the embargo reverts to its previous Active terms. The receiver restores the pre-revision state. ECA DetailsPreconditions - EM state is Revise; CS matches pattern ...pxa - EM state: REVISE- CS pattern: ...pxaSteps 1. [receiver] Transition EM state from REVISE back to ACTIVE → Receiver EM state is ACTIVE 2. [receiver] CASE_MANAGER commits the received EJ and announces the ledger entry (discharges EK) → Ledger entry committed and fanned out; no EK message exists Postconditions - Receiver EM state is ACTIVE (revision rejected), or REVISE while another proposal is open; EK discharged by the ledger commit |
Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies*: MSM-02-004 — EJ shorthand wire-type mapping |
EMB-04-002 |
MUST | A Participant in EM Revise (q^em ∈ R) with CS in a public/exploit/attack state (q^cs ∉ ···pxa) receiving EJ MUST emit ET to terminate the embargo immediately rather than reverting to Active. Reverting to Active is not viable once public disclosure has occurred. VP-11-002 requires termination in these states. "Rather than reverting" describes where the act ends: EM ends EXITED, never back in Active. Inside the act, the CASE_MANAGER records the rejection as decided (the revision REJECTED) and then terminates the embargo, as two entries a replica copies in order (MSM-07-009, CM-23-016). ECA DetailsPreconditions - EM state is Revise; CS matches pattern ...P.. - EM state: REVISE- CS pattern: ...P..- EM state is Revise; CS matches pattern ....X. - EM state: REVISE- CS pattern: ....X.- EM state is Revise; CS matches pattern .....A - EM state: REVISE- CS pattern: .....ASteps 1. [receiver] Emit ET to terminate the embargo immediately → ET queued for participants; EM transitions to EXITED Postconditions - ET emitted; EM state is EXITED |
Satisfies: VP-11-002 — SHALL terminate embargo when public/exploit Satisfies: MSM-02-004 — EJ shorthand wire-type mapping |
Receive EC (Embargo Revision Accepted)
Behaviors triggered when a participant receives an EC (Embargo Revision Accepted) message, advancing EM state from Revise back to Active with updated terms and acknowledging receipt.
Trigger: Message Received — EC
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-05-001 |
MUST | A Participant in EM Revise (q^em ∈ R) with CS not yet public/exploit/attack (q^cs ∈ ···pxa) receiving EC MUST transition their EM state back to Active (q^em R → A) with the revised terms when the accepted revision was the last proposal open, and MUST remain in Revise (q^em R → R) under the revised terms while another proposal is open (EMB-18-005). "Emit EK" is discharged by the CASE_MANAGER's commit of the case owner's Accept(EmbargoEvent, target=Case), fanned out as Announce(CaseLedgerEntry); no EK message is emitted (EP-09-009, MSM-02-009).EC signals that a revision proposal has been accepted. The embargo returns to Active state under the new terms agreed by the revision. ECA DetailsPreconditions - EM state is Revise; CS matches pattern ...pxa - EM state: REVISE- CS pattern: ...pxaSteps 1. [receiver] Transition EM state from REVISE to ACTIVE with revised embargo terms → Receiver EM state is ACTIVE; revised terms recorded 2. [receiver] CASE_MANAGER commits the case owner's Accept(EmbargoEvent, target=Case) and announces the ledger entry (discharges EK) → Ledger entry committed and fanned out; no EK message existsPostconditions - Receiver EM state is ACTIVE with new terms, or REVISE while another proposal is open; EK discharged by the ledger commit |
Satisfies: VP-04-005 — Once accepted, revisions MAY be accepted Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies*: MSM-02-005 — EC shorthand wire-type mapping |
EMB-05-002 |
MUST | A Participant in EM Revise (q^em ∈ R) with CS in a public/exploit/attack state (q^cs ∉ ···pxa) receiving EC MUST emit ET to terminate the embargo rather than accepting the revision. Accepting a revision is not viable once public disclosure has occurred. VP-11-002 requires termination in these states. ECA DetailsPreconditions - EM state is Revise; CS matches pattern ...P.. - EM state: REVISE- CS pattern: ...P..- EM state is Revise; CS matches pattern ....X. - EM state: REVISE- CS pattern: ....X.- EM state is Revise; CS matches pattern .....A - EM state: REVISE- CS pattern: .....ASteps 1. [receiver] Emit ET to terminate the embargo immediately → ET queued for participants; EM transitions to EXITED Postconditions - ET emitted; EM state is EXITED |
Satisfies: VP-11-002 — SHALL terminate embargo when public/exploit Satisfies: MSM-02-005 — EC shorthand wire-type mapping |
Receive ER (Embargo Rejected)
Behaviors triggered when a participant receives an ER (Embargo Rejected) message, transitioning EM state from Proposed to None and acknowledging receipt.
Trigger: Message Received — ER
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-06-001 |
MUST | A Participant in EM Proposed (q^em ∈ P) receiving ER MUST transition their EM state back to None (q^em P → N) when the rejected proposal was the last one open, MUST remain in Proposed (q^em P → P) while another proposal is open (EMB-18-005), and MUST emit EK to acknowledge receipt. ER signals that a proposed embargo has been rejected. When it was the last open proposal, the case returns to None (no embargo in effect); another open proposal keeps it in Proposed (ADR-0130). The EK acknowledges the rejection. ECA DetailsPreconditions - EM state is Proposed - EM state: PROPOSEDSteps 1. [receiver] Transition EM state from PROPOSED to NONE → Receiver EM state is NONE 2. [receiver] Emit EK to acknowledge receipt of ER → EK queued for sender Postconditions - Receiver EM state is NONE (proposal rejected), or PROPOSED while another proposal is open; EK emitted |
Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies: VP-05-017 — Participants MAY reject proposed embargo terms for any reason Satisfies*: MSM-02-006 — ER shorthand wire-type mapping |
Receive ET (Embargo Terminated)
Behaviors triggered when a participant receives an ET (Embargo Terminated) message, transitioning EM state to Terminated and acknowledging receipt.
Trigger: Message Received — ET
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-07-001 |
MUST | A Participant in EM Active (q^em ∈ A) receiving ET MUST transition their EM state to Exited (q^em A → X) and emit EK to acknowledge receipt. ET is the message that terminates an active embargo. The receiver records that the embargo has ended. ET has immediate effect regardless of other pending messages. ECA DetailsPreconditions - EM state is Active - EM state: ACTIVESteps 1. [receiver] Transition EM state from ACTIVE to EXITED → Receiver EM state is EXITED 2. [receiver] Emit EK to acknowledge receipt of ET → EK queued for sender Postconditions - Receiver EM state is EXITED; EK emitted |
Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies*: MSM-02-007 — ET shorthand wire-type mapping |
EMB-07-002 |
MUST | A Participant in EM Revise (q^em ∈ R) receiving ET MUST transition their EM state to Exited (q^em R → X) and emit EK to acknowledge receipt. ET terminates the embargo regardless of whether a revision is pending. The revision proposal is abandoned upon termination. ECA DetailsPreconditions - EM state is Revise - EM state: REVISESteps 1. [receiver] Transition EM state from REVISE to EXITED → Receiver EM state is EXITED 2. [receiver] Emit EK to acknowledge receipt of ET → EK queued for sender Postconditions - Receiver EM state is EXITED; pending revision abandoned; EK emitted |
Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies*: MSM-02-007 — ET shorthand wire-type mapping |
EMB-07-003 |
MUST | A Participant already in EM Exited (q^em ∈ X) receiving ET MUST emit EK without any state transition. Exited is already the terminal state. The EK acknowledges the duplicate termination message; no additional transition is needed. ECA DetailsPreconditions - EM state is Exited - EM state: EXITEDSteps 1. [receiver] Emit EK to acknowledge receipt of ET (no state change) → EK queued for sender; EM state remains EXITED Postconditions - EK emitted; EM state remains EXITED |
Satisfies: VP-11-004 — SHOULD send EK in acknowledgment of any E message except EK itself Satisfies*: MSM-02-007 — ET shorthand wire-type mapping |
Receive EE (Embargo Error)
Behaviors triggered when a participant receives an EE (Embargo Management Error) signal, which has no dedicated semantic dispatch value and is handled outside the standard AS2 pattern-matching pipeline.
Trigger: Message Received — EE
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-08-001 |
SHOULD | A Participant receiving EE SHOULD emit EK to acknowledge receipt and SHOULD emit GI to inquire about the nature of the error. EE signals a protocol error in the EM process. Acknowledging with EK and following up with GI allows the sender to explain the error and enables recovery. EE does not trigger an EM state transition. ECA DetailsSteps 1. [receiver] Emit EK to acknowledge receipt of EE → EK queued for sender 2. [receiver] Emit GI to inquire about the nature of the error → GI queued for sender Postconditions - EK and GI emitted; EM state unchanged |
Satisfies: VP-11-005 — SHOULD acknowledge (EK) and inquire (GI) about the nature of any error reported by EE Satisfies: MSM-02-008 — EE shorthand wire-type mapping |
Receive EK (Embargo Acknowledgment)
Behaviors triggered when a participant receives an EK (Embargo Management Acknowledgment) passthrough signal, which has no dedicated semantic dispatch value and requires no state transition.
Trigger: Message Received — EK
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-09-001 |
MAY | A Participant receiving EK MAY process it as a passthrough acknowledgment with no state transition and no required response. EK is a pure acknowledgment message. It confirms the sender received a prior E message. No state change or response is required from the receiver.* |
Satisfies: VP-11-004 — EK is the acknowledgment; no acknowledgment of acknowledgment required Satisfies: MSM-02-009 — EK shorthand wire-type mapping |
Enter EM Proposed
Trigger: State Entered — EM.PROPOSED
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-10-001 |
MAY | A Participant entering EM Proposed MAY evaluate the proposed terms and respond with EA (accept), EV (counter-propose a revision), or ER (reject). EM Proposed marks that embargo negotiation is underway. The participant must evaluate the proposal and eventually accept, counter, or reject to move the EM process forward. ECA DetailsPreconditions - EM state is Proposed - EM state: PROPOSED |
Satisfies: VP-04-004 — Once proposed, an embargo MAY be accepted or rejected Satisfies: VP-06-003 — Receivers SHOULD accept any embargo proposed by Reporters Satisfies: VP-06-010 — Participants MAY accept or reject any proposed embargo as they see fit |
EMB-10-002 |
SHOULD NOT | A Participant in RM Invalid or Deferred or Closed (q^rm ∈ {I, D, C}) SHOULD NOT begin or continue embargo negotiations while in those inactive RM states. VP-13-002 and VP-13-003 discourage starting EM in inactive RM states. An embargo proposed while a report is invalid or deferred is unlikely to result in productive coordination. ECA DetailsPreconditions - Participant is in RM Invalid/Deferred/Closed - RM state: INVALID, DEFERRED, CLOSED |
Satisfies: VP-13-002 — SHOULD NOT begin EM in an inactive RM state Satisfies: VP-13-003 — SHOULD NOT begin EM with a proposal from a Participant in RM Invalid |
Enter EM Active
Trigger: State Entered — EM.ACTIVE
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-11-001 |
SHOULD NOT | A Participant in EM Active SHOULD NOT share the vulnerability report with a newly invited Participant until that Participant has accepted the existing embargo terms. VP-08-006 prohibits sharing the vulnerability report with new Participants who have not yet accepted an existing embargo. Sharing before acceptance undermines the embargo's confidentiality guarantees. ECA DetailsPreconditions - EM state is Active - EM state: ACTIVE |
Satisfies: VP-08-006 — SHOULD NOT share vulnerability report with newly invited Participant unless they accepted the embargo |
EMB-11-002 |
SHOULD | A Participant entering EM Active SHOULD maintain the embargo by not publishing information about the vulnerability until the embargo is terminated, whether on reaching its end time or early. An embargo whose end time has passed is ended by the CASE_MANAGER with the reason END_TIME_REACHED (EMB-13-003); until that termination reaches a Participant, its end time still bounds what it withholds.VP-05-014 specifies that Participants SHOULD NOT publish information about the vulnerability when there is an active embargo. Entering Active marks the formal commitment to these terms. Reaching the end time does not end the embargo by itself: the termination is a register change the CASE_MANAGER commits, so every Participant learns of it the same way (ADR-0122). ECA DetailsPreconditions - EM state is Active - EM state: ACTIVE |
Satisfies: VP-05-014 — SHOULD NOT publish information when there is an active embargo |
EMB-11-003 |
MUST NOT | A Participant in RM Closed or Deferred (q^rm ∈ {C, D}) in EM Active while other Participants remain engaged MUST NOT automatically terminate the embargo due to their own RM closure or deferral. VP-13-009 explicitly requires that a single Participant's closure or deferral SHALL NOT automatically terminate the embargo. The embargo persists as long as other Participants remain engaged and the EM process is Active. ECA DetailsPreconditions - Participant is in RM Closed/Deferred; EM state is Active - RM state: CLOSED, DEFERRED- EM state: ACTIVE |
Satisfies: VP-13-009 — A Participant's closure or deferral SHALL NOT automatically terminate the embargo |
EMB-11-004 |
SHOULD | Reports with no further tasks (q^rm ∈ {D, I}) SHOULD be held in Deferred or Invalid while the embargo remains Active or Revise (q^em ∈ {A, R}) rather than being closed. VP-13-006 advises keeping the report in a non-terminal RM state until the embargo has exited. Closing early may prematurely signal disengagement from the coordinated timeline. ECA DetailsPreconditions - Participant is in RM Deferred/Invalid; EM state is Active or Revise - RM state: DEFERRED, INVALID- EM state: ACTIVE, REVISE |
Satisfies: VP-13-006 — SHOULD be held in Deferred or Invalid until embargo exits |
EMB-11-005 |
SHOULD | A Participant who intends to change their adherence stance to an active embargo SHOULD communicate that intent clearly to other Participants. VP-13-008 requires communicating changes to embargo adherence. Surprises about who is or is not honoring the embargo undermine coordinated disclosure. ECA DetailsPreconditions - EM state is Active - EM state: ACTIVE |
Satisfies: VP-13-008 — Changes to adherence intention SHOULD be communicated clearly |
Enter EM Revise
Trigger: State Entered — EM.REVISE
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-12-001 |
MAY | A Participant entering EM Revise MAY evaluate the revision proposal and respond with EC (accept the revision), EJ (reject the revision), or EV (counter-propose a further revision). EM Revise means an active embargo has a pending revision proposal. The participant must evaluate the proposed new terms and either accept, reject, or counter-propose to move the EM process forward. ECA DetailsPreconditions - EM state is Revise - EM state: REVISE |
Satisfies: VP-04-005 — Once accepted, revisions MAY be proposed, which MAY be accepted or rejected |
EMB-12-002 |
MUST NOT | A Participant in RM Closed or Deferred (q^rm ∈ {C, D}) in EM Revise while other Participants remain engaged MUST NOT automatically terminate the embargo due to their own RM state. VP-13-009 applies equally in Revise state. A single Participant's closure or deferral SHALL NOT automatically terminate the embargo. The embargo remains in force even when one Participant's engagement has declined. ECA DetailsPreconditions - Participant is in RM Closed/Deferred; EM state is Revise - RM state: CLOSED, DEFERRED- EM state: REVISE |
Satisfies: VP-13-009 — A Participant's closure or deferral SHALL NOT automatically terminate the embargo |
EMB-12-003 |
SHOULD | A Participant who intends to change their adherence stance while an embargo is in Revise state SHOULD communicate that intent clearly to other Participants. VP-13-008 requires communicating changes to embargo adherence. This applies during revision negotiations as well as during the stable Active state. ECA DetailsPreconditions - EM state is Revise - EM state: REVISE |
Satisfies: VP-13-008 — Changes to adherence intention SHOULD be communicated clearly |
Enter EM Exited
Trigger: State Entered — EM.EXITED
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-13-001 |
SHOULD | A Participant entering EM Exited SHOULD treat no participant as bound by an embargo: the embargo register's ACTIVE entry is TERMINATED, and no consent row is written (MSM-07-006, ADR-0118, ADR-0122).Once the embargo exits, no consent row binds anyone: with no ACTIVE register entry nobody is a signatory, and a register with a TERMINATED entry accepts no further change (ADR-0122), so no later invitation to the terminated embargo can rebind anyone. The position is derived from the case, so it is not copied onto each participant record.ECA DetailsPreconditions - EM state is Exited - EM state: EXITEDSteps 1. [participant] Terminate the register's ACTIVE entry; write no Participant consent row → No Participant is bound by an embargo; consent rows are as they werePostconditions - No active embargo and no signatory; EM state is terminal EXITED |
Satisfies: VP-04-001 — Accepted embargoes MUST eventually terminate |
EMB-13-003 |
MUST | When the end time of the embargo in force has passed, the CASE_MANAGER MUST end it with the reason END_TIME_REACHED, through one idempotent termination path reached in either of two ways: its own lazy check when it next handles the case, whether for an inbound activity or one of its own triggers, or an outside request to end the embargo at its end time, such as from a Sentinel monitoring embargo timers. A request to end the embargo early made after its end time takes the same path. Both commit the CASE_MANAGER's Remove(EmbargoEvent) with content END_TIME_REACHED and run the full termination cascade. Whichever arrives after the embargo has ended MUST commit nothing and MUST be answered as already done. The outside request is made to the CASE_MANAGER itself; a Remove received from the case owner is the owner's own message and is ledgered on receipt like any other. An END_TIME_REACHED termination claimed before the embargo's end time MUST be refused, by the CASE_MANAGER before it commits and by a replica that replays it.VP-04-001 requires every accepted embargo to have an end time, so every embargo reaches one; recording the end as a termination keeps EM derived from the register, and lets the content gate stop withholding content under an embargo that has expired. The lazy check needs no scheduler; the outside request lets a deployment end an embargo on time without waiting for the next activity on the case. One path keeps the two from both committing: a register with a TERMINATED entry accepts no further change (ADR-0122).ECA DetailsPreconditions - An embargo is in force and its end time has passed - EM state: ACTIVE, REVISEPostconditions - The register's ACTIVE entry is TERMINATED with reason END_TIME_REACHED, every open proposal CANCELLED, EM EXITED, and the teardown announced |
Refines: VP-04-001 — Every accepted embargo has an end time; reaching it ends the embargo Depends on: EP-08-004 — The termination cancels every open proposal in the same step |
EMB-13-004 |
MUST | A termination Remove(EmbargoEvent) MUST carry what ended the embargo in content, exactly END_TIME_REACHED or EARLY, checked at the parse edge, where anything else MUST be refused; its summary carries a human-readable account the protocol does not read. The TERMINATED register entry MUST record that reason (the what) and, when known, a structured cause (the why), an object discriminated on kind whose details depend on the kind. The kinds are THREAT_SIGNAL, with the signals (P, X, A) that ended the embargo, distinct and in that order; FIX_READY, with the vendor participants whose readiness made the quorum (VP-09-007); FIX_DEPLOYED; and OTHER, whose account is the summary. Every kind is EARLY: an END_TIME_REACHED termination MUST NOT record a cause, and an EARLY termination with no cause is valid and records the what alone.The what is all a receiver needs to apply a termination and to check it against the end time; the why records the decision. A cause is a kind with its own details, so a new cause (adversary awareness, exploit code held by adversaries, a participant's leak) is a new kind rather than a change to a closed list (ADR-0122). |
Refines: EMB-13-001 — Records why the ACTIVE entry was TERMINATED |
EMB-13-005 |
MUST | A termination Remove(EmbargoEvent) whose termination has a cause (EMB-13-004) MUST carry it as terminationCause, checked at the parse edge, where an unknown kind, a missing or malformed detail, or a cause on an END_TIME_REACHED termination MUST be refused. On the case owner's Remove to the CASE_MANAGER, a FIX_READY, FIX_DEPLOYED or OTHER cause is the owner's claim, which the CASE_MANAGER MUST record as given; a THREAT_SIGNAL cause MUST be refused before it is committed, with a reason directing the sender to a case status update setting P, X or A as the way a threat signal ends an embargo. Only a Remove the CASE_MANAGER sends MAY name THREAT_SIGNAL. A replica MUST record the cause the termination's ledger entry carries on its TERMINATED entry, and MUST NOT work a cause out from its own case state.One ledger entry is one activity, and the termination's entry is the whole TERMINATE register step (ADR-0124), so the why reaches a replica replaying the ledger only if the Remove carries it. A threat signal is a fact about the case that every participant learns from a status entry; naming it on a Remove while the case status does not show it would leave the register and the status disagreeing, and reading the Remove as a status change would commit one no participant sent (ADR-0122). |
Refines: EMB-13-004 — Carries the recorded cause on the wire so replicas can copy it |
EMB-13-002 |
MUST NOT | A Participant in EM Exited MUST NOT seek new embargoes or accept new embargo proposals (EP) for this case when the CS state is public (q^cs ∈ ···P··), exploit-public (q^cs ∈ ····X·), or attacks-observed (q^cs ∈ ·····A). VP-14-001 through VP-14-003 prohibit seeking new embargoes once public disclosure, exploit publication, or attacks have occurred. The EXITED state after any of these events is terminal for embargo negotiations on this case. ECA DetailsPreconditions - EM state is Exited; CS matches pattern ...P.. - EM state: EXITED- CS pattern: ...P..- EM state is Exited; CS matches pattern ....X. - EM state: EXITED- CS pattern: ....X.- EM state is Exited; CS matches pattern .....A - EM state: EXITED- CS pattern: .....A |
Satisfies: VP-14-001 — Once Public Awareness, new embargoes SHALL NOT be sought Satisfies: VP-14-002 — Once Exploit Publication, new embargoes SHALL NOT be sought Satisfies: VP-14-003 — Once Attacks observed, new embargoes SHALL NOT be sought |
Actor-Voluntary Embargo Termination (Trigger Side)
Trigger-side factory for voluntarily ending an active embargo (e.g., fix deployed or ready). "The actor" here is the actor enacting the CASE_MANAGER role: the factory's canonical case-state write (EM → EXITED, ledger commit, teardown announce) is gated on CVDRole.CASE_MANAGER (terminate_embargo_bt runs ResolveCaseManagerNode before the state mutation) and is authorised by the Case Owner by default (CM-02-005; EmbargoTeardownAuthorizationGate defaults to RequireCaseOwnerApproval, RSH-02-002). A non-manager Participant that wishes to end an embargo does not run this trigger to write canonical state; it initiates termination by reporting the condition or notifying intent (VP-11-003, VP-13-017/018), and terminates only its own compliance. See CONCERN-3280 for the reconciliation of the participant-facing protocol prose with this authority model.
Trigger: State Entered — EM.ACTIVE
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-14-001 |
MUST | The create_terminate_active_embargo_tree factory MUST require the actor to supply a termination reason before delegating to terminate_embargo_bt. Valid reasons are: fix-deployed, fix-ready, or other. The reason is chosen via the matching Evaluator call-out point (ExitEmbargoWhenDeployed, ExitEmbargoWhenFixReady, or ExitEmbargoForOtherReason) and MUST be recorded as the termination's cause, FIX_DEPLOYED, FIX_READY or OTHER respectively (EMB-13-004), never as an EARLY termination with no cause.Voluntary termination requires an auditable reason. Structuring reason selection as a Selector of Evaluator call-out points gives actors a clear interface (three buttons) while preserving the audit trail and allowing policy-rule backends to be injected per ADR-0025. ECA DetailsPreconditions - EM state is Active or Revise - EM state: ACTIVE, REVISESteps 1. [actor] Select termination reason (ExitEmbargoWhenDeployed, ExitEmbargoWhenFixReady, or ExitEmbargoForOtherReason) → Matching Evaluator call-out point returns SUCCESS; reason recorded 2. [system] Run EmbargoExitPolicyGuard or EmbargoExitOverride → Policy check passes or actor override accepted 3. [system] Run OnEmbargoExit actuator → Site-specific integration hooks execute 4. [system] Delegate to terminate_embargo_bt → ET emitted; EM transitions to EXITED Postconditions - EM state is EXITED; ET queued; termination reason recorded |
Refines: EMB-07-001 — Voluntary actor termination is a sub-case of active-embargo termination |
EMB-14-003 |
SHOULD | The create_terminate_active_embargo_tree factory SHOULD include an EmbargoExitOverride Evaluator call-out point in a Selector alongside EmbargoExitPolicyGuard. When the actor explicitly accepts responsibility for overriding policy, EmbargoExitOverride SHOULD return SUCCESS so that termination proceeds despite a policy veto. Providing an override seam lets actors exercise their authority to exit in exceptional circumstances without requiring code changes. The override is a distinct call-out point so that integrators can log, gate, or audit it separately from the normal policy path. ECA DetailsPreconditions - EM state is Active or Revise; reason Selector succeeded; EmbargoExitPolicyGuard returned FAILURE - EM state: ACTIVE, REVISESteps 1. [actor] Invoke EmbargoExitOverride call-out point → Override accepted; pipeline continues to OnEmbargoExit Postconditions - Override recorded; termination proceeds; audit trail reflects override |
Refines: EMB-14-002 — Override is the fallback arm of the policy authorization Selector |
Inbound Embargo-Response Decision (Received Side)
Behaviors triggered when an actor receives an inbound embargo overture and must decide how to respond. Two overture flows are distinguished: (Flow A) an EM-level embargo proposal (EP) proposing terms, whose acceptance moves the shared EM state; and (Flow B) a PEC-level invitation (InviteToEmbargoOnCase) asking the actor to become a signatory to an embargo, whose acceptance advances that participant's Participant Embargo Consent (PEC) state. The pre-PEC simulator conflated these into a single _ChooseEmProposedResponse fallback; the production protocol separates the shared EM lifecycle (EMB-01..EMB-09) from per-participant consent (CM-18, ADR-0048). This group specifies a single decision seam layered over both flows: it selects the response (accept / defer-to-owner / reject / counter) while delegating the mechanical transition to the existing received and trigger BTs. The mechanics already exist (invite/accept/reject trees, propose/accept/reject trigger use cases); what is missing is the decision layer that chooses among them, with a default-accept policy and a plug-in seam for owner adjudication.
Trigger: Message Received — EP | InviteToEmbargoOnCase
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-15-001 |
MUST | The inbound embargo-response decision seam MUST default to accepting a well-formed embargo overture (Flow A: EP proposing terms; Flow B: InviteToEmbargoOnCase). The recommended CVD behavior is to accept an offered embargo and negotiate revisions separately rather than block coordination. A deterministic accept-by-default keeps actors runnable without a human in the loop, and the DETERMINISTIC call-out bundle (BT-23) supplies the default. Structuring acceptance as an Evaluator call-out point preserves the seam for later owner/agent adjudication without changing the tree. ECA DetailsPreconditions - Inbound EP or InviteToEmbargoOnCase received; case is embargo-eligible (CS not public/exploit/attacks per EMB-01-002); no external adjudication backend injected (deterministic default in force). Steps 1. [system] Run EvaluateEmbargoProposal Evaluator call-out point → Returns SUCCESS (default-accept) under the DETERMINISTIC bundle 2. [system] Delegate to the flow-appropriate acceptance BT → Flow A activates/records via accept_embargo_trigger_bt; Flow B marks the consent row AGREED via accept_invite_to_embargo_tree Postconditions - Overture accepted by default; EM (Flow A) or PEC (Flow B) advanced by the existing mechanism |
Satisfies: BT-18-004 — Decision node accepts a CallOutBackendFactory; deterministic default Refines: EMB-01-001 — Adds the response-decision layer above EP receipt Refines: CM-18-003 — Flow B acceptance advances the participant's consent row (INVITED → AGREED) |
EMB-15-005 |
MUST | When no external adjudication backend is injected, the EvaluateEmbargoProposal Evaluator call-out point MUST return SUCCESS so the actor accepts by default, delegating to the existing acceptance mechanism (accept_embargo_trigger_bt for Flow A; accept_invite_to_embargo_tree for Flow B).A deterministic accept-by-default keeps actors runnable without a human in the loop; the DETERMINISTIC call-out bundle (BT-23) supplies the default. |
Refines: EMB-15-001 |
EMB-15-002 |
MUST | The decision seam MUST include a CASE_OWNER hard-bypass arm ahead of the adjudication call-out point. This mirrors the two-seam authorization model (ADR-0046) and the case_proposal_received_tree pattern: the CASE_OWNER is the human decision-maker whose choice is gospel, while other actors need an adjudication seam that a UI or LLM agent can later fulfil. The distinction matters because a CASE_MANAGER may process an overture routed to it without being the party whose consent is being recorded. ECA DetailsPreconditions - Inbound EP or InviteToEmbargoOnCase received by the deciding actor Steps 1. [system] Run CheckIsCaseOwner guard (hard-bypass arm of the Selector) → SUCCESS when actor holds CASE_OWNER (owner's response is authoritative); FAILURE otherwise, falling through to the adjudication call-out point 2. [system] For non-owners, run CaseOwnerApprovesEmbargoResponse call-out point → Returns SUCCESS (default) or FAILURE (adjudication withholds) Postconditions - Owner responses proceed unconditionally; non-owner responses gated by the adjudication seam |
Satisfies: BT-18-004 Refines: EMB-15-001 |
EMB-15-007 |
MUST | Non-owner actors (e.g., a CASE_MANAGER processing an overture on the owner's behalf) MUST route through the CaseOwnerApprovesEmbargoResponse call-out point, which defaults to SUCCESS under the DETERMINISTIC bundle.Non-owner actors need an adjudication seam that a UI or LLM agent can later fulfil; defaulting to SUCCESS keeps them runnable without an external backend. |
Refines: EMB-15-002 |
EMB-15-003 |
SHOULD | For Flow A (EP proposing terms), the decision seam SHOULD provide a counter-proposal arm gated by a WillingToCounterEmbargoProposal Evaluator call-out point. Countering MUST NOT introduce a new mechanism: when the arm is taken, the seam delegates to the existing embargo proposal trigger (propose_embargo_trigger_bt via SvcProposeEmbargoUseCase), which emits a fresh EP while the EM state remains PROPOSED. The default DETERMINISTIC backend for this call-out point SHOULD return FAILURE (do not counter), reflecting the recommended practice of accepting and revising rather than countering. A counter-proposal is operationally identical to proposing: the propose trigger already records a "counter-proposed, no state change" outcome when invoked in PROPOSED. Modeling counter as routing to the existing trigger avoids duplicate mechanism and keeps the EM lifecycle single-sourced. Countering is intentionally low-automation and off by default; the recommended path is accept-then-revise. ECA DetailsPreconditions - Flow A only; inbound EP received; actor declined to accept outright and the adjudication arm elects to counter - EM state: PROPOSEDSteps 1. [system] Run WillingToCounterEmbargoProposal Evaluator call-out point → SUCCESS elects the counter arm; FAILURE (default) skips it 2. [actor] Delegate to propose_embargo_trigger_bt with new terms → New EP emitted; EM remains PROPOSED (counter recorded) Postconditions - Counter EP emitted via the existing proposal mechanism; no new counter-specific transition introduced |
Refines: EMB-15-001 Satisfies: EMB-01-001 — Counter reuses the EP proposal path |
EMB-15-004 |
MUST | When the decision resolves to reject, the seam MUST delegate to the flow-appropriate existing rejection mechanism rather than duplicating the transition: reject_embargo_trigger_bt (Flow A, emitting ER; when the deciding actor is the case owner the trigger sends the owner's Reject(EmbargoEvent, target=Case), which returns the shared EM to NONE per EMB-06 and MSM-07-009) or, for Flow B, the invitee's Reject of the Invite (recording that participant's consent per MSM-07-004: DECLINE of the rejected embargo's row, a withdrawal when it names the active embargo; a signatory refusing proposed terms keeps its row for the embargo in force). The case owner's decision for the case is a separate activity, Reject(EmbargoEvent, target=Case) (MSM-07-009); an owner MAY still send a Reject(Invite(EmbargoEvent)), which records only its own consent like any sender's (MSM-07-004). In this prototype the owner's auto-answer to a Flow B Invite is its decision for the case, not a Reject of the Invite. The seam MUST also honor the mandatory rejection required by EMB-01-002 when CS is public/exploit/attacks, independent of any adjudication outcome. Rejection paths are already implemented and carry the correct EM-vs-PEC semantics. The decision layer selects among them; it must not re-implement the transition. The pre-public constraint (EMB-01-002) is a hard protocol rule that overrides the default-accept policy. ECA DetailsPreconditions - Inbound overture received; decision resolves to reject, OR CS is public/exploit/attacks forcing rejection (EMB-01-002) Steps 1. [system] Select the flow-appropriate rejection BT → Flow A emits ER (EM → NONE); Flow B advances PEC to DECLINED Postconditions - Overture rejected via the existing mechanism with correct semantics |
Refines: EMB-06-001 — Flow A rejection reuses the ER emission path Refines: CM-18-003 — Flow B rejection advances PEC to DECLINED Satisfies: EMB-01-002 — Mandatory rejection when public/exploit/attacks overrides default-accept |
CS Public/Exploit/Attacks Detected While EM Proposed
Behaviors required when a Participant detects that the vulnerability has become public (q^cs ∈ ···P··), an exploit is available (q^cs ∈ ····X·), or attacks have been observed (q^cs ∈ ·····A) while the case EM state is still Proposed (q^em ∈ P). Embargo negotiation is not viable in any of these states; the proposed embargo MUST be abandoned.
Trigger: State Entered — CS.public_exploit_attacks
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-16-001 |
MUST | A Participant in EM Proposed (q^em ∈ P) that detects a CS transition into a public (q^cs ∈ ···P··), exploit-available (q^cs ∈ ····X·), or attacks-observed (q^cs ∈ ·····A) state MUST abandon the proposed embargo by cancelling each open proposal in the case's embargo register, so EM derives None (q^em P → N), and MUST notify the other Participants. The CASE_MANAGER cancels each proposal with the activity its AS2 shape names: its ER, Reject(Invite(EmbargoEvent)), of a proposal it received, committed as reject_invite_to_embargo_on_case, or its Undo(Invite(EmbargoEvent)) of a proposal it made itself, committed as undo_invite_to_embargo_on_case, followed by its Undo(Invite(EmbargoEvent)) of each Invite it relayed for that proposal. A proposal never relayed is cancelled all the same. The entries that abandon every open proposal MUST commit in one transaction (CLP-07-015), and a replica MUST apply each proposal's CANCEL once, from the entry that carries it (ADR-0152).EMB-01-002 establishes that embargo negotiation is not viable once public disclosure, exploit availability, or attacks have occurred. EMB-01-002 is framed as a guard on receiving EP, but the same viability constraint applies when a P/X/A state change arrives while a proposal is already in flight. Continuing to negotiate a proposed embargo in these conditions would be misleading to other Participants and counter to the purpose of coordinated disclosure. CANCEL records that nobody decided the proposal (ADR-0122). An actor undoes only an activity it performed, so withdrawing an Invite one sent is an Undo and declining one received is a Reject; a CASE_MANAGER act with a participant's verb and object carries the participant's semantic type (ADR-0152).ECA DetailsPreconditions - EM state is Proposed - EM state: PROPOSED- CS transitions to public (q^cs ∈ ···P··) - CS pattern: ...P..- CS transitions to exploit-available (q^cs ∈ ····X·) - CS pattern: ....X.- CS transitions to attacks-observed (q^cs ∈ ·····A) - CS pattern: .....ASteps 1. [system] Detect that CS has transitioned to a P/X/A state while EM is Proposed → ThreatTerminationBranchNode recognises an open proposal (not skipped) 2. [system] As the CASE_MANAGER, cancel each open proposal via reject_proposed_embargo_bt (AbandonEmbargoProposalsLifecycleNode commits each proposal's Reject, or its Undo of an own proposal, then an Undo of each Invite relayed for it, and cancels that proposal) → Each proposal's register entry is CANCELLED and EM derives NONE; the entries are committed and replayed by every replica Postconditions - EM state is NONE; the abandonment is committed; proposed embargo abandoned |
Extends: EMB-01-002 — EMB-01-002 guards against entering PROPOSED with P/X/A set; this entry covers the symmetric case where P/X/A fires while already in PROPOSED Refines: EMB-06-001 — ER emission semantics apply; EM returns to NONE |
EMB-16-002 |
MUST NOT | A Participant that does not hold CVDRole.CASE_MANAGER and detects a P/X/A CS transition while EM is Proposed MUST NOT emit ER as the abandonment of the case's open proposals, and MUST NOT write the abandonment to its own EM state.Abandoning the case's open proposals is a decision for the case, made by the case owner or the CASE_MANAGER it delegates to (EMB-16-001, EP-09-008). The P/X/A signal already reaches the CASE_MANAGER as a status declaration, and the CASE_MANAGER abandons on its own detection. An ER sent on the case's behalf by another Participant arrives at the CASE_MANAGER as that Participant declining the proposal (MSM-07-004), so it would record an abandonment the Participant had no standing to make (#4148). |
Refines: EMB-16-001 Depends on: EP-09-008 |
Late Accept of an Expired Embargo Invitation
Behaviors required when an Accept(Invite(EmbargoEvent)) arrives after the invitation's RSVP deadline (CM-28-001) has passed. The protocol is liberal in what it accepts: a late accepter has signalled willingness to coordinate and MUST NOT be sent away. Codified by ADR-0065. Source: IDEA-2066.
Trigger: Message Received — EA
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-17-001 |
MUST | A CASE_MANAGER receiving an Accept(Invite(EmbargoEvent)) after the invitation's effective RSVP deadline MUST NOT reject it solely because the deadline has passed. It MUST evaluate whether the accepted embargo is still compatible with the case's current embargo and then apply EMB-17-002, EMB-17-003, or EMB-17-004.Being liberal in what is accepted serves CVD outcomes. A late accepter is volunteering to coordinate; refusing them converts a missed deadline into a lost participant, which is worse for the case than a late consent. See ADR-0065. ECA DetailsPreconditions - The referenced invitation's effective RSVP deadline has passed Postconditions - The Accept is not refused on deadline grounds; one of EMB-17-002, EMB-17-003, or EMB-17-004 has been applied |
Depends on: CM-28-003 |
EMB-17-002 |
MUST | When the accepted embargo is the case's current embargo, the CASE_MANAGER MUST honour the late Accept and mark the participant's consent row for it AGREED, exactly as it would for an on-time Accept. A row at TIMED_OUT moves directly to AGREED (TIMED_OUT → AGREED, ADR-0118), and so does one at DECLINED (DECLINED → AGREED, CM-18-003): the participant has changed its answer, so it is not re-invited first. The CASE_MANAGER honours it by committing the received Accept verbatim as accept_invite_to_embargo_on_case, carrying the row it states, as an on-time Accept is committed (EMB-17-009, ADR-0152).If the terms the participant agreed to are the terms in force, the participant is a valid signatory. The deadline governs whether the invitation is open, not whether the agreement is sound. See ADR-0065. The Accept states the change, so its entry is the change's entry (CLP-07-013), and a replica reaches the state an on-time Accept leaves. ECA DetailsPreconditions - EM state is Active - EM state: ACTIVE- The accepted embargo ID matches the case's current active embargo Steps 1. [system] Record the PEC AGREE transition for the accepting participant → The participant's consent row for the current embargo is AGREED Postconditions - The participant is a signatory to the current embargo |
Refines: EMB-17-001 |
EMB-17-003 |
MUST | When the accepted embargo is NOT the case's current embargo (for example the embargo was revised or replaced after the invitation was sent), the CASE_MANAGER MUST issue a fresh Invite(EmbargoEvent) to the accepting actor carrying the case's current embargo. An accepting participant at TIMED_OUT moves TIMED_OUT → INVITED for the current embargo (ADR-0118). The fresh Invite is a relay (EP-09-002): its actor is the CASE_MANAGER and its attributedTo names the current embargo's proposer, or the CASE_MANAGER itself when it made the embargo without a proposal, as it makes the protocol default. The CASE_MANAGER MUST commit it as invite_to_embargo_on_case once it has queued it (SYNC-09-002), followed by the invitee's INVITED row with the Invite's deadline (CM-28-013), and a replica MUST apply it through the relay slot: its register already holds the embargo the Invite names, so the Invite proposes nothing and EM does not move (ADR-0152).Recording the stale Accept as consent would make the participant a signatory to terms they never saw. Rejecting them would discard a willing participant. Re-inviting resolves both: the actor stays in the case and is asked to consent to the terms actually in force. See ADR-0065. The re-invite asks about terms somebody proposed, exactly as a relay does, so it carries the relay's shape and semantic type (ADR-0152). ECA DetailsPreconditions - The accepted embargo ID does not match the case's current active embargo, and the case has a current embargo Steps 1. [system] Emit a fresh Invite(EmbargoEvent) to the accepting actor carrying the case's current embargo → Accepting participant's consent row for the current embargo is INVITED; stale Accept is not recorded as consent Postconditions - A fresh invitation for the current embargo has been sent; the actor remains a case participant |
Refines: EMB-17-001 Depends on: CM-28-013 |
EMB-17-005 |
MUST NOT | When the accepted embargo is not the case's current embargo, the CASE_MANAGER MUST NOT record the stale Accept as consent to the current terms. Recording the stale Accept as consent would make the participant a signatory to terms they never saw. See ADR-0065. |
Refines: EMB-17-003 |
EMB-17-006 |
MUST NOT | When the accepted embargo is not the case's current embargo, the CASE_MANAGER MUST NOT reject the accepting actor. Rejecting a willing participant discards coordination capacity; re-inviting to the current terms is the correct response. See ADR-0065. |
Refines: EMB-17-003 |
EMB-17-004 |
MUST | When the case has no current embargo (EM state is EXITED or NONE), the late Accept changes nothing, and the CASE_MANAGER MUST treat it as a no-op: it MUST NOT commit any ledger entry for it, MUST NOT send any activity in reply, and MUST leave the participant's consent rows unchanged: nothing is recorded after a termination (EM EXITED), and a TIMED_OUT row stays TIMED_OUT when no embargo has started (EM NONE) (ADR-0118). A redelivery of the same Accept gets the same result. It is a correct no-op, reported SKIPPED (HP-01-003).Follows the EMB-07-003 precedent for post-terminal messages: no transition. There is nothing to invite the actor to, so a fresh invitation would manufacture a negotiation nobody requested — but the actor's case participation is independent of embargo consent and MUST survive. An entry or reply acknowledging the Accept would change nothing at any receiver, and anything that holds the case and its ledger can see the Accept named an embargo the case no longer holds, so the no-op is not ledgered (ADR-0152, CLP-02-013). See ADR-0065. ECA DetailsPreconditions - EM state is EXITED (embargo terminated) or NONE (no embargo started) - EM state: EXITED, NONESteps 1. [system] Treat the Accept as a no-op, committing and sending nothing → Participant consent rows are unchanged; participant remains in the case; no ledger entry is committed and nothing is sent Postconditions - No embargo state change; accepting actor remains a case participant |
Refines: EMB-17-001 Extends: EMB-13-002 — EMB-13-002 covers the P/X/A case; this entry covers an Accept arriving after EM EXITED when CS is not yet P/X/A |
EMB-17-007 |
MUST NOT | When the case has no current embargo (EM state is EXITED or NONE), the CASE_MANAGER MUST NOT issue a fresh embargo invitation to the accepting actor. There is no active embargo to invite the actor to; a fresh invitation would manufacture a negotiation nobody requested. See ADR-0065. |
Refines: EMB-17-004 |
EMB-17-008 |
MUST NOT | When the case has no current embargo (EM state is EXITED or NONE), the CASE_MANAGER MUST NOT remove the accepting actor from the case. Case participation is independent of embargo consent; the actor's willingness to coordinate must not result in their removal. See ADR-0065. |
Refines: EMB-17-004 |
EMB-17-009 |
MUST | When the CASE_MANAGER honours a late Accept (EMB-17-002), it MUST commit the received Accept(Invite(EmbargoEvent)) verbatim as accept_invite_to_embargo_on_case, its entry carrying the AGREED row it states, exactly as it commits an on-time Accept, so that a replica copies the row without re-evaluating the deadline (RSH-08-004, ADR-0124, CLP-07-011). It MUST NOT commit any further entry for the same row, such as its own Update(EmbargoConsent) of the AGREED row (CLP-07-002, ADR-0152).Without a committed entry the replica has no signal that the row moved to AGREED; the replica's participant record would silently stay TIMED_OUT while the CASE_MANAGER's record shows AGREED, breaking ledger-driven convergence (ADR-0108, ADR-0118). The Accept states the change, so its entry is the change's entry (CLP-07-013); a follow-up Update of the same row would record one state change twice. The deadline having passed is an observation anything holding the case can make, so it is not ledgered either (ADR-0152).ECA DetailsPostconditions - The late Accept is committed as accept_invite_to_embargo_on_case in the CASE_MANAGER's store, and the accepter's AGREED row is copied from it in every replica's store |
Refines: EMB-17-001 Depends on: RSH-08-004 |
EM Write-Boundary Consolidation
Requirements ensuring that all EM state transitions are executed through the EmbargoLifecycle service layer rather than written directly to case.current_status.em in BT nodes or helpers. Identified by audit in CONCERN-2412: three BT nodes (EmActivateEmbargo, ClearEmbargo, SetDefaultActiveEmbargoNode) bypass EmbargoLifecycle with direct EmDimension assignment. Complements SM-04-001 (general write-boundary guard requirement) with an EM-specific routing rule.
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-18-004 |
MUST | A receiver MUST refuse an inline EmbargoEvent whose context is neither the case that carries it nor a report that case holds (EP-04-009), and MUST refuse it before writing anything: the event is not stored and no embargo state of either case changes. The same refusal applies to the record the receiver already holds under the embargo's id, whether the case or ledger entry carries the embargo inline or names it by bare id. For a ledger entry, the case's reports are those of the replica's stored case.An embargo belongs to the case named by its context. The inbox stores a carried embargo ahead of the handler's trust checks, and a first write wins, so an unchecked event would let a sender plant another case's embargo under an id that case will later name. The case would then name the held record, not the carried copy, so checking only the copy, or a bare id only for resolving, leaves the case pointing at another case's embargo (#4962). A Reporter's proposal names its report until the case exists and keeps its id once it is about the case (EP-04-004), so the Reporter holds it about the report; refusing that would refuse the Reporter its own case. Giving the refusal a requirement lets ratchets and docs cite it instead of a code comment. |
Refines: EMB-18-003 |
EMB-18-005 |
MUST | Every change to a case's embargo register MUST move the case's EM state only along an EM transition for the change's EM trigger: a proposal is propose, an activation is accept, a rejection, or the cancellation of open proposals with no embargo in force, is reject, and a termination is terminate. The EM transitions are propose: N→P, P→P, A→R, R→R; reject: P→N, P→P, R→A, R→R; accept: P→A, P→R, R→A, R→R; and terminate: A→X, R→X. EM leaves Proposed or Revise only when no proposal remains open.EM is computed from the embargo register (ADR-0122), not replaced by it: the EM state machine remains the protocol's model, and the register is a layer beneath it. With several proposals open, rejecting or activating one leaves the others open, so EM stays in Proposed or Revise until the last one is off the table (ADR-0130). |
Refines: EMB-18-001 |
Embargo Teardown Announcement
Who authors the Announce(EmbargoEvent) that follows an embargo teardown, and who receives it. Added by ISSUE-2238 after the recipients were found to be unspecified, which is how they came to disagree with the tree's role gate.
| ID | Priority | Requirement | Related |
|---|---|---|---|
EMB-19-001 |
MUST | The Announce(EmbargoEvent) emitted after an embargo teardown MUST be authored by the actor holding CVDRole.CASE_MANAGER for the case, and MUST be addressed to every active participant (CM-10-004) except the announcing actor. It MUST NOT be addressed to the Case Manager.The teardown is canonical case state, so the role holder announces it and the participants are the audience. Addressing the Case Manager was correct while the tree ran as an ordinary participant telling the manager; once the tree's ledger commit became gated on CVDRole.CASE_MANAGER the executing actor is the manager, so the announce was addressed to itself and the teardown reached nobody. Nothing in this corpus governed the recipients, which is why only one half of the pair was updated when the gate was added — the gap is the reason this entry exists. |
Refines: BT-05-006 Refines: CLP-10-001 |
EMB-19-002 |
MUST | When a case has no participants besides the Case Manager, the teardown announcement MUST be skipped rather than emitted with an empty recipient list. An activity addressed to nobody is undeliverable, and delivery discards it anyway; constructing one only adds a queue entry that can never be resolved. Skipping keeps the teardown itself unaffected, consistent with the announcement being best-effort — a notification gap MUST NOT block a teardown. |
Refines: EMB-19-001 |
Embargo Policy
Requirements for a standardized embargo policy record associated with an Actor profile. Allows actors to declare their default embargo preferences so that coordinators can evaluate compatibility before proposing an embargo or inviting an actor to a case.
File ID: EP
Policy Record Format
| ID | Priority | Requirement | Related |
|---|---|---|---|
EP-01-001 |
MAY | An Actor profile MAY include an embargo_policy field containing the Actor's structured embargo policy record inline. The policy is part of the profile: an Actor has at most one, and it is published, read and replaced as a field of the profile, not as a separate object.Whoever holds an Actor's profile then holds its embargo terms, so carrying the profile (CP-01-010) carries the policy, and no lookup has to choose among several records. |
|
EP-01-002 |
MUST | The embargo policy record MUST include the following fields: actor_id (full URI string identifying the Actor to which this policy applies), inbox (URL string of the Actor's ActivityPub inbox), preferred_duration (ISO 8601 duration string representing the Actor's preferred embargo length) | Implements: VP-05-001 Implements: VP-07-001 Implements: VP-07-002 Depends on: DUR-01-001 — ISO 8601 duration format |
EP-01-003 |
SHOULD | The embargo policy record SHOULD include the optional fields minimum_duration (ISO 8601 duration), maximum_duration (ISO 8601 duration), and notes (free-text string describing the Actor's embargo preferences). |
Implements: VP-05-001 Implements: VP-07-006 Depends on: DUR-01-001 — ISO 8601 duration format |
EP-01-004 |
MUST | The embargo policy record MUST be serializable as a Pydantic model and persisted in the DataLayer as part of the Actor's profile record (EP-01-001) | |
EP-01-005 |
MUST | The embargo policy record MUST use full-URI IDs for actor_id |
Depends on: OID-01-001 |
EP-01-006 |
SHOULD | When minimum_duration is declared, an Actor SHOULD reject embargo proposals shorter than that value.The minimum_duration field signals a floor below which the Actor will not accept an embargo; evaluating it prevents compatibility mismatches. |
Refines: EP-01-003 |
EP-01-007 |
SHOULD | When maximum_duration is declared, an Actor SHOULD reject embargo proposals longer than that value.The maximum_duration field signals a ceiling above which the Actor will not accept an embargo; evaluating it prevents compatibility mismatches. |
Refines: EP-01-003 |
API Endpoint
| ID | Priority | Requirement | Related |
|---|---|---|---|
EP-02-001 |
SHOULD | Each Actor SHOULD expose its embargo policy at GET /actors/{actor_id}/embargo-policy |
|
EP-02-002 |
MUST | The policy endpoint MUST be machine-readable (returns JSON) and MUST return the same record the Actor's profile carries inline under embargo_policy (EP-01-001) |
|
EP-02-005 |
SHOULD | A hosted Actor SHOULD be able to publish or replace its embargo policy at PUT /actors/{actor_id}/embargo-policy, carrying only the terms (EP-01-002, EP-01-003). The record's actor_id and inbox MUST be derived from the addressed actor's own record, never taken from the body, and the record MUST be persisted as the embargo_policy field of that actor's profile in its own store (EP-01-004), so that the profile carries it inline as the actor default shortest-wins reads at case creation (CP-01-010, EP-04-003). A replace MUST leave exactly one policy for the actor.Nothing in the API wrote an EmbargoPolicy before #3972; the only writers were tests and the demo seeder, so a Receiver could not be given an actor default without reaching into its store. Deriving actor_id from the path actor keeps a body from publishing terms in another actor's name (EP-01-005). Writing the terms into the one profile field makes a replace an overwrite, so there is never a withdrawn default left beside the current one for anything to read (#4027). |
Refines: EP-01-004 |
Policy Compatibility Evaluation
| ID | Priority | Requirement | Related |
|---|---|---|---|
EP-03-001 |
SHOULD | Before proposing an embargo or inviting an actor to join an existing embargo, the CASE_MANAGER SHOULD retrieve and evaluate the target actor's embargo policy for compatibility with the proposed embargo terms | Implements: VP-07-001 Implements: VP-07-006 Implements: VP-07-007 |
EP-03-002 |
MUST | Compatibility evaluation MUST check that the proposed duration falls within the target actor's minimum_duration–maximum_duration range, if declared |
Implements: VP-07-003 Implements: VP-07-004 Implements: VP-07-005 Depends on: DUR-01-001 Depends on: DUR-04-001 |
Default Embargo Semantics
| ID | Priority | Requirement | Related |
|---|---|---|---|
EP-04-001 |
MUST | When the CASE_OWNER's actor default embargo applies at case creation and the sender submitted no explicit embargo proposal, the initial CaseStatus.em.state MUST be set to EM.ACTIVE, not EM.PROPOSED. The sender's report submission without a counter-proposal constitutes tacit acceptance of the CASE_OWNER's actor default embargo. |
Refines: DUR-07-003 |
EP-04-002 |
MUST NOT | When a default embargo is created, the intermediate EM Proposed state MUST NOT be observable to other participants: no message, ledger entry or replica shows the embargo as Proposed. | |
EP-04-003 |
MUST | When both the sender and the CASE_OWNER have an applicable embargo proposal at case creation (the sender proposes a duration and the CASE_OWNER's profile carries an embargo policy, its actor default — EP-01-001, CP-01-010), the shorter of the two MUST be made active immediately (em_state = EM.ACTIVE) and the longer SHOULD be registered as a pending revision (em_state = EM.REVISE).Two parties that propose 30 and 60 days respectively agree on days 1–30 and disagree only on days 31–60. Activating the shorter embargo immediately captures all mutual agreement; the longer duration becomes a pending revision so the proposer can negotiate the contested portion. This is strictly better than rejecting outright: both parties secure the overlap they both wanted. See docs/topics/process_models/em/formal_model.md § "Why the shortest proposal wins" for the full vector-agreement derivation. |
Refines: EP-08-001 — EP-04-003 is the two-party case-creation instance of this ordering rule. Both MUST resolve earliest-end-date first, and both MUST share one comparator (#3392). |
EP-04-004 |
MUST | A sender MUST be able to propose embargo terms when submitting a report, by carrying a proposed EmbargoEvent, inline or by reference, as the proposedEmbargo of the Offer(VulnerabilityReport) activity. The proposed EmbargoEvent carries the report URI as its context (EP-04-009), and the CASE_MANAGER MUST rewrite that context to the case URI at case creation.This discharges the contingency this requirement previously recorded. Until ADR-0096 no sender-proposal mechanism existed, so EP-04-003 was unreachable and only the no-sender-proposal path applied. Implemented by #3392: the Offer carries the proposal as proposedEmbargo; the report receiver's CaseProposal carries the Offer whole (CP-01-008); the case-actor's CreateEmbargoEventNode keeps the Reporter's event and rewrites its context. |
Refines: EP-04-003 |
EP-04-005 |
MUST | A protocol default embargo duration MUST exist and MUST be configurable. It MUST be no less than 72 hours and no more than 5 days. When an embargo-eligible case is created and no embargo proposal and no actor default applies, the protocol default MUST be used and the case MUST begin at EM.ACTIVE. Where the case is not embargo-eligible, EP-04-008 applies instead and no protocol default embargo is created.The goal of the EM process is to lock in an embargo as early as possible ( docs/topics/process_models/em/principles.md). A case with no embargo is the outcome the process exists to avoid, and reaching it through mutual silence is the least deliberate way to arrive there. |
|
EP-04-006 |
MUST NOT | The protocol default MUST NOT participate in the shortest-wins comparison of EP-04-003. It is the value applied when no proposal and no actor default applies, never a candidate within that comparison. A 72-hour protocol default that competed under shortest-wins would win against every longer proposal and cap every embargo in the system at 72 hours, so no longer embargo could ever be agreed. The protocol default answers "what if nobody said anything", not "whose terms are shortest". See ADR-0096. |
Refines: EP-04-003 |
EP-04-007 |
MUST NOT | The protocol default MUST NOT be applied as a minimum on agreed embargo terms. A proposal or actor default shorter than the protocol default MUST be honored at its stated duration and MUST NOT be clamped up to it.em/principles.md requires embargo duration be "limited to the shortest duration possible", and a Participant is free to set their own terms. EP-04-005's range bounds what the fallback may be configured to; it does not constrain what parties may agree. A reporter who proposes 12 hours gets 12 hours. See ADR-0096. |
|
EP-04-008 |
MUST NOT | The protocol default MUST NOT be applied to a case that is not embargo-eligible. When any of P/X/A is set at case creation, no protocol default embargo is created and the case MUST remain at EM.NONE.VP-06-001 already forbids proposing or accepting an embargo negotiation once the vulnerability is public, exploit code is public, or attacks are observed (EMB-01-002 is the accept half of the same prohibition). An embargo on an already-public vulnerability protects nothing, so the protocol default inherits that guard rather than bypassing it. See ADR-0096. |
Refines: VP-06-001 Refines: EP-04-005 |
EP-04-009 |
MUST | EmbargoEvent.context MUST identify the subject the embargo concerns: the report URI before a case exists, and the case URI afterwards. A reader MUST NOT assume the context is a case without checking.A proposal embedded on Offer(VulnerabilityReport) (EP-04-004) precedes the case, so it has no case URI to point at. Widening the subject is a semantic change, not a type change: the field is EmbargoEvent.context in vultron/core/models/embargo_event.py, typed NonEmptyString, and nothing validates it as a case reference. This also gives pre-case embargo terms a legitimate home, which is what CONCERN-2215 asked for; what ADR-0096 declines is the pre-case phase, not the representation. |
|
EP-04-011 |
MUST | The pending revision that EP-04-003 registers at case creation MUST follow the same relay as any other revision proposal (EP-09). The CASE_MANAGER MUST propose it on behalf of the party whose terms lost the shortest-wins comparison, with that party in attributedTo, MUST commit it as a proposal entry — the committed relayed Invite is that entry, and no separate proposal entry is committed — MUST record it in the open-proposal record so that the owner's default selection (EP-08-002) can reach it, and MUST relay it as an Invite(EmbargoEvent) to the other party. The party whose terms lost is the proposer and is not invited (EP-09-002).A revision registered in the CASE_MANAGER's store alone is invisible to the party whose terms won and unreachable by the owner: it was never indexed, so the default selection could not name it, and no Invite was ever emitted for it (#3863). The creation-time revision is a revision like any other; the only thing special about it is who proposes it, and CM-24 already covers the CASE_MANAGER proposing on a participant's behalf. |
Refines: EP-04-003 Depends on: EP-09-002 — The creation-time revision is relayed by the same rule as any other revision. Depends on: EP-08-002 — Indexing the revision is what lets the default selection reach it. |
Revision Activation Consent Rule
| ID | Priority | Requirement | Related |
|---|---|---|---|
EP-05-001 |
MUST | When the case owner activates a revised embargo that ends later than the embargo it replaces (REVISE → ACTIVE), the CASE_MANAGER MUST NOT carry any participant's consent over to it: a participant is bound by the revised embargo only through an AGREED consent row for it, so each signatory to the replaced embargo that has not agreed to the revised one has lapsed (CM-18-001). A revised embargo that ends no later than the one it replaces MUST carry every existing signatory over: the CASE_MANAGER applies CARRY_OVER to the revised embargo's row for every participant whose row for the replaced embargo is AGREED, including one whose row for the revision is DECLINED (CM-10-001, CM-18-003, MSM-07-005). Apart from the case owner's own agreement, no activation advances anyone: a participant holding an AGREED row for the embargo now in force is its signatory by lookup.A participant who signed for terms T1 has not consented to longer terms T2; treating them as a signatory once T2 is in force would imply consent they never gave, and because consent is per embargo the lookup already says so — no write is needed to lapse them. Shorter terms ask nothing new: agreeing to N days is agreeing to every shorter period, the same containment that makes shortest-wins (EP-04-003) safe without asking anyone, so their agreement is carried over by writing the row. That holds for a participant that declined the shorter revision too: its decline objected to losing time, not to keeping the embargo, and the decline stays in the ledger (ADR-0122). The rule applies at activation rather than at proposal because the participant remains bound by T1 for as long as T1 is the active embargo (ADR-0093, ADR-0122). |
Implements: CM-18-001 |
EP-05-002 |
MUST NOT | A revision proposal (ACTIVE → REVISE) and any further counter-revision (REVISE → REVISE) MUST NOT change any participant's consent to the embargo in force. The proposer's row for the proposed embargo is marked AGREED (MSM-07-005) and every other participant gets an UNINVITED row for it (CM-18-001); every other row is left as it is until the case owner activates a revision (EP-05-001), and until then the prior embargo is in force and every signatory to it remains one.A participant cannot lapse until the embargo on the case differs from the one they agreed to. Lapsing on proposal stranded signatories when the revision was rejected, left participants still bound and still receiving embargoed content reported as not bound, and lapsed the proposer of the revision (ADR-0093, Concern #3884). With per-embargo rows a proposal only ever adds rows for the proposed embargo. |
Refines: EP-05-001 |
PEC Idempotency
| ID | Priority | Requirement | Related |
|---|---|---|---|
EP-06-001 |
MUST NOT | An idempotent retry or re-processing of a participant add-or-update MUST NOT remove or downgrade a consent row. Once a participant's row for an embargo has left UNINVITED (INVITED, AGREED, DECLINED or TIMED_OUT), that row MUST be preserved across retries.Dropping an AGREED or DECLINED row on retry silently revokes or erases consent that was already given. The positive-path idempotency rule (don't double-add) is covered by CM-13-005; this rule covers the negative path (don't downgrade). |
Refines: CM-13-005 Implements: CM-18-001 |
Embargo Invite RSVP Window Policy
Policy requirements for the embargo invite RSVP window: the fallback default when no deadline is on the wire, the minimum permitted window, and the clamp-on-receipt rule that neutralises coercively short deadlines. Codified by ADR-0065. Source: IDEA-2066.
| ID | Priority | Requirement | Related |
|---|---|---|---|
EP-07-001 |
MUST | The embargo invite RSVP window MUST be configurable. When an Invite(EmbargoEvent) carries no Invite.end_time, implementations MUST apply the configured default window, measured from the invitation's published timestamp. The default value SHOULD be 7 days, matching CM-18-002.Aligning the fallback with the pre-existing CM-18-002 policy window means an invitation that omits end_time behaves exactly as it does today, so adopting CM-28 changes no existing behaviour. See ADR-0065. |
Implements: CM-28-002 |
EP-07-002 |
MUST | A minimum RSVP window MUST be enforced. An inviting actor MUST NOT send an Invite(EmbargoEvent) whose Invite.end_time is less than the configured minimum after the invitation's published timestamp. The minimum is the configured window — SHOULD default to 72 hours — or the time remaining in the embargo, whichever is shorter.An unbounded-downward deadline weaponises the mechanism: a one-minute RSVP window formally invites a participant while guaranteeing they cannot answer, excluding them with the appearance of due process. See ADR-0065. |
Refines: EP-07-006 |
EP-07-003 |
MUST | When a receiving actor receives an Invite(EmbargoEvent) whose Invite.end_time is earlier than the applicable minimum window, it MUST clamp the effective deadline up to that minimum. The applicable minimum is the one computed by EP-07-002, so the clamp never raises the deadline past the embargo's own end.Clamping makes a coercive deadline ineffective rather than fatal. Rejecting the invitation outright would hand the sender exactly what a hostile sender wants — an invite that never takes effect — and would punish the invitee for the inviter's misbehaviour. See ADR-0065. |
Refines: EP-07-002 |
EP-07-006 |
MUST NOT | An RSVP deadline MUST NOT fall after the end of the embargo it concerns. Where a computed deadline — whether from an explicit Invite.end_time or from the CM-18-002 policy window — would fall after the embargo's end_time, it MUST be clamped down to that end_time.An invitee must be able to answer while the embargo still exists. Nothing previously compared the RSVP deadline against the embargo's own end, so an answer window could outlive the thing it was about. See ADR-0096. |
Constrains: CM-18-002 |
EP-07-004 |
MUST NOT | A receiving actor MUST NOT reject an Invite(EmbargoEvent) as malformed solely on the basis of a sub-minimum deadline.Rejecting the invitation would hand a hostile sender exactly what they want — an invite that never takes effect — and punishes the invitee for the inviter's misbehaviour. |
Refines: EP-07-003 |
EP-07-005 |
MUST | A receiving actor MUST log every deadline clamp it applies to an Invite(EmbargoEvent) deadline — both the EP-07-003 clamp up to the applicable minimum window and the EP-07-006 clamp down to the embargo's end_time.Logging the clamp makes the adjustment observable for auditing and debugging without rejecting the invitation. |
Refines: EP-07-003 Refines: EP-07-006 |
Open Proposal Resolution Order
How a Participant resolves two or more simultaneously open embargo proposals: earliest expiration first, with the remainder handled as revisions. Codified by ADR-0100, which also records why there is no multi-candidate poll activity. The heuristic itself predates this group as normative prose in docs/topics/process_models/em/defaults.md; stating it here is what makes it testable. Source: CONCERN-3433.
| ID | Priority | Requirement | Related |
|---|---|---|---|
EP-08-001 |
SHOULD | When two or more embargo proposals are open for a case and none has been accepted, a Participant SHOULD accept the proposal whose embargo end_time is earliest and propose the remainder as revisions. When two or more revisions are open against an active embargo, a Participant SHOULD accept or reject them individually in earliest-to-latest end_time order.Two parties proposing 30 and 60 days agree on days 1–30 and disagree only beyond that, so accepting the shorter proposal captures all mutual agreement immediately and leaves the contested remainder to negotiate as a revision. This is the Shortest Embargo Proposed Wins heuristic stated in docs/topics/process_models/em/defaults.md, derived in docs/topics/process_models/em/formal_model.md, and applied to case growth in docs/topics/process_models/em/working_with_others.md. |
|
EP-08-002 |
MUST | When an implementation must select among two or more open embargo proposals and the caller supplied no proposal identifier, it MUST select the earliest-expiring open proposal. It MUST NOT select by insertion or arrival order. A counter-proposal is emitted while the EM state remains PROPOSED (EMB-15-003), so arrival order places the superseded terms first. Selecting by arrival therefore accepts the terms the counter-proposal was sent to replace, which inverts EP-08-001 in exactly the situation that makes a default selection necessary. |
Refines: EP-08-001 |
EP-08-004 |
MUST | When an embargo terminates (EM ACTIVE or REVISE → EXITED), the implementation MUST close every open proposal, cancelling it in the same embargo register step that terminates the embargo (ADR-0122), not only the entry for the terminated embargo itself. Every proposal open while EM is ACTIVE or REVISE is a revision of the active embargo, and a revision of an embargo that no longer exists cannot be accepted.A case has at most one active embargo (VP-04-002), so while EM is ACTIVE or REVISE every open proposal is by construction a revision of that embargo; no field linking a revision to the embargo it revises is needed to know which proposals a termination decides. EP-08-003's text reaches the terminated embargo; this entry reaches the proposals about it. |
Refines: EP-08-003 Depends on: VP-04-002 — One active embargo at a time is what makes every open proposal a revision of it. |
Embargo Negotiation Relays Through the CASE_MANAGER
How an embargo proposal — the first one for a case or a revision of the active embargo — reaches every participant and every replica in the CASE_MANAGER topology. The behavioural specs (EMB-01 through EMB-06) are written in the voice of the formal protocol, in which every Participant is a peer and every Participant "receives EP" or "receives EV". Here a participant addresses its proposal to the CASE_MANAGER alone (PCR-08-001), so the Participant receiving EP or EV is the CASE_MANAGER on receipt, and every other participant learns the resulting case state from the ledger. The protocol interactions the behavioural specs define still happen: the CASE_MANAGER relays the proposal to the participants as an Invite, they answer the CASE_MANAGER, and the owner's answer decides. Each step is recorded in the ledger. Ledger entries carry case state to replicas and set no parse-and-respond expectation; they augment the negotiation as the record of the case, they do not replace it. Decided by ADR-0113 from Concerns #3892, #3836 and #3863 for revisions; generalised to every proposal, with the acknowledgement, RSVP-deadline, trigger-side write and invitee rules below, by its 2026-09-30 amendment from Concern #3918.
| ID | Priority | Requirement | Related |
|---|---|---|---|
EP-09-001 |
MUST | When the CASE_MANAGER receives an Invite(EmbargoEvent) for a case whose CS state is not yet public, exploit or attack, it MUST move the canonical case through EmbargoLifecycle — EM NONE → PROPOSED for the first proposal, ACTIVE → REVISE for a revision — and commit the proposal as a ledger entry. When the case is already at PROPOSED or REVISE (a counter-proposal) it MUST commit the proposal without a further EM transition. When the case is public, exploit or attack it MUST refuse the proposal (EMB-01-002, EMB-03-003). This is how EMB-01-001, EMB-01-003, EMB-03-001 and EMB-03-002 are realised in the CASE_MANAGER topology: the CASE_MANAGER on receipt, every other participant through ledger replay (EP-09-007).The received Invite tree stored the activity, moved the invitee's consent and committed an entry, but never moved the case, so the CASE_MANAGER's canonical record never showed that a revision was under negotiation when a non-owner proposed one, and everything keyed on EM.REVISE ran only in the proposer's store (#3892). The CASE_MANAGER is the sole writer of canonical case state (ADR-0108); if it does not move the case, no replica can. |
Implements: EMB-01-001 Implements: EMB-01-002 Implements: EMB-01-003 Implements: EMB-03-001 Implements: EMB-03-002 Implements: EMB-03-003 Depends on: EMB-18-001 — The EM write goes through EmbargoLifecycle, never a direct assignment. Depends on: RSH-08-003 |
EP-09-002 |
MUST | After committing a revision proposal, the CASE_MANAGER MUST emit an Invite(EmbargoEvent) for the revision to every case participant whose RM state is not RM.CLOSED, except the proposer and except the executing CASE_MANAGER itself, with actor set to the CASE_MANAGER's ID and attributedTo set to the proposer's ID (CM-24-001, CM-24-002), and MUST commit each emission as a ledger entry once it is in the outbox (SYNC-09-002). The CASE_MANAGER MUST NOT invite the proposer or itself. Instead of an Invite, the CASE_MANAGER MUST write consent rows at the same commit: the proposer's row for the proposed embargo AGREED (proposing is consenting), and, when the actor holding the role is also a participant with an embargo stake of its own (a role beyond CVDRole.CASE_MANAGER and CVDRole.COORDINATOR), its own row for that embargo — AGREED when it is the proposer, otherwise AGREED or DECLINED as its embargo-proposal policy call-out decides (EMB-15-001). The CASE_MANAGER role alone carries no embargo stake: no consent is solicited or recorded for a bare container, and when the actor holding the role also holds CVDRole.CASE_OWNER, that actor's consent is its decision as owner (EP-09-005), not written at the commit.Participants answer only an Invite addressed to them (EP-09-003), so without the relay nobody but the proposer would be asked. Proposing terms is consenting to them (ADR-0093), so the proposer is not invited. The CASE_MANAGER is the Case Owner's delegate that routes communications and manages ledger entries, with no view of its own on the embargo, and a container never mails an actor it hosts (ADR-0109). |
Refines: CM-24-001 Refines: CM-24-002 Depends on: PCR-08-007 — The same relay shape as the case Invite. |
EP-09-003 |
MUST NOT | A participant that receives an Invite(EmbargoEvent) MUST NOT write case EM state or any participant's consent state on receipt; it stores the Invite and answers it with Accept or Reject addressed to the CASE_MANAGER through its response decision (EMB-15). A participant MUST NOT treat an Announce(CaseLedgerEntry) that carries a proposal or an Invite as an invitation to answer: a participant answers only an Invite addressed to it. Consent is recorded when the CASE_MANAGER commits the answer, and the replica learns it from the ledger.Only the CASE_MANAGER turns an announcement into case state and every other participant takes case state from the ledger (ADR-0108, RSH-08-003). The ledger is a channel for case state, not a protocol interaction; giving an entry parse-and-respond semantics would let every future entry type become an implicit request and would fork the negotiation into two paths, one over Invites and one over entries. The interactions the behavioural specs define are the ones that happen; the ledger records them. |
Refines: RSH-08-003 Depends on: PCR-08-003 — The fan-out is the only mechanism by which participants learn accepted state. |
EP-09-004 |
MUST NOT | A revision Invite(EmbargoEvent) MUST NOT change the consent of a participant that is already a signatory to the active embargo. The PEC INVITE trigger is applied to the participant's row for the revision (CM-18-003), so a signatory's row for the revision moves from UNINVITED to INVITED and it keeps its AGREED row for the active embargo; its answer to the revision is recorded on the revision's row and binds it to nothing new until the owner activates the revision (EP-05-001, EP-05-002).A signatory is bound by the active embargo for as long as it is active; a proposal to revise it asks nothing of them yet (ADR-0093). Because consent is per embargo (ADR-0122) the Invite lands on its own row and cannot touch the row that binds the signatory, so a receive tree that applies INVITE for every recipient no longer faults on exactly the participants a revision most concerns. |
Refines: EP-05-002 Depends on: CM-18-003 |
EP-09-005 |
MAY | The case owner MAY accept or reject an open revision at any time, without waiting for any other participant to answer the revision Invite. The owner's Accept(EmbargoEvent, target=Case) activates the revision and the owner's Reject(EmbargoEvent, target=Case) keeps the prior terms (MSM-07-008, MSM-07-009); either is committed as a ledger entry and decides the embargo. The owner's Accept or Reject of the revision Invite is only its own consent (MSM-07-003, MSM-07-004).Only the case owner's answer changes the embargo on the case; the other participants' answers inform that decision, they do not make it (ADR-0093). The Invites are not a vote: they gather the consent records the activation cascade reads (EP-05-001). The owner's decision settles the embargo; the answers settle who is bound by it. |
|
EP-09-006 |
SHOULD | The case owner SHOULD wait for at least some participants to answer a revision Invite before accepting or rejecting the revision, so as to gauge consensus (VP-08-003). The protocol defines no quorum, no vote and no waiting period; which answers, how many and how long are actor policy. Consensus on embargo terms is the protocol's stated norm (VP-08-003), and "quorum" is deliberately undefined so that different CVD communities can adopt different decision rules (VP-09-007). A fixed threshold here would take that room away. Leaving it to actor policy makes the waiting decision a natural call-out point rather than a rule. |
Refines: VP-08-003 Depends on: VP-09-007 — VP-09-007's rationale is where the protocol leaves "quorum" undefined; it stays undefined here too. |
EP-09-008 |
MUST | A trigger use case MUST write shared EM state — proposing, accepting, rejecting or terminating an embargo — only when the executing actor holds CVDRole.CASE_MANAGER for the case, inside the same role gate received trees use (BT-17-001). When it does not, the trigger MUST emit its activity to the CASE_MANAGER, record it in the pending-assertion store (SYNC-11-002), write no EM state, and emit no case-status declaration of an EM state; the actor's replica moves when the CASE_MANAGER's commit is announced (EP-09-007). The CASE_MANAGER's Reject of a refused proposal closes the pending entry, and so does its ER to an Accept it refuses once P/X/A is set (EMB-02-002): a participant takes a Reject(Invite(EmbargoEvent)) from the CASE_MANAGER and from nobody else (PCR-08-001, HP-01-006).Every embargo trigger wrote its own local EM state before the CASE_MANAGER had answered, then declared that state to the manager, and nothing corrected the write when the manager refused, so a proposer's replica could sit at PROPOSED while the canonical case never moved (#3918). The manager is the one writer of shared case state (ADR-0108); a participant's replica of it comes from the ledger like everyone else's. |
Depends on: EP-09-007 Depends on: SYNC-11-002 Depends on: BT-17-001 Refines: RSH-08-003 — The receive-side rule, applied to the trigger side for shared EM state. |
EP-09-009 |
MUST NOT | The CASE_MANAGER's commit of a received embargo activity, fanned out as Announce(CaseLedgerEntry), is the acknowledgement the EMB behavioural requirements express as "emit EK". Implementations MUST NOT add an EK message type, pattern or emission to acknowledge an embargo activity.No EK exists in the production stack and MSM-02-009 records that its purpose is served by ledger hash-chain continuity. Under the relay every accepted embargo activity is committed and announced, so the record the specs want the receiver to produce already exists; a second acknowledgement would be a message with no reader (#3918). |
Refines: MSM-02-009 Implements: EMB-09-001 |
EP-09-010 |
MUST | The invitee of an Invite(EmbargoEvent) is its sole to recipient. A receiving actor MUST resolve the invitee from that single recipient, MUST refuse an Invite that names no to recipient or more than one as a misrouting (OX-08-001), and MUST NOT infer the invitee from the receiving actor.Every emitter sends a single-recipient Invite — a participant to the CASE_MANAGER, the CASE_MANAGER to one participant per relayed Invite (EP-09-002) — so the multi-recipient resolution built for a shape nothing emits was dead code, and its fallback to the receiving actor put the deadline on the enforcer's own record (ISSUE-2762). Refusing names the defect; guessing hides it. |
Depends on: EP-09-002 |
EP-09-011 |
MUST | When the CASE_MANAGER admits a participant (Accept(Invite(stub))) while an embargo proposal is open, it MUST relay an Invite(EmbargoEvent) for every open proposal to the joiner and commit each as a ledger entry, after the joiner holds the case and the prior ledger. Each Invite has actor set to the CASE_MANAGER's ID and attributedTo set to the proposal's original proposer (EP-09-002). A proposal the joiner already holds a consent row for is not relayed again. The joiner's row for the active embargo is unchanged (EP-09-004), so a joiner that accepts the revision is a signatory of it at activation and one that does not lapses (ADR-0093, EP-05-001).A joiner is seated after the relay of a proposal (EP-09-002), so it was never on the roster when the Invite went out, and nothing else would ask it. A new participant becomes a signatory to the embargo in effect when it joins, and is asked about a revision on the table exactly like the participants who were present when it was proposed. |
Refines: EP-09-002 Depends on: CM-11-001 |
EP-09-012 |
MUST | The accept and reject embargo triggers MUST let a participant answer the embargo in force when the caller names the Invite(EmbargoEvent) to answer and the participant's consent row for that Invite's embargo is INVITED or TIMED_OUT (CM-18-001). The trigger MUST resolve the named Invite from the participant's own record of the Invite it received, not from the record of open proposals, and MUST raise a not-found error when the participant holds no such Invite or its row is in any other state. When the caller names no Invite, the trigger MUST select among the open proposals only (EP-08-002) and MUST NOT fall back to the embargo in force.Activation takes a proposal out of the record of open proposals (EP-08-003), so a participant relayed the proposal (EP-09-011) that had not answered when it was activated, or whose reply deadline has since passed (EMB-17), can no longer name it there. The receive side already honours the answer (MSM-07-003, EMB-17-002). A no-name call never falls back to the embargo in force, because answering an embargo is an intentional act. |
Constrains: EP-08-002 Depends on: MSM-07-003 Depends on: EMB-17-002 |
EP-09-013 |
MUST | A participant whose consent row for the embargo in force is AGREED MUST be able to withdraw from that embargo by a trigger that sends Leave(EmbargoEvent) naming the embargo in force to the CASE_MANAGER (MSM-07-010, PCR-08). The trigger needs no Invite: it MUST resolve the embargo from the case's active embargo, and MUST refuse with a reported reason when no embargo is in force, when the participant's row for it is not AGREED, or when the participant is the case owner. Neither the accept nor the reject embargo trigger MUST withdraw from the embargo in force.VP-13-017 lets a participant end its own compliance at any time, and VP-13-008 asks that it say so. A signatory made by seeding (CM-14-005) or by joining (CM-11-001) holds no Invite(EmbargoEvent) for the embargo in force, but every participant holds the embargo itself. Keeping withdrawal out of the reject trigger means a no-name reject still refuses an open proposal (EP-08-002) and never withdraws a signatory. |
Depends on: MSM-07-010 Refines: VP-13-017 |
Encryption
Requirements for encrypted messaging and actor key management using ActivityPub extension conventions. Ensures that sensitive vulnerability information can be protected in transit between actors.
File ID: ENC
Key Management
| ID | Priority | Requirement | Related |
|---|---|---|---|
ENC-01-001 |
MUST | Each actor MUST generate a unique asymmetric key pair when its identity is first provisioned. Key material binds to an actor identity, not to a case. An actor that enacts CVDRole.CASE_MANAGER for several cases uses one key pair for all of them, so per-case key separation is a consequence of deployment topology rather than a key-binding rule. Regenerating a key pair would invalidate any peer's cached public key and break encrypted communication mid-case. See notes/encryption.md for the topology discussion. |
Implements: VP-15-004 |
ENC-01-002 |
MUST | Each actor MUST publish its public key in its ActivityPub actor profile using the publicKey property per ActivityPub HTTP Signatures conventions |
|
ENC-01-003 |
MUST | The actor enacting CVDRole.CASE_MANAGER MUST share its public key with each case participant when they are added to the case |
|
ENC-01-004 |
MUST NOT | Private keys MUST be stored securely and MUST NOT be logged or included in API responses | |
ENC-01-005 |
MUST | An actor's asymmetric key pair MUST be persisted and reused across process restarts for the lifetime of that actor identity. Regenerating on restart would invalidate any peer's cached public key and break encrypted communication mid-case. |
Refines: ENC-01-001 |
ENC-01-006 |
MUST NOT | A new key pair MUST NOT be generated on process resumption for an existing actor identity. Generating a new key pair on restart breaks any encrypted channel established with the previous key, invalidating cached public keys held by case participants. |
Refines: ENC-01-001 |
Message Encryption
| ID | Priority | Requirement | Related |
|---|---|---|---|
ENC-02-001 |
MAY | Case participants MAY encrypt messages sent to the CASE_MANAGER using the CASE_MANAGER's published public key | Implements: VP-15-004 |
ENC-02-002 |
SHOULD | When sending messages to case participants, the CASE_MANAGER SHOULD encrypt each outbound message to the recipient's public key individually (one encrypted payload per recipient) The specific encryption scheme (e.g., ECDH key agreement + AES-256-GCM, RSA-OAEP) is intentionally deferred pending a protocol-level agreement. Until a scheme is standardized, the choice is implementation-defined; interoperability between independently-built implementations is not yet a requirement. |
Implements: VP-15-004 |
Event-Driven Control Flow
Defines the event-driven processing model for Vultron actors: how primary events enter the system, how cascading consequences are produced automatically, and what constraints apply to demo scripts and integration tests.
File ID: EDF
Cascade Implementation
Handler Protocol
Handler use-case classes process VultronEvent domain objects and implement protocol business logic. All handlers follow a common contract defined by the UseCase Protocol.
File ID: HP
Protocol Semantics
Handler Signature
| ID | Priority | Requirement | Related |
|---|---|---|---|
HP-01-006 |
MUST | A handler MUST establish that the sender of a received activity is entitled to make its assertion before any write, outbound activity or ledger commit, and MUST report REFUSED with a reason naming the missing entitlement when it cannot. Entitlement is the sender's standing, not the receiver's: the governing group's sender clause names it per message (CM-11-017, CM-16-019, CM-21-011, CM-30-001, CM-30-002, CM-33-002, CM-33-004, CM-33-005, CP-06-005, HP-01-010, RMB-08-004, SYNC-03-005), and a receiver role check such as the CASE_MANAGER gate (BT-17-001) never satisfies it.A received activity is an assertion about the sender's state (HP-00-001), so the first question is whether the sender may assert it. An audit found handlers that let any actor claim ownership, admit itself, or obtain the full ledger, each behind a CASE_MANAGER gate that checked only the receiver. See #3733 and ADR-0115. |
Depends on: HP-00-001 Refines: HP-01-005 |
HP-01-010 |
MUST | A receiver MUST accept a new report, sent as Offer(VulnerabilityReport) (RS) or Create(VulnerabilityReport), from any actor, whether or not that actor takes part in any case, and MUST NOT refuse it on the sender's standing. Only the addressing rules apply to it (HP-01-005, HP-09-001).A report precedes any case, so the sender has no participant record or role to hold, and anyone who finds a vulnerability must be able to report it. Decided by the maintainer on #4325. |
Refines: HP-01-006 |
Offer Recipient Addressing
| ID | Priority | Requirement | Related |
|---|---|---|---|
HP-09-001 |
MUST | Handlers processing an inbound Offer activity MUST determine case-creation eligibility from the to field only |
|
HP-09-002 |
MUST NOT | The target field MUST NOT be used to determine case-creation eligibility for Offer activities |
Depends on: VAM-01-008 |
Unsupported Addressing Fields
| ID | Priority | Requirement | Related |
|---|---|---|---|
HP-10-001 |
MUST | When a receiving actor finds itself in the cc field of an inbound Offer(Report) activity, the handler MUST log a WARNING and discard the activity without creating a casecc addressing has no defined handler semantics in the current protocol version. Implementing an "informational receive" use case for cc recipients would require defining what "informational only" means, which is deferred. Deliberate simplification. |
Refines: HP-09-001 |
HTTP Protocol Requirements
HTTP protocol requirements for the Vultron API, covering status codes, headers, and content negotiation.
File ID: HTTP
Content-Type Validation
Response Headers
Inbox Endpoint
The inbox endpoint is the primary entry point for actor-to-actor communication in Vultron. It receives ActivityStreams activities via HTTP POST, validates them, and queues them for asynchronous processing.
File ID: IE
Activity Validation
| ID | Priority | Requirement | Related |
|---|---|---|---|
IE-04-001 |
MUST | The endpoint MUST validate activity structure via schema validation |
Activity Addressing
| ID | Priority | Requirement | Related |
|---|---|---|---|
IE-11-001 |
MUST | The inbox endpoint MUST refuse any Activity whose addressing fields (to, cc, bto, bcc) are all non-empty and none of them names the receiving actor, and MUST NOT write anything to the receiving actor's store for it. Per-actor DataLayer isolation (ADR-0073) makes each actor's store the authoritative answer to "whose replica is this?". That invariant is only trustworthy if the store's owner was the intended recipient of every Activity it processed. Which HTTP response carries the refusal is this codebase's choice, not a protocol rule: it answers HTTP 400 in place of the 202 (IE-07-002, project), so the sender learns of the refusal from the answer to its own delivery. |
Depends on: IE-07-002 — the HTTP status that carries the refusal is this codebase's choice |
IE-11-002 |
MUST | The inbox endpoint MUST accept Activities whose addressing fields (to, cc, bto, bcc) are all absent or unresolvable (Liberal Accept). An address is unresolvable if it cannot be confirmed to refer to a specific individual actor — for example, a collection URI such as {case_id}/participants.Requiring explicit addressing would break flows where Activities are sent without formal addressing (e.g., internal relay, legacy senders). The narrowest thing that can be refused is provable exclusion; uncertainty defaults to acceptance. Fan-out deliveries using collection URIs (e.g. {case_id}/participants) cannot be resolved to individual actors at inbox processing time and must therefore be accepted. |
|
IE-11-003 |
MUST | The inbox endpoint MUST consider all four addressing fields — to, cc, bto, bcc — when determining whether the receiving actor is named as a recipient. A match on any one field is sufficient. Vultron chooses which ActivityStreams audience-targeting properties count as addressing, so the rule is its own (maintainer ruling, 2026-10-15, #4673). Vultron defines no use for audience on any object or activity, so an actor named only there is not addressed. Vultron sets only to (OX-08), but another implementation may name this actor only in cc, bto or bcc; checking only to would refuse it wrongly. A cc-only address is HP-10's question. |
Lifecycle-Staged Domain Types
Rules for lifecycle-staged domain types anchored on guaranteed-field changes. A lifecycle milestone earns a distinct staged type only when it changes the set of required (non-None) fields; milestones that only gate permitted actions are modeled as predicates and preconditions rather than new types.
File ID: LST
VulnerabilityCase Staged Types
| ID | Priority | Requirement | Related |
|---|---|---|---|
LST-02-001 |
MUST | The VulnerabilityCase staged family MUST consist of exactly three types: IncomingReport (report data, no case), Case (a case exists), and EmbargoedCase (an embargo is active), because only these boundaries change the guaranteed-field set.Case existence and embargo-active each add a guaranteed field; no other case-universal milestone does. See ADR-0033. |
|
LST-02-002 |
MUST | A Case MUST guarantee at least one vulnerability report, the reporter and receiver participants (at least two parties), and a seeded case_statuses list.ADR-0041 (superseding ADR-0015) requires the CASE_MANAGER to author all initialization entries when accepting a CaseProposal, so these structural minimums are guaranteed for any case that exists. |
Depends on: ARCH-10-001 |
LST-02-003 |
MUST | An EmbargoedCase MUST guarantee active_embargo is non-None and em_state is in {ACTIVE, REVISE}.The EM machine never returns to NONE/PROPOSED once ACTIVE, so this is a monotonic milestone with a genuine type-level non-None guarantee. |
Read-Boundary Promotion and Transition Model
| ID | Priority | Requirement | Related |
|---|---|---|---|
LST-05-002b |
MUST | The staged type MUST always be re-derived from the base case data at read time; the base case data is the single source of truth for stage determination. | Refines: LST-05-002 |
LST-05-003 |
MUST | Staged types MUST remain round-trippable through the DataLayer: a persisted staged object rehydrates to the base type and re-validates to its staged type without loss. Rehydration always yields a valid object; the staged-type guarantee must survive persistence. See ADR-0033. |
Protocol Shorthand to MessageSemantics Mapping
Authoritative normative bridge from protocol message shorthand labels (RS, EP, CV, etc.) used in behavioral conformance specs to their MessageSemantics enum values and corresponding AS2 wire-format specifications. Each entry provides one row in the three-hop chain: shorthand to MessageSemantics enum value to AS2 wire form. The correspondence is many-to-many in both directions: several shorthands may collapse onto one wire activity (CV/CF onto vfState, CP/CX/CA onto pxaState, EV/EJ/EC onto EP/ER/EA), and one shorthand may expand across several wire activities (GI). Shorthands with no dedicated dispatch value are the protocol-layer error and acknowledgment signals; these are not unimplemented — the purposes they name are served by mechanisms partitioned on a different axis, specified normatively in MSM-05.
File ID: MSM
RM Protocol Message Shorthands
Normative shorthand-to-semantic mappings for Report Management (RM) protocol messages, covering RS, RI, RV, RD, RA, RC, RE, and RK.
| ID | Priority | Requirement | Related |
|---|---|---|---|
MSM-01-001 |
MUST | Protocol shorthand RS (Report Submission) MUST dispatch as MessageSemantics.SUBMIT_REPORT; its AS2 wire form is Offer(VulnerabilityReport) per VAM-02-002. |
Derives from: VAM-02-002 |
MSM-01-002 |
MUST | Protocol shorthand RI (Report Invalid) MUST dispatch as MessageSemantics.INVALIDATE_REPORT; its AS2 wire form is TentativeReject(Offer(VulnerabilityReport)) per VAM-02-005. This is the direct report recipient's RI; a participant that joined through an Invite sends RI as the full-case Invite reply of VAM-04-013. |
Derives from: VAM-02-005 |
MSM-01-003 |
MUST | Protocol shorthand RV (Report Valid) MUST dispatch as MessageSemantics.VALIDATE_REPORT; its AS2 wire form is Accept(Offer(VulnerabilityReport)) per VAM-02-004. This is the direct report recipient's RV; a participant that joined through an Invite sends RV as the full-case Invite reply of VAM-04-012. |
Derives from: VAM-02-004 |
MSM-01-004 |
MUST | Protocol shorthand RD (Report/Case Deferred) MUST dispatch as MessageSemantics.DEFER_CASE; its AS2 wire form is Ignore(VulnerabilityCase) per VAM-03-004.A deferred report indicates the receiving actor will not engage with the case at this time. The wire representation uses Ignore(VulnerabilityCase) rather than a report-level verb because deferral is a case-participation decision, not a report-validity judgment. |
Derives from: VAM-03-004 |
MSM-01-005 |
MUST | Protocol shorthand RA (Report/Case Accepted) MUST dispatch as MessageSemantics.ENGAGE_CASE; its AS2 wire form is Join(VulnerabilityCase) per VAM-03-003.Accepting a report means the receiving actor commits to engaging with the vulnerability case. The wire verb Join captures the case-participation decision, symmetric with Ignore for RD. |
Derives from: VAM-03-003 |
MSM-01-006 |
MUST | Protocol shorthand RC (Report Closed) MUST dispatch as MessageSemantics.CLOSE_REPORT; its AS2 wire form is Reject(Offer(VulnerabilityReport)) per VAM-02-006. This is the direct report recipient's RC; a participant that joined through an Invite sends RC as the full-case Invite reply of VAM-04-014. |
Derives from: VAM-02-006 |
MSM-01-008 |
MUST | Protocol shorthand RK (Report Management Acknowledgment) MUST dispatch as MessageSemantics.ACK_REPORT; its AS2 wire form is Read(Offer(VulnerabilityReport)) per VAM-02-003. |
Derives from: VAM-02-003 |
EM Protocol Message Shorthands
Normative shorthand-to-semantic mappings for Embargo Management (EM) protocol messages, covering EP, EA, EV, EJ, EC, ER, ET, EE, and EK. Embargo revision shorthands (EV, EJ, EC) share wire formats with their initial-proposal counterparts (EP, ER, EA); the distinction is contextual rather than structural.
| ID | Priority | Requirement | Related |
|---|---|---|---|
MSM-02-001 |
MUST | Protocol shorthand EP (Embargo Proposed) MUST dispatch as MessageSemantics.INVITE_TO_EMBARGO_ON_CASE; its AS2 wire form is Invite(Event)[context=VulnerabilityCase] per VAM-05-005. |
Derives from: VAM-05-005 |
MSM-02-002 |
MUST | Protocol shorthand EA (Embargo Accepted) MUST dispatch as MessageSemantics.ACCEPT_INVITE_TO_EMBARGO_ON_CASE; its AS2 wire form is Accept(Invite(Event)[context=VulnerabilityCase]) per VAM-05-006. |
Derives from: VAM-05-006 |
MSM-02-004 |
MUST | Protocol shorthand EJ (Embargo Revision Rejected) MUST dispatch as MessageSemantics.REJECT_INVITE_TO_EMBARGO_ON_CASE; its AS2 wire form is Reject(Invite(Event)[context=VulnerabilityCase]) per VAM-05-007 — identical to ER.EJ is an alias for ER at the wire layer. See MSM-02-003 rationale. |
Derives from: VAM-05-007 |
MSM-02-005 |
MUST | Protocol shorthand EC (Embargo Revision Accepted) MUST dispatch as MessageSemantics.ACCEPT_INVITE_TO_EMBARGO_ON_CASE; its AS2 wire form is Accept(Invite(Event)[context=VulnerabilityCase]) per VAM-05-006 — identical to EA.EC is an alias for EA at the wire layer. See MSM-02-003 rationale. |
Derives from: VAM-05-006 |
MSM-02-006 |
MUST | Protocol shorthand ER (Embargo Rejected) MUST dispatch as MessageSemantics.REJECT_INVITE_TO_EMBARGO_ON_CASE; its AS2 wire form is Reject(Invite(Event)[context=VulnerabilityCase]) per VAM-05-007. |
Derives from: VAM-05-007 |
MSM-02-007 |
MUST | Protocol shorthand ET (Embargo Terminated) MUST dispatch as MessageSemantics.REMOVE_EMBARGO_EVENT_FROM_CASE; its AS2 wire form is Remove(Event) per VAM-05-003. |
Derives from: VAM-05-003 |
CS Protocol Message Shorthands
Normative shorthand-to-semantic mappings for CVD Case State (CS) protocol messages, covering CV, CF, CD, CP, CX, CA, CE, and CK. CS status shorthands are split across two semantics by dimension scope, because the CS hypercube mixes participant-scoped and case-scoped dimensions: the participant-scoped V/F/D dimensions (CV, CF, CD) dispatch as ADD_PARTICIPANT_STATUS_TO_PARTICIPANT and are encoded in the ParticipantStatus payload, while the case-scoped P/X/A dimensions (CP, CX, CA) dispatch as ADD_CASE_STATUS_TO_CASE and are encoded in the CaseStatus payload. There are no case-level V/F/D states (ADR-0075). In both groups the specific state transition is encoded in the object payload rather than in the activity type.
| ID | Priority | Requirement | Related |
|---|---|---|---|
MSM-03-001 |
MUST | Protocol shorthand CV (Vendor Aware) MUST dispatch as MessageSemantics.ADD_PARTICIPANT_STATUS_TO_PARTICIPANT; its AS2 wire form is Add(ParticipantStatus)[target=CaseParticipant] per VAM-08-004. The ParticipantStatus payload encodes vendor awareness in the vfState field (CS_vf), advancing the vendor-path ladder from vf to Vf.The V dimension is participant-scoped, not case-scoped: it records whether a specific vendor is aware. Per ADR-0075 there is no case-level V/F/D state, and a case-level Add(CaseStatus) cannot express which vendor became aware, which is the purpose of the V dimension in MPCVD. Corrected from an earlier revision that mapped CV to ADD_CASE_STATUS_TO_CASE with a nonexistent vendor_aware payload field; see ADR-0083. |
Derives from: VAM-08-004 |
MSM-03-002 |
MUST | Protocol shorthand CF (Fix Ready) MUST dispatch as MessageSemantics.ADD_PARTICIPANT_STATUS_TO_PARTICIPANT; its AS2 wire form is Add(ParticipantStatus)[target=CaseParticipant] per VAM-08-004. The ParticipantStatus payload encodes fix readiness in the vfState field (CS_vf), advancing the vendor-path ladder from Vf to VF.The F dimension shares the participant-scoped vendor-path sub-machine with V ( CS_vf: vf → Vf → VF), so fix readiness is recorded against the specific vendor that produced the fix. Same scope reasoning as MSM-03-001. Corrected from an earlier revision that mapped CF to ADD_CASE_STATUS_TO_CASE with a nonexistent fix_ready payload field; see ADR-0083. |
Derives from: VAM-08-004 |
MSM-03-003 |
MUST | Protocol shorthand CD (Fix Deployed) MUST dispatch as MessageSemantics.ADD_PARTICIPANT_STATUS_TO_PARTICIPANT; its AS2 wire form is Add(ParticipantStatus)[target=CaseParticipant] per VAM-08-004. The ParticipantStatus payload encodes deployment in the dState field (CS_d), advancing the deployer-path ladder from d to D.The D dimension is participant-scoped on the deployer, and ADR-0075 split it into its own sub-machine ( CS_d) precisely because its scope differs from the vendor path. as_ParticipantStatus.d is None for non-DEPLOYER participants (structurally enforced). Corrected from an earlier revision that mapped CD to ADD_CASE_STATUS_TO_CASE with a nonexistent fix_deployed payload field; see ADR-0083. |
Derives from: VAM-08-004 |
MSM-03-004 |
MUST | Protocol shorthand CP (Public Aware) MUST dispatch as MessageSemantics.ADD_PARTICIPANT_STATUS_TO_PARTICIPANT when a participant reports it, which it does only in the CaseStatus that its Add(ParticipantStatus)[target=CaseParticipant] embeds (RSH-04-001), and as MessageSemantics.ADD_CASE_STATUS_TO_CASE when it is carried in Add(CaseStatus)[target=VulnerabilityCase] (VAM-08-002), the CASE_MANAGER's record of the case status it adopted (RSH-04-001, RSH-04-005). In both forms the CaseStatus payload encodes the public_aware state flag.A participant embeds its suggested CaseStatus in Add(ParticipantStatus) (RSH-04-001), so the formal message has one wire form per sender. The participant's is its view, not a change: the CASE_MANAGER adopts it, by default only with the Case Owner's authorization (RSH-07-001), as its own Add(CaseStatus) ledger entry (RSH-01-003), and a receiver refuses an Add(CaseStatus) from anyone else (RSH-04-005). Each form dispatches to its own entry (MSM-08-001). |
Derives from: VAM-08-002 Depends on: RSH-04-001 |
MSM-03-005 |
MUST | Protocol shorthand CX (Exploit Published) MUST dispatch as MessageSemantics.ADD_PARTICIPANT_STATUS_TO_PARTICIPANT when a participant reports it, which it does only in the CaseStatus that its Add(ParticipantStatus)[target=CaseParticipant] embeds (RSH-04-001), and as MessageSemantics.ADD_CASE_STATUS_TO_CASE when it is carried in Add(CaseStatus)[target=VulnerabilityCase] (VAM-08-002), the CASE_MANAGER's record of the case status it adopted (RSH-04-001, RSH-04-005). In both forms the CaseStatus payload encodes the exploit_published state flag.A participant embeds its suggested CaseStatus in Add(ParticipantStatus) (RSH-04-001), so the formal message has one wire form per sender. The participant's is its view, not a change: the CASE_MANAGER adopts it, by default only with the Case Owner's authorization (RSH-07-001), as its own Add(CaseStatus) ledger entry (RSH-01-003), and a receiver refuses an Add(CaseStatus) from anyone else (RSH-04-005). Each form dispatches to its own entry (MSM-08-001). |
Derives from: VAM-08-002 Depends on: RSH-04-001 |
MSM-03-006 |
MUST | Protocol shorthand CA (Attacks Observed) MUST dispatch as MessageSemantics.ADD_PARTICIPANT_STATUS_TO_PARTICIPANT when a participant reports it, which it does only in the CaseStatus that its Add(ParticipantStatus)[target=CaseParticipant] embeds (RSH-04-001), and as MessageSemantics.ADD_CASE_STATUS_TO_CASE when it is carried in Add(CaseStatus)[target=VulnerabilityCase] (VAM-08-002), the CASE_MANAGER's record of the case status it adopted (RSH-04-001, RSH-04-005). In both forms the CaseStatus payload encodes the attacks_observed state flag.A participant embeds its suggested CaseStatus in Add(ParticipantStatus) (RSH-04-001), so the formal message has one wire form per sender. The participant's is its view, not a change: the CASE_MANAGER adopts it, by default only with the Case Owner's authorization (RSH-07-001), as its own Add(CaseStatus) ledger entry (RSH-01-003), and a receiver refuses an Add(CaseStatus) from anyone else (RSH-04-005). Each form dispatches to its own entry (MSM-08-001). |
Derives from: VAM-08-002 Depends on: RSH-04-001 |
Fault and Acknowledgement Mechanism Evolution
Normative record of how the formal protocol's per-model error and acknowledgement shorthands (RE, EE, CE, GE and RK, EK, CK, GK) are realized in the AS2 wire vocabulary. The formal set partitions both concerns by which state machine the message belonged to; the implementation partitions faults by failure mode and acknowledges replicated state cumulatively. These are orthogonal axes, so the shorthands have no one-to-one wire counterparts even though the behaviours they describe are implemented.
| ID | Priority | Requirement | Related |
|---|---|---|---|
MSM-05-001 |
MUST | Fault reporting MUST be partitioned by failure mode rather than by state machine, using exactly three mechanisms: Create(ProcessingFault) when a message was received but not understood; as:Reject when a message was received and understood but declined; and Add(Note)[target=VulnerabilityCase] when the condition requires narrative explanation.Failure mode is actionable to a receiver in a way that originating state machine is not. Not-understood calls for a format or version fix, declined calls for a protocol-level decision, and needs-explanation calls for a human. The formal RE, EE, CE and GE partition carries no such signal, which is why it was not reproduced. |
|
MSM-05-002 |
MUST | Acknowledgement of replicated case-ledger state MUST be cumulative and implicit via hash-chain continuity. A receiver whose prev_log_hash matches its local tail MUST NOT emit an acknowledgement, and MUST emit Reject(CaseLedgerEntry) on a mismatch, whereupon the CASE_MANAGER replays all entries after the last accepted hash.A matching prev_log_hash already proves receipt of every prior entry, so per-message positive acknowledgement would be redundant. This is negative acknowledgement with gap-fill replay, structurally closer to a cumulative ACK or SACK scheme than to the formal per-message EK, CK and GK design. RK remains a real wire activity (MSM-01-008) because report submission is not ledger-replicated. |
Refines: SYNC-15-003 |
MSM-05-005 |
MUST | All protocol-significant activities MUST be committed to the canonical case ledger by the CASE_MANAGER before fan-out to participant actors. No protocol-significant activity bypasses the ledger replication path. The hash-chain acknowledgement model (MSM-05-002) is only complete if every activity a participant needs to acknowledge reaches them via an Announce(CaseLedgerEntry). Activities that bypassed the ledger would have no acknowledgement mechanism. Report submission (RS) is the only activity not carried by the ledger; it uses RK (MSM-01-008) instead, which is the intended exception rather than a gap. |
|
MSM-05-006 |
MAY | A conformant implementation MAY rely solely on Announce(CaseLedgerEntry) deliveries for liveness inference, without emitting a dedicated periodic heartbeat. Silence between Announce deliveries is therefore an accepted protocol property: a participant that is online but has received all entries is indistinguishable from one that is unreachable. This does not preclude a future revision adding a normative heartbeat requirement. The hash-chain NAK model (MSM-05-002) provides implicit acknowledgement only when new entries arrive. Between deliveries, silence and unreachability are observationally equivalent. Documenting this as a deliberate MAY-level property (rather than a defect) closes the liveness question raised by CONCERN-3006 without foreclosing a future heartbeat mechanism (CaseActorHeartbeat is already sketched in notes/case-actor-cluster.md for the multi-node CASE_MANAGER cluster design). |
|
MSM-05-003 |
MUST NOT | Implementations MUST NOT infer error semantics from the as:Reject verb alone, because as:Reject also carries legitimate protocol rejections, including closing a report and declining an embargo invitation, a case invitation, a case proposal or a case-ownership transfer.as:Reject is overloaded across error reporting and ordinary protocol refusal. A receiver that treats every as:Reject as a fault will misclassify routine protocol traffic as errors. |
Participant Embargo Consent (PEC) State Machine Coupling to EM Wire Activities
Normative record of how each EM wire activity drives the per-participant, per-embargo Participant Embargo Consent (PEC) rows as a side-effect, and which consent positions are derived rather than written. PEC is participant-local state tracking each actor's relationship to each embargo in the case's embargo register; its transitions are not independent protocol messages and have no formal shorthands in the 28-shorthand set. A participant's consent to an embargo is one row holding UNINVITED, INVITED, AGREED, DECLINED or TIMED_OUT; whether the participant is bound by the current active embargo is a lookup of the active embargo's row, and a lapse is read from the rows and the register when a longer embargo replaces the one the participant agreed to. Unlike RM state (self-reported by the participant) and VF/D state (self-reported by vendor/deployer), consent rows are set by the CASE_MANAGER (CM-28-003) based on observed participant behavior — incoming Accept/Reject wire activities and timer-enforced invitation expiry — and replayed on each replica. Normative authority: specs/case-management.yaml CM-18. Decisions: ADR-0048, ADR-0091, ADR-0118, ADR-0122.
| ID | Priority | Requirement | Related |
|---|---|---|---|
MSM-07-001 |
MUST | The Participant Embargo Consent (PEC) state machine MUST NOT be assigned any formal shorthand in the 28-shorthand protocol message set; all PEC transitions are either side-effects of EM wire activities (MSM-07-002 through MSM-07-005, MSM-07-008) or timer-based internal events (MSM-07-007) with no dedicated wire message or dispatch value of their own. The CASE_MANAGER's Create(EmbargoConsent) and Update(EmbargoConsent), which record a consent row it brings into existence or changes as its own ledger entry (CLP-07-013), are case-file record changes, not PEC transitions: they carry no shorthand, and the registry labels them create_embargo_consent and update_embargo_consent with no received use case (ADR-0140).PEC is participant-local state tracking each actor's relationship to the shared EM state, not a parallel message-driven machine. Extending the formal set with PEC-specific shorthands would imply dedicated wire messages where none exist and none are needed — the EM messages already carry the PEC signal as a side-effect. Documenting PEC as a side-effect is the faithful description of the design. A ledger entry's semantic type is the registry's label for its payload (ADR-0140), so the CASE_MANAGER's consent-row records need registry entries; they name the record, not a PEC trigger, and no receiver dispatches on them. |
Derives from: CM-18-003 |
MSM-07-002 |
MUST | The wire activity INVITE_TO_EMBARGO_ON_CASE (EP wire form: Invite(Event)[context=VulnerabilityCase]) MUST apply the PEC INVITE trigger to the invited participant's row for the invited embargo, advancing it from UNINVITED, DECLINED or TIMED_OUT to INVITED. An embargo proposal is an invitation to consent; the EP wire activity conveys the invitation and the PEC rows record each participant's position on each embargo. A participant whose row is AGREED is unaffected by EP; INVITE is not legal from AGREED (CM-18-003), and a signatory asked about a revision has its row for the revision invited while its row for the active embargo stays AGREED (EP-09-004). The PEC trigger is applied via EmbargoLifecycle.record_embargo_invite() on the CASE_MANAGER as it relays the Invite, and on each replica as it replays the relay's ledger entry (EP-09-007). |
Derives from: CM-18-003 Refines: MSM-02-001 |
MSM-07-003 |
MUST | The wire activity ACCEPT_INVITE_TO_EMBARGO_ON_CASE (EA wire form: Accept(Invite(Event)[context=VulnerabilityCase])) is always the sending actor's own consent, whoever sends it, the case owner included: it MUST apply the PEC AGREE trigger to the sender's row for the accepted embargo, advancing it from UNINVITED, INVITED, DECLINED or TIMED_OUT to AGREED, and MUST NOT change any embargo register entry. The rule is the same whether the accepted embargo is the case's active embargo, a first proposal (EM PROPOSED) or a proposed revision of an embargo still in force: the accepted embargo's row is marked, and only that row. A participant is a signatory exactly when the row for the active embargo is AGREED, so accepting a proposed revision binds the participant to nothing new until the owner activates it (MSM-07-008). An Accept from a DECLINED row is the participant changing its answer, so the row moves directly to AGREED with no re-invite first (CM-18-003). An Accept of an embargo that is neither the active embargo nor an open proposal, because the register does not hold it or holds it in a final status, records nothing (CM-18-003): the CASE_MANAGER MUST refuse it before it is committed, with a reported reason, and answer it with the refusal notice (CLP-05-001). The case owner's decision for the case has its own activity (MSM-07-008, ADR-0122), so an Accept of an Invite means one thing whoever sends it and no handler branches on the sender to learn what it means. The PEC AGREE trigger is valid directly from UNINVITED because consent can be self-determined without an invitation (ADR-0048, ADR-0091, CM-18-003). Consent is per embargo (CM-10-001, ADR-0093, ADR-0122): an Accept is always a statement about the embargo it names, so it always marks that embargo's row and there is no first-proposal versus revision asymmetry to special-case. |
Derives from: CM-18-003 Refines: MSM-02-002 |
MSM-07-004 |
MUST | The wire activity REJECT_INVITE_TO_EMBARGO_ON_CASE (ER wire form: Reject(Invite(Event)[context=VulnerabilityCase])) is the sending actor's own consent whenever a participant sends it, the case owner included: it MUST apply the PEC DECLINE trigger to the sender's row for the rejected embargo (valid from UNINVITED, INVITED, AGREED or TIMED_OUT) and MUST NOT change any embargo register entry, except that a Reject of the case's active embargo from an AGREED row is not consent withdrawal and MUST be refused with a reported reason: withdrawal is Leave(EmbargoEvent) (MSM-07-010). A Reject of the active embargo from any other row refuses the terms in force and declines that row only. Rejecting a proposed embargo that is not active declines that embargo's row only and MUST leave the participant's row for the active embargo unchanged — a signatory's rejection of a proposed revision refuses those terms; it is not withdrawal from the embargo in force. When no embargo is in force (EM PROPOSED) a Reject of a proposal declines that proposal's row only: it withdraws from nothing, and other open proposals the participant agreed to stay AGREED. The CASE_MANAGER's own Reject of a proposal is not its consent but its abandonment of that proposal, which cancels it (EMB-16-001), and a replica MUST tell the two apart by the payload's actor (ADR-0152). A CASE_MANAGER with an embargo stake of its own (EP-09-002) MUST therefore record a change of its own consent as its Update(EmbargoConsent) of the row, and MUST NOT send or commit a Reject(Invite(Event)) as that consent.Symmetric with MSM-07-003: the case owner's rejection of a proposal for the case has its own activity (MSM-07-009, ADR-0122), so a Reject of an Invite is the sender's consent and nothing else. The PEC DECLINE trigger is valid directly from UNINVITED for symmetric reasons to AGREE (ADR-0048, ADR-0091, CM-18-003). Leaving an embargo one is bound by is a different act with its own activity (MSM-07-010, ADR-0122), so a Reject never has to be read against the case's state to tell a refusal from a withdrawal. Consent is per embargo, so a signatory who refuses proposed terms is still bound by, and still consents to, the embargo that remains active; only the case owner decides whether the proposal replaces it. A CASE_MANAGER act with a participant's verb and object carries the participant's semantic type (ADR-0152), so the CASE_MANAGER's abandonment and a participant's refusal share one; the CASE_MANAGER's own consent is the row it changes (CLP-07-013), which keeps every CASE_MANAGER-sent Reject an abandonment. |
Derives from: CM-18-003 Refines: MSM-02-006 |
MSM-07-005 |
MUST | When the case owner's Accept(Event)[target=VulnerabilityCase] (MSM-07-008) replaces active embargo A with revision B (the register activates B and supersedes A, so the derived EM goes REVISE → ACTIVE, or stays REVISE while another proposal is open, EMB-18-005), implementations MUST settle every participant's consent rows against B: if B ends no later than A, every participant whose row for A is AGREED MUST be carried over by applying CARRY_OVER to its row for B, a DECLINED row for B included (CM-18-003); if B ends later than A, implementations MUST NOT write any row to lapse anyone — a participant whose row for B is not AGREED is not a signatory to B and has lapsed if it had agreed to A (CM-18-001). The owner's row for B is AGREED unless it already is; an owner whose row for B is DECLINED has changed its answer by activating B, so its row moves directly to AGREED with no re-invite first (MSM-07-008, CM-18-003). Activating B advances no one else: a participant holding an AGREED row for B is its signatory by lookup, whether it is the proposer or an early acceptor. Only participants whose row for A is AGREED are carried over: a participant that had lapsed from an earlier embargo stays lapsed until it is re-invited or agrees to B, and a participant INVITED to A whose invitation B has made stale is handled by the stale-terms re-invite path (EMB-17), because its row for the now-SUPERSEDED A is frozen. A revision proposal (EV, ACTIVE → REVISE, or a counter-revision REVISE → REVISE) MUST NOT change any participant's consent to the embargo in force, and the proposer's row for the proposed embargo MUST be marked AGREED. The CASE_MANAGER decides the carry-over and MUST commit each carried-over row as its own CASE_MANAGER Update(EmbargoConsent) ledger entry, after the Accept's, and every replica MUST copy those entries rather than run the carry-over itself (ADR-0124, CLP-07-013, CM-23-016); the owner's own row is what its Accept states, applied from the Accept's entry. No activity is sent to a participant for these consent writes beyond the ledger fan-out.A participant has lapsed when the active embargo was replaced by longer terms it has not agreed to. While a revision is merely proposed the prior embargo is still in force and every signatory to it is still bound by it, so nothing about their consent has changed (ADR-0093). Agreeing to N days is agreeing to every shorter period — the containment that makes shortest-wins safe (EP-04-003) — so a shorter revision asks nothing new of an existing signatory and carries them over, even one that declined B: its decline objected to losing time, not to keeping the embargo. Longer terms ask for more than they promised, so only those who agreed to B are bound by B, and the rows say so without a write. Only the owner's acceptance changes the embargo on the case; other participants' accepts and rejects are recorded per embargo and inform that decision. Committing each carried-over row as its own entry keeps every replica's consent rows in step with the CASE_MANAGER's without a replica re-running the containment comparison on its own state (#4716). |
Derives from: CM-18-003 Refines: MSM-02-003 |
MSM-07-006 |
MUST | When an ET / REMOVE_EMBARGO_EVENT_FROM_CASE wire activity terminates the embargo register's ACTIVE entry (cancelling every open proposal in the same step, so the derived EM becomes EXITED), implementations MUST leave the case with no active embargo and MUST NOT write any participant's consent rows: with no active embargo no participant is a signatory and no row binds anyone, and an Accept or Reject that arrives afterwards records nothing (EMB-17-004). No separate outbound wire activity is emitted; the participant position is derived from the case. A register with a TERMINATED entry accepts no further change (ADR-0122), so a terminated embargo can never be invited to again. That is a fact about the case, which every replica holds once it replays the teardown, so it does not need to be copied onto each participant record as the terminal state it was before ADR-0122 (ADR-0118). Leaving the rows as they were also keeps the history of who agreed to what, and every row of a final entry is frozen (CM-18-003). |
Derives from: CM-18-003 Refines: MSM-02-007 |
MSM-07-007 |
MUST | The timer-based pocket-veto transition — INVITED to TIMED_OUT on the row of the invitation whose deadline passed — MUST be enforced lazily with no outbound wire message emitted for the consent change itself. A lapse has no timer path (CM-18-002): a lapsed participant is re-invited, and the deadline attaches to that invitation. The CASE_MANAGER MUST author a case-ledger entry recording the expiry (CM-28-005) so the consent change is visible in the canonical ledger, but no additional protocol wire activity is required. The case owner learns of the expiry, as of an explicit refusal (MSM-07-004), from that entry: by its Announce(CaseLedgerEntry) fan-out while the owner is an active participant (CM-10-004), or from its own ledger when the owner holds CVDRole.CASE_MANAGER. No separate notification activity is sent to the owner.Inaction cannot produce an outbound message because there is no actor-observable event to report. The expiry is a derived conclusion from (the row's rsvp_deadline, now); the ledger entry is the record of that conclusion, not a message sent by the silent participant. Requiring a wire message would demand the system speak on behalf of a participant who has said nothing, which contradicts the Actor Knowledge Model (AKM). The entry already reaches every owner entitled to case content, so a second message would only duplicate it; for an explicit refusal, a received embargo activity, it would also be the acknowledgement EP-09-009 forbids. |
Derives from: CM-18-002 Derives from: CM-28-005 |
Message Validation
Requirements for validating ActivityStreams 2.0 activities at the inbox handler boundary, covering ActivityStreams structure and semantic validation. Invalid activities are rejected with appropriate error responses before dispatch.
File ID: MV
Required Field Validation
| ID | Priority | Requirement | Related |
|---|---|---|---|
MV-03-001 |
MUST | The system MUST validate required fields based on activity type | |
MV-03-002 |
MUST | A receiver MUST treat a required field that is present but blank as absent, and MUST report it with the same error as an omitted field. A blank value is an empty string or one containing only whitespace. A value that is present and non-blank but unparseable MUST be reported as a schema fault instead, not as a missing field. These guards enforce requirements about values, not keys: CLP-15-006 refuses an activity that "carries no published", and a field present but empty carries none — CS-08-001's "if present, then non-empty". Testing the key let published: "" reach schema validation, replacing that explanation with a Pydantic dump, and type: "" reach the vocabulary lookup, so two spellings of one omission drew two status codes. Unparseable values stay on the schema path. |
Refines: MV-03-001 |
Object Type Validation
URI Validation
Outbound Activity Object Integrity
Stub Objects
| ID | Priority | Requirement | Related |
|---|---|---|---|
MV-10-001 |
MUST | A stub object MUST carry at minimum an id field (full URI), a type field and a non-empty summary field giving human-readable context, and a receiver MUST refuse a stub whose summary is absent or blank. The stub form is permitted ONLY for a case, as the distinct VulnerabilityCaseStub type of CM-11-013, and a receiver MUST classify a stub by its type. For every other object type, a sender that does not inline the full object MUST reference it by URI (or by an ActivityStreams Link), not by a partial inline object.The stub form is a narrow affordance, not a substitute for a full object. It serves one need: an invitee must decide whether to join a case before it may receive the case (MV-10-005), so the stub stands in for the case until the participant is active (CM-10-004). No other type has that need or is implemented. The summary is what tells the invitee what it is being asked to join; what it says is the sender's choice (CM-17-010); #4165 implemented the enforcement. |
Constrains: MV-04-003 |
MV-10-002 |
MUST | A VulnerabilityCaseStub object is ONLY permitted in the following field positions of an ActivityStreams activity: actor, object, target, and origin.Scopes the position rule to the one permitted stub type (MV-10-001). A reference in any of these positions may also be a bare URI or a Link; those forms are always acceptable and are unaffected by MV-04-003's refusal of malformed inline objects. |
|
MV-10-004 |
MUST NOT | Inbound stubs MUST NOT be used to create new domain records | |
MV-10-005 |
MUST | A participant MUST be active (CM-10-004) before the CASE_MANAGER delivers full case details to it. | Depends on: MV-10-006 |
MV-10-006 |
MUST | When a participant accepts the stub Invite (Accept(Invite(Actor, VulnerabilityCaseStub))) and the case has an active embargo, the acceptance MUST imply consent to the active embargo, marking that participant's consent row for it AGREED (CM-11-001). |
Depends on: CM-03-008 |
Outbox
Requirements for actor outboxes, which hold response activities generated by handlers. Covers outbox structure, population, delivery, local and federated dispatch, idempotency, outbound reference dehydration, and outbox addressing rules.
File ID: OX
Undelivered-Activity Correlation
Requirements linking a permanently undelivered activity back to the canonical case ledger entry that recorded the event the activity carries, so that divergence between the canonical log and a participant's replica is observable rather than silent. Derived from CONCERN-2657 and ADR-0080.
Participant Case Replica
Requirements for how each Vultron Actor maintains and manages its local copy (replica) of a VulnerabilityCase. Covers replica identity, bootstrap, update authority, case-context message routing, reporter case discovery, and unknown-context handling.
File ID: PCR
Replica Identity (No Stub CASE_MANAGER)
| ID | Priority | Requirement | Related |
|---|---|---|---|
PCR-01-001 |
MUST | Each Vultron Actor MUST maintain its local case replica as an internal concern — there is exactly one externally-addressable Actor identity per participant, regardless of how many cases that Actor is involved in. | Refines: CM-01-001 Refines: AKM-01-002 |
PCR-01-002 |
MUST | There is exactly one CASE_MANAGER per case, operated by the case creator/owner. | Refines: CM-02-001 |
PCR-01-003 |
MUST | Actor co-location is an implementation choice the protocol MUST remain indifferent to. Whether every Actor runs on one host or on many, Actors MUST exchange information only through protocol messages. An Actor MUST NOT read, write, or otherwise reach into another Actor's store, and MUST NOT rely on out-of-band communication with a co-located Actor. In particular, an Actor that has asked a CASE_MANAGER to create a case MUST treat the case as absent from its own knowledge until the CASE_MANAGER's Create(VulnerabilityCase) replica has been delivered to it, and MUST NOT assume co-location makes the case visible sooner.Co-location is a deployment convenience; a back-end cheat that only works when two Actors happen to share a host encodes that convenience into the protocol and stops being a protocol at all. Per-actor stores (ADR-0073) make the cheat impossible for reads and writes, but they do not stop code from expecting it. Issue #2548 is the worked example: a co-located CASE_MANAGER created the case in its own store, and the report receiver's behavior tree proceeded as though the case were already its own. It was not — under ADR-0041 the receiver never creates the case — so the case-scoped half of the RM.VALID transition had nothing to act on. Stating the indifference as a MUST makes "wait for the message" the only correct answer, on one host or many. |
Refines: CM-01-001 Refines: AKM-01-002 Implements: DL-07-001 |
Replica Bootstrap
| ID | Priority | Requirement | Related |
|---|---|---|---|
PCR-02-001 |
MUST | Announce(VulnerabilityCase) is the unified mechanism for delivering a full case snapshot to a participant, at every lifecycle stage. |
Implements: AKM-03-001 Implements: SYNC-02-001 |
PCR-02-002 |
MUST | When a participant becomes active (CM-10-004) — at case creation, or on accepting the stub Invite (CM-11-008) — the CASE_MANAGER MUST send Announce(VulnerabilityCase) to that participant to deliver its initial case copy. It MUST NOT send one to an inert participant. |
Implements: CM-06-001 |
PCR-02-003 |
MUST | When a participant actor receives Announce(VulnerabilityCase) and has no existing local copy for that case ID, it MUST create a local replica from the received case object. |
|
PCR-02-004 |
MUST | When a participant actor receives Announce(VulnerabilityCase) and already has a local replica for that case ID, it MUST apply the received case state as an authoritative update, subject to PCR-03-001 and to the ledger-tail comparison of SYNC-16-006.An update is safe only when the copy names the ledger position it reflects, because an old copy can arrive late over an unordered transport (ADR-0142). |
|
PCR-02-005 |
MUST NOT | Create(VulnerabilityCase) retains its semantic meaning ("a case was created") but MUST NOT be used as the mechanism for delivering a full case snapshot to participants. |
Replica Update Authority
| ID | Priority | Requirement | Related |
|---|---|---|---|
PCR-03-001 |
MUST | A participant actor's local case replica MUST only accept case-state updates that originate from the CASE_MANAGER for that case. | Implements: CM-06-002 |
PCR-03-002 |
MUST NOT | The case owner actor MUST NOT write directly to its own local case copy. | Refines: CM-02-010 |
PCR-03-003 |
MUST | A participant actor MUST authenticate Announce(VulnerabilityCase) activities before accepting them as authoritative updates to its local replica. |
|
PCR-03-004 |
MUST NOT | A participant actor MUST NOT create a local case replica from an Announce(VulnerabilityCase) message unless prior trust for that case has been established: either (a) the actor has already processed a Create(VulnerabilityCase) from the report receiver that introduced the CASE_MANAGER identity, or (b) the actor has completed an InviteActorToCase exchange (the Invite and its Accept) that records a pending-expectation for that case. Accepting any Announce(VulnerabilityCase) from an unauthenticated or unexpected sender as a bootstrap signal is not permitted. For path (b), the pending-expectation record MUST be created only when the Invite's object is the receiving actor; the record MUST bind to the Invite's actor id (the named CASE_MANAGER), not to the transport-level delivering sender; and a later Invite for the same case naming a different CASE_MANAGER MUST replace the anchor while the receiver has not accepted the Invite that set it, and MUST be refused, leaving the anchor unchanged, once the receiver has accepted that Invite. Whether the named CASE_MANAGER is genuine cannot be verified without actor identity and signatures (#2841); this requirement records the structural rules that apply before that verification is available.An Announce(VulnerabilityCase) roster is supplied by the sender; a fabricated case naming the sender as its own CASE_MANAGER passes a roster-only check. Permitting a first-contact Announce when no locally derived record exists lets an attacker pre-seed a fake replica, blocking the real CASE_MANAGER on the seeded path (PCR-03-001 fails closed). No legitimate protocol sequence delivers an unsolicited first-contact Announce without a preceding Create(VulnerabilityCase) or InviteActorToCase. The path (b) sub-rules prevent a misaddressed Invite from anchoring trust on behalf of a different actor and prevent a second Invite from replacing the anchor the receiver has accepted. Until the receiver accepts, a later Invite replaces the anchor, so a forged stub Invite cannot lock out the real CASE_MANAGER's Invite until the bootstrap; a forged Invite the receiver accepts first still wins, a residual risk accepted until signatures (#2841). |
Refines: PCR-03-001 Refines: PCR-02-003 |
PCR-03-005 |
MUST | The case owner actor MUST follow the same replica rules as all other participants. Exempting the case owner from replica rules would create an inconsistent update path that bypasses the CASE_MANAGER consistency checks. |
Refines: PCR-03-002 |
PCR-03-006 |
MUST | The case owner actor MUST route all case-state changes through the CASE_MANAGER. Routing through the CASE_MANAGER ensures all case-state mutations go through the single-writer authority and are recorded in the case ledger. |
Refines: PCR-03-002 |
Case Context Routing
| ID | Priority | Requirement | Related |
|---|---|---|---|
PCR-04-001 |
MUST | The context field of an ActivityStreams activity is the canonical mechanism by which a participant actor identifies the local case replica a case-scoped message belongs to. |
Implements: CM-01-002 |
PCR-04-002 |
MUST | All case-scoped protocol activities sent by the CASE_MANAGER to participants MUST carry context set to the case ID. |
|
PCR-04-003 |
MUST NOT | The participant actor MUST be the only externally-addressable entity for receiving case-scoped messages on behalf of all cases it participates in; there MUST NOT be a separate per-case inbox. |
Reporter Case Discovery
Unknown Case Context Handling
| ID | Priority | Requirement | Related |
|---|---|---|---|
PCR-06-001 |
MUST NOT | When a participant actor receives an activity whose context field does not match any known local case replica, the actor MUST NOT silently discard the activity. |
|
PCR-06-002 |
SHOULD | When an activity with an unknown case context arrives, the actor SHOULD queue the activity briefly and request a case snapshot from the CASE_MANAGER. | |
PCR-06-003 |
MUST NOT | Activities with unknown case context MUST NOT update any local state until a valid case replica has been established. | Depends on: PCR-06-005 |
PCR-06-005 |
MUST | A stub Invite (CM-11-013) MUST be processed on arrival, not queued under PCR-06-002, even though its receiver holds no replica of the case it names. Processing it MUST write nothing but the intake archive of the Invite (CLP-10-017) and the trust anchor of PCR-03-004 path (b). Every other activity naming a case the receiver does not hold is still queued. An invitee decides whether to join a case before the case is delivered to it (MV-10-005): the case arrives in an Announce only after it accepts. Queuing the Invite until the case is known held it back until the very Announce its own Accept brings, so it reached no use case, intake never archived it, and the invitee could answer it only from a copy held under the sender's id, which DL-06-006 forbids (#4915). |
Refines: PCR-06-002 Depends on: PCR-03-004 |
Outbound Participant Message Routing
| ID | Priority | Requirement | Related |
|---|---|---|---|
PCR-08-001 |
MUST | After a case is created and the CASE_MANAGER is introduced, a participant actor MUST address all case-scoped outbound messages (notes, status updates, embargo proposals, RM transitions, and any other protocol messages targeting the case) exclusively to the CASE_MANAGER for that case. A participant actor MUST NOT send these messages directly to other participants. The one exception is RK, Read(Offer(VulnerabilityReport)): it answers the reporter's original Offer, outside the case, so an addressee of that Offer sends it to the reporter directly and never to the CASE_MANAGER (RMB-08-003).The RK exception was decided by the maintainer on #4560. |
|
PCR-08-002 |
MUST NOT | A participant actor MUST NOT assume that other participants will receive its case-scoped messages through any channel other than the CASE_MANAGER's Announce(CaseLedgerEntry) broadcast. Sending to the whole roster or any multi-recipient list that includes non-CASE_MANAGER actors is a violation of this rule. |
|
PCR-08-003 |
MUST | The CASE_MANAGER MUST, for every participant message it receives and accepts, commit a CaseLedgerEntry recording the outcome and broadcast Announce(CaseLedgerEntry) to all case participants as the standard cascade. This cascade is the only mechanism by which participants learn of accepted case-state changes. |
|
PCR-08-013 |
MUST | The CaseLedgerEntry commit and Announce(CaseLedgerEntry) broadcast (PCR-08-003) MUST follow automatically from the CASE_MANAGER processing any accepted participant message, without any further request or operator action. |
Refines: PCR-08-003 |
PCR-08-005 |
MUST | A participant sending a case-scoped message MUST take its recipient from its replica of the case: the participant holding the CASE_MANAGER role, as the message's sole to: recipient. The case's full participant list MUST NOT serve as the recipients of such a message. |
|
PCR-08-007 |
MUST | An Invite that invites an actor to a case (InviteActorToCase) MUST be sent with actor set to the CASE_MANAGER's ID, not the case owner's ID. The case owner triggers the invite action, but the CASE_MANAGER MUST be the ActivityStreams actor performing the send. Optionally, attributedTo on the activity MAY carry the case owner's ID to record who initiated the invitation. |
Refines: PCR-08-001 |
PCR-08-008 |
MUST | The invitee's Accept of that Invite (PCR-08-007) MUST be addressed to the CASE_MANAGER (not the case owner). The CASE_MANAGER is the authoritative recipient and processor of all case-management handshake messages after case creation. |
Refines: PCR-08-001 |
PCR-08-009 |
MUST | The CASE_MANAGER MUST process an invitee's replies and record their effect on its own copy of the case: a reply to the stub Invite as CM-11-001 and CM-11-007 require, and a reply to the full-case Invite as CM-11-011 requires. | Refines: PCR-08-001 Refines: PCR-08-003 |
PCR-08-014 |
MUST NOT | The case owner MUST NOT author or emit any RM state-change activity on behalf of the invitee. | Refines: PCR-08-010 |
PCR-08-010 |
MUST NOT | An actor processing a received activity MUST NOT construct or emit an activity whose actor is a different actor's ID (identity spoofing). |
Refines: PCR-08-001 |
PCR-08-012 |
MUST | The invitee's Accept(Invite(Actor, VulnerabilityCaseStub)) is a join and embargo-consent decision, not an RM engagement decision. The CASE_MANAGER MUST NOT record any RM transition from it (CM-11-001), and MUST record the invitee as RM.ACCEPTED only on receiving Join(VulnerabilityCase) from it (CM-11-004).PCR-08-014 prohibits the case owner from emitting RM state-change activities on behalf of the invitee. The invitee's own messages are the authoritative signals, and each carries only the decision it names: accepting the stub joins the case, the full-case Invite reply judges it (CM-11-011), and Join engages. |
Refines: PCR-08-009 |
Participant Role Management
Defines the API for reading and mutating CVD roles on CaseParticipant, the one participant class shared by the core domain and the wire layer (ADR-0099). Requires the role-management methods, prohibits direct attribute mutation after construction, and mandates test coverage for each method.
File ID: PRM
Role Read API
Role Mutation API
Wire Layer Interface Parity
Assertion Authority and On-Behalf Exceptions
Protocol Asks and Outstanding-Ask Registers
Requirements for the protocol-ask primitive: how an actor requests something it lacks authority to do, how outstanding asks are recorded and enumerated in both directions, how asks expire, and how a receiver reports that it could not process an inbound message. Derived from ADR-0080 and the G01 planning session (CONCERN-2829).
File ID: ASK
Ask and Terminate
An ask is an ordinary protocol message that completes the asking behavior. Nothing suspends awaiting a reply.
Conversation-State Routing
A gated interaction is one tree that determines where the conversation stands before taking any action.
Ask Kinds
Each kind of ask declares the replies that close it, its expiry consequence, and a default deadline.
Outstanding-Ask Register
A durable, actor-scoped record of asks outstanding in both directions.
Expiry and Reaping
How an expired ask is noticed, given that elapsed time is not an event.
Ask Visibility
Asks replicate like any other canonical entry, which constrains what they may carry.
Processing Faults
How a receiver tells an authenticated sender that it could not process what was sent.
| ID | Priority | Requirement | Related |
|---|---|---|---|
ASK-07-001 |
MUST | An actor that cannot process an inbound activity from an authenticated sender MUST notify that sender by emitting Create(ProcessingFault). |
|
*Discharges the deferral recorded in ADR-0049; without it a failure that took milliseconds costs the sender its full ask deadline in silence. |
|||
| * | |||
ASK-07-002 |
MUST NOT | An actor MUST NOT emit a processing fault in response to an inbound activity whose sender is not authenticated, or whose failure occurred before authentication. | |
*Explaining a parse or validation failure to an unauthenticated sender is a parser oracle, and an unauthenticated sender identity cannot be trusted as a reply address. |
|||
| * | |||
ASK-07-003 |
MUST | ProcessingFault MUST be a single dedicated object type carrying a machine-readable failure class, rather than a reason field on an existing reply activity. |
|
*SE-08-003 prefers a dedicated object type over field-level discrimination; Reject additionally presupposes a rejectable object, which unreadable traffic does not provide. |
|||
| * | |||
ASK-07-004 |
MUST | ProcessingFault MUST identify the activity that failed by reference, and MUST NOT require an inline copy of it. |
|
*When a payload fails validation, a typed copy of it cannot be constructed — constructing it is what failed — and echoing sender-supplied content is a reflection hazard. |
|||
| * | |||
ASK-07-005 |
MUST | The failure classes of ProcessingFault MUST be identified by URIs minted in the Vultron namespace, and the fault MUST be structured according to RFC 9457 Problem Details. |
|
*A URI-identified, dereferenceable problem type is extensible without a schema change and lets a receiver that does not recognise a class resolve its meaning; RFC 9457 is the standard for machine-readable error reporting over HTTP. |
|||
| * | |||
ASK-07-006 |
MUST NOT | ProcessingFault MUST NOT carry implementation diagnostics such as stack traces, parser internals, or the identity of the failing code path. |
|
*Faults replicate to all case participants and are also read by an untrusted-by-default peer; the diagnosis of a failure belongs in the actor's own log, governed by specs/structured-logging.yaml. |
|||
| * | |||
ASK-07-007 |
MUST | Receiving a processing fault that refers to an outstanding ask MUST close that ask's register entry. | |
*The exchange cannot complete, so leaving the entry open costs the asker its whole deadline for a failure already reported. |
|||
| * | |||
ASK-07-008 |
MUST | A processing fault that can be attributed to a case MUST be recorded as a canonical CaseLedgerEntry with disposition recorded, unless it reports a failure to apply a CaseLedgerEntry, which SYNC-16-002 keeps out of the ledger. |
|
*The statement that a message arrived and could not be processed is true, legible protocol history, and it explains why a subsequent retransmission occurred; it is not per-actor observability content, so CLP-07-004 does not exclude it. A fault about applying a ledger entry is replication signalling instead: ledgering it loops, because the replica that sent it cannot apply the entry that records it. Whether the remaining faults belong in the ledger at all is Concern #4800. |
|||
| * | |||
ASK-07-009 |
MUST NOT | The activity that could not be processed MUST NOT be recorded as a canonical CaseLedgerEntry. |
|
*There is no legible assertion to record; a snapshot of an unreadable payload cannot satisfy CLP-02-003, and CLP-03-002 already excludes unrouteable inbound from the ledger. |
|||
| * | |||
ASK-07-010 |
MUST | When a received-side use case raises while processing an inbound activity from an authenticated sender, the inbox MUST answer that sender with Create(ProcessingFault) of an internal-error failure class, emitted after any DataLayer transaction the act opened has rolled back. It MUST cover every received use case, not only the trees that commit an act. An attempt the inbox will retry (IE-06-004) MUST NOT be answered: a deferred activity replayed from the pending queue is answered only when it is dead-lettered. The fault id MUST be derived from the failed activity and the failure class, so a repeated failure of the same activity emits no second fault. |
|
*A failed received act keeps only intake's archive (CLP-07-015), and the inbox answered before processing, so without this fault the sender waits out its ask deadline in silence. Emitting after the rollback keeps the fault from being undone with the act; answering only when no retry follows keeps a retry that later succeeds from sending a false fault that closes the ask early (ASK-07-007). ASK-07-008 decides whether the fault is ledgered, as for any fault. |
|||
| * | Refines: ASK-07-001 Depends on: IE-06-004 |
Lost-Reply Recovery
What an answerer does when a request is repeated because its reply was lost.
Received-Side Status Authorization
Requirements for the two-gate authorization model governing how a CASE_MANAGER adopts an inbound participant's reported CaseStatus as canonical (StatusAdoptionGate) and whether to execute embargo teardown side-effects (EmbargoTeardownAuthorizationGate). Derived from ADR-0046 and IDEA-1836 planning session.
File ID: RSH
StatusAdoptionGate
Controls whether a participant's reported CaseStatus is adopted as canonical by the CASE_MANAGER.
EmbargoTeardownAuthorizationGate
Controls whether embargo teardown or other side-effects execute after a canonical CaseStatus write.
ThreatTerminationBranchNode
Node that triggers embargo teardown on any of the three threat-signal CaseStatus flags.
| ID | Priority | Requirement | Related |
|---|---|---|---|
RSH-03-001 |
MUST | ThreatTerminationBranchNode MUST trigger embargo teardown when the canonical CaseStatus carries CS.P (public awareness), CS.X (exploit public), or CS.A (attacks observed). |
|
*All three flags signal a breach of the conditions that justify an active embargo. Covering only CS.P (as the prior PublicDisclosureBranchNode did) leaves exploit and attack signals without a teardown trigger. |
|||
| * | |||
RSH-03-003 |
MUST | add_participant_status_tree MUST tear an embargo down through ThreatTerminationBranchNode, after the canonical write, and MUST NOT contain PublicDisclosureBranchNode. add_case_status_tree MUST NOT contain ThreatTerminationBranchNode. |
|
*The node belongs in the tree that writes canonical state. A received Add(CaseStatus) writes none: it is admitted only at a participant replica, from the CASE_MANAGER, whose own teardown the replica leaves to the committed entry (RSH-04-005, RSH-03-004). PublicDisclosureBranchNode covered only CS.P and predates the two-seam design (RSH-03-001). |
|||
| * | |||
RSH-03-004 |
MUST NOT | An actor that does not hold CVDRole.CASE_MANAGER MUST NOT ask the CASE_MANAGER for an embargo teardown when the P/X/A status that triggered ThreatTerminationBranchNode was declared by the CASE_MANAGER; it waits for the CASE_MANAGER's committed teardown entry (EP-09-007). |
|
*The CASE_MANAGER tears down on its own P/X/A detection, commits the teardown and records the new status, each as a ledger entry announced to every participant. A replica asking it to terminate the embargo it has already terminated earns only a refusal (#4149). An open proposal raises no ask either way (EMB-16-002). The CSB-18 diagnostic note raises no such false alarm: PxaEmInvariantDiagnosticNode runs only at the CASE_MANAGER (#4829). |
|||
| * | Refines: RSH-03-001 Depends on: EP-09-008 |
CaseStatus Emission Authority and Outbound Emit Invariant
Specifies which actors may emit Add(CaseStatus) directly and mandates that every CASE_MANAGER-side EM or PXA state mutation is followed by a canonical CaseStatus ledger write. CaseStatus is the only protocol channel for communicating EM and PXA state changes across actors; omitting the emit leaves all participant replicas stale until the next round-trip.
| ID | Priority | Requirement | Related |
|---|---|---|---|
RSH-04-001 |
MUST | Only the CASE_MANAGER MUST emit Add(CaseStatus) directly. All other participants MUST embed a suggested CaseStatus inside Add(ParticipantStatus) rather than sending Add(CaseStatus) independently. |
|
*CaseStatus encodes shared EM and PXA state for the entire case. The CASE_MANAGER is the authoritative writer of canonical case state (ADR-0046, CLP-09). Allowing participants to emit Add(CaseStatus) directly would bypass the two-seam authorization model. |
|||
| * | |||
RSH-04-002 |
MUST | Every BT node or tree that mutates EM state (Propose, Accept, Reject, Terminate, and all cascade variants) MUST immediately follow the state-mutation step with a write of the updated CaseStatus to the canonical case ledger. A received tree whose guarded commit records the very activity that caused the transition, and whose entry type has a replica apply node that replays the same transition (RSH-08-004, ADR-0113), has made that write: the committed entry carries the change to every replica, and a second CaseStatus entry for it would be a duplicate. |
|
*EM state changes are only observable to remote actors via CaseStatus entries in Announce(CaseLedgerEntry) messages. A mutation without a subsequent ledger write leaves participant replicas with a stale EM state (observed in rv/rcv/rvv CI scenarios — see CONCERN-1667). |
|||
| * | |||
RSH-04-003 |
MUST | Every BT node or tree that mutates PXA state (public awareness, exploit public, attacks observed) MUST immediately follow the state-mutation step with a write of the updated CaseStatus to the canonical case ledger. |
|
*PXA state is participant-agnostic and shared across the case. The rationale mirrors RSH-04-002: omitting the ledger write leaves replicas stale and breaks the Avoid Surprise principle. |
|||
| * | |||
RSH-04-004 |
MUST | The CaseStatus ledger write described in RSH-04-002 and RSH-04-003 MUST be implemented as a shared BT node (EmitCaseStatusUpdateNode) that writes the new CaseStatus directly to case.case_statuses and commits a canonical CaseLedgerEntry, then emits Announce(CaseLedgerEntry) for participant replica sync. It MUST NOT route through the inbox authorization seam (add_case_status_tree) — the CASE_MANAGER is always the authority for its own outbound state changes. A snapshot whose EM and PXA equal those of the case's last committed CaseStatus changes nothing, so the node MUST NOT commit or write it (CLP-07-013, ADR-0152). The change is measured against the ledger, not the case's current status, which already carries the register's EM (ADR-0122). |
|
*The inbox seam handles a received Add(CaseStatus), which only the CASE_MANAGER sends, and only to a participant replica, which writes nothing on receipt (RSH-04-005, RSH-08-003). The CASE_MANAGER writing its own EM/PXA transition is an authoritative state change, not a received message. Routing through the inbox loopback (as EmitAddCaseStatusToSelfNode does) inverts causality: the ledger write should precede the announce, not be triggered by a self-addressed inbox message. DRY: one shared node, not N inline patches per BT tree factory. |
|||
| * | |||
RSH-04-005 |
MUST | A received Add(CaseStatus) MUST be admitted only from the case's CASE_MANAGER, sent to a participant. A receiver MUST refuse an Add(CaseStatus) from any other sender, and an Add(CaseStatus) sent to the CASE_MANAGER, with nothing written past intake and nothing sent. |
|
*RSH-04-001 reserves Add(CaseStatus) for the CASE_MANAGER, which sends it to participants inside a case ledger entry. add_case_status_tree has no StatusAdoptionGate, so admitting it from another sender would adopt that participant's P, X or A report without the Case Owner approval RSH-07-001 requires. A participant embeds the P, X or A it reports in its own Add(ParticipantStatus) instead (RSH-04-001), which the CASE_MANAGER adopts through the gate (RSH-01-003). |
|||
| * | Refines: RSH-04-001 Refines: HP-01-006 |
RM Anomaly Notification Policy (non-adjacent jumps and backward regressions)
When the receiver detects an RM transition anomaly — either a non-adjacent forward jump (sender skipped intermediate states) or a backward regression (sender reports a state earlier on the RM progress scale than the receiver has already observed) — it must act correctly and non-silently. Because CVD is inherently asynchronous, anomalies are detected at receipt time (t₁ > t₀ when the event occurred), and silence at that point breaks observability for both parties. Derived from ISSUE-2258 and ADR-0067.
One Move, One Mover — Received-Side Write Subject and Pipeline
A protocol state transition may reach a receiver in more than one wire shape: the activity that performed the move (TentativeReject(Offer(Report))) and a declaration of the resulting state (Add(ParticipantStatus) carrying rmState). These are two layers of one move, not competing encodings. Authority is a property of the pipeline, not of the message: the mover announces, the CASE_MANAGER adjudicates and commits, replicas apply the ledger. Derived from CONCERN-3473 and ADR-0108.
| ID | Priority | Requirement | Related |
|---|---|---|---|
RSH-08-001 |
MUST | A received RM status declaration reports the sending actor's own RM state: its subject MUST be the sender, never the receiver. A receiver's own RM state MUST move on receipt only when it is itself the mover: when sender and receiver are the same actor, or when CM-23-002 directs the CASE_MANAGER to advance its own participant on Case Owner departure. The one write whose subject is neither is the CASE_MANAGER's creation of an invitee's participant record when, in response to a received activity, it emits the stub Invite (CM-11-006, CM-17-007, CM-16-006): its subject is the invitee. *A received activity is an assertion about the sender's state (HP-00-001) and participant status is self-declaratory (ADR-0121). BT-17-006 chooses the store a received-side tree runs in; it says nothing about whose state the tree writes, and its own sender-ID clause presumes the two differ. Conflating them is the store-versus-subject error ADR-0089 named as bug #2300, and it produced two RM handlers that advance the wrong participant (CONCERN-3473). |
|
| * | |||
RSH-08-002 |
MUST | When an act and a status declaration for the same move both arrive (for example TentativeReject(Offer(VulnerabilityReport)) followed by Add(ParticipantStatus) with rmState: INVALID), the receiver MUST treat the second as a same-state confirmation and MUST NOT refuse it, fault it, or record it as an anomaly. A declaration whose RM value disagrees with the act the receiver saw MUST be adjudicated under the RM acceptance rule (RSH-06) like any other declaration; the disagreement is not itself grounds for refusal. |
|
*One move producing both messages is expected (ADR-0108). The declaration is the participant's own statement about itself and the participant is the authority on that; a mismatch with the act is a gap to flag under RSH-06-003, not a reason to prefer one message over the other. |
|||
| * | |||
RSH-08-006 |
MUST NOT | A participant that declares its own RM, VF or D state, or a case owner that asserts one on another participant's behalf, MUST NOT append the status to a participant record in its own case replica. It sends the declaration to the CASE_MANAGER, and its replica copies the status the CASE_MANAGER commits for it, as every other replica does. It MAY check the move before sending, and MAY store the status object the activity carries. | |
*A status the sender writes for itself has its own id and times, or a value the CASE_MANAGER then filters, so the sender's replica held a status no entry records and differed from every replica at the same ledger position (#4295). PCR-03-002 and PCR-03-005 already keep the case owner from writing its own copy; this extends that to every sender of a status declaration (CLP-07-013, CM-23-016). |
|||
| * | Extends: PCR-03-002 Refines: RSH-08-003 |
||
RSH-08-007 |
MUST | Participant status is self-declared. The CASE_MANAGER MUST refuse, before the commit, a received Add(ParticipantStatus) whose target participant record is not the sender's own (the record's attributed_to is not the sender), whichever dimensions it carries: RM, VF, D and the P, X and A of its case status. The only exceptions are the on-behalf assertions of PRM-06-003 and PRM-06-004: a sender holding CASE_MANAGER or CASE_OWNER MAY assert v→V for a Vendor-role participant, d→D for a Deployer-role participant, or both, when the status changes nothing else (RM as the target holds it, and no case status). |
|
*A received activity is an assertion about the sender's state (HP-00-001), and RSH-08-001 already made the sender the subject of an RM declaration. Nothing enforced it for Add(ParticipantStatus), so a participant could write any status onto another participant's record and the CASE_MANAGER committed it (#4987). The on-behalf trigger sends exactly the PRM-06 moves, so the receive side admits those and nothing wider (ADR-0121). |
|||
| * | Extends: RSH-08-001 Depends on: HP-01-008 Depends on: PRM-06-003 Depends on: PRM-06-004 |
Whole Accept or Reject of a Received Status
A received Add(ParticipantStatus, CaseParticipant) or Add(CaseStatus, VulnerabilityCase) states a whole status, so the CASE_MANAGER accepts it as sent or refuses all of it. It never records a status the sender did not send. Derived from ADR-0107 and ISSUE-3741.
| ID | Priority | Requirement | Related |
|---|---|---|---|
RSH-09-001 |
MUST | A CASE_MANAGER receiving an Add(ParticipantStatus, CaseParticipant) MUST either accept the status whole, recording exactly the status it received, or refuse the whole update. It MUST NOT record, commit or replicate a status that differs from the one received, and MUST NOT fill a dimension the sender omitted with a value of its own. |
|
*The case ledger records what each participant asserted, and every replica copies what it records (CLP-07-013, CM-23-016). A status rewritten before it is recorded is one nobody sent: the ledger could then neither show what the sender claimed nor let a replica verify it (ADR-0107). An omitted dimension is absent from the status the sender sent (ADR-0075), so recording it as absent is recording the status as sent. |
|||
| * | |||
RSH-09-002 |
MUST | When any acceptance rule refuses a received status, the CASE_MANAGER MUST refuse the whole update: it appends nothing, commits no ledger entry for it, adopts nothing from it, and its refusal notice to the sender MUST name every refused dimension and the reason for each. | |
*A sender can correct an update only if it learns everything that is wrong with it in one answer (EH-07-001). Naming one reason at a time would cost a round trip per mistake. |
|||
| * | Refines: RSH-09-001 | ||
RSH-09-003 |
MUST | A received ParticipantStatus MUST be refused when any of these holds: the participant's recorded RM state is CLOSED, which is terminal; its RM state moves backwards (RSH-06-002); it states a fix-readiness value but the sender does not hold the VENDOR role, or a deployment value but the sender does not hold the DEPLOYER role; a stated fix-readiness, deployment or public-state (P/X/A) value is neither the participant's most recent recorded value nor a forward move from it. A dimension the sender omits is not refused, and does not reset the recorded value a later status is judged against. Every status states an RM state: one that leaves rmState out states the initial START. The embargo state the status embeds is not judged against the case: the case derives EM from its embargo register. |
|
*Each rule refuses a claim no sequence of events could produce from the participant's recorded state. A non-adjacent forward move is accepted (RSH-06-001, CSB-16-001): a peer may have advanced several steps between messages. EM belongs to the embargo register (ADR-0122), and the report's P/X/A reaches the case only through the adoption gate (RSH-01-003). |
|||
| * | Refines: RSH-09-001 | ||
RSH-09-004 |
MUST | A CASE_MANAGER MUST evaluate the cross-machine entailments (RM↔VF and RM↔D per CSB-18-001, VF↔D per CSB-17-001) against a received ParticipantStatus as received, and MUST refuse the whole update when any is violated. It MUST evaluate the same entailment set the emit path evaluates. |
|
*Each dimension can be acceptable alone while the combination is impossible: a fix that is ready or deployed entails a report the participant has accepted, and a deployed fix entails a ready one. A replica refuses to apply such a status (RSH-09-006), so recording it would commit an entry no replica can apply. One shared evaluator keeps the emit and receive paths from diverging (ISSUE-2906). |
|||
| * | Refines: RSH-09-001 | ||
RSH-09-005 |
MUST | A participant receiving an Add(CaseStatus, VulnerabilityCase) from the case's CASE_MANAGER MUST accept the status whole only when its EM state is identical to the EM state the case derives from its embargo register and its P/X/A state is the case's or a monotone forward move from it, and MUST otherwise refuse the whole update. |
|
*EM changes only through the embargo messages that cause each register step (ADR-0122), so a status can report EM but never move it. P/X/A bits only advance, and a remote peer may report a combined advance that skips intermediate steps, so the received path uses the monotone check rather than the single-step adjacency that applies to local writes (CSB-16-002). A status partly accepted would record one nobody sent (RSH-09-001). |
|||
| * | |||
RSH-09-006 |
MUST | When applying an Announce(CaseLedgerEntry) whose entry adds a participant status, a receiver MUST evaluate the composite-state entailments (RM↔VF and RM↔D per CSB-18-001, VF↔D per CSB-17-001) against the participant state the entry carries. If any entailment is violated the receiver MUST NOT apply the status update, and MUST send Create(ProcessingFault) with failure class StatusAssertionRefused/ImpossibleState to the CASE_MANAGER. |
|
*A CASE_MANAGER that is buggy or hostile can commit an entry whose participant state violates the entailments the emit and receive paths enforce (RSH-09-004). Without this check every replica silently adopts an impossible combination it would have refused to emit. A Create(ProcessingFault) rather than a Reject(CaseLedgerEntry) avoids a CASE_MANAGER replay loop: the committed entry cannot be uncommitted, so replaying it would be refused forever (ASK-07-001). |
|||
| * |
Response Format
The Vultron protocol uses ActivityStreams activities for both requests and responses. When handlers process inbound activities, they generate response activities (Accept, Reject, Update, etc.) sent to the originating actor or other participants.
File ID: RF
Accept Response
| ID | Priority | Requirement | Related |
|---|---|---|---|
RF-02-001 |
MUST | Accept responses MUST use Accept activity type |
Implements: VP-06-004 |
RF-02-002 |
MUST | Accept responses MUST include object field referencing accepted activity or object |
|
RF-02-003 |
MUST | Accepting an Offer of an object MUST reference the Offer activity in the object field of the Accept response |
|
RF-02-004 |
MUST | When accepting an offered object, the object of the Accept activity MUST be the Offer activity itself, carried inline or by reference, not the underlying object being offered. |
|
RF-02-005 |
MUST | When accepting an Invite, the object of the Accept activity MUST be the Invite activity itself, carried inline or by reference, not the case, embargo, or any other domain object. |
Refines: RF-02-002 |
Reject Response
| ID | Priority | Requirement | Related |
|---|---|---|---|
RF-03-001 |
MUST | Reject responses MUST use Reject activity type |
Implements: VP-06-004 |
RF-03-002 |
SHOULD | Reject responses SHOULD include reason in content field |
|
RF-03-003 |
MUST | Rejecting an Offer of an object MUST reference the Offer activity in the object field of the Reject response |
TentativeReject Response
| ID | Priority | Requirement | Related |
|---|---|---|---|
RF-04-001 |
MUST | TentativeReject responses MUST use TentativeReject activity type |
Implements: VP-06-004 |
RF-04-002 |
SHOULD | TentativeReject responses SHOULD include reason in content field |
|
RF-04-003 |
MUST | Tentatively Rejecting an Offer of an object MUST reference the Offer activity in the object field of the TentativeReject response |
Error Response
| ID | Priority | Requirement | Related |
|---|---|---|---|
RF-05-001 |
SHOULD | Error responses SHOULD use ActivityStreams error extensions | Implements: VP-03-010 Implements: VP-03-011 Implements: VP-11-005 |
Response Delivery
Response Correlation
| ID | Priority | Requirement | Related |
|---|---|---|---|
RF-08-001 |
MUST | Response activities MUST include inReplyTo field |
Implements: VP-03-009 Implements: VP-11-004 Implements: VP-11-005 |
Idempotent Responses
| ID | Priority | Requirement | Related |
|---|---|---|---|
RF-09-001 |
MUST | Response generation MUST be idempotent | Depends on: ID-02-001 |
Report Management Behavioral Requirements
Event-Condition-Action behavioral requirements for Report Management (RM). Each group is triggered by either a received RM message or a local RM state transition, and specifies typed preconditions, ordered steps, and postconditions that implementations must satisfy.
File ID: RMB
Receive RS (Report Submission)
Behaviors triggered when a participant receives an RS (Report Submission) message, including RM state transition from Start to Received and vendor CS awareness update.
Trigger: Message Received — RS
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-01-001 |
MUST | A Participant in RM Start (q^rm ∈ S) receiving RS MUST transition their RM state to Received (q^rm S → R). RS is the sole RM message that directly changes the receiver's own RM state. All other RM messages announce sender state only. ECA DetailsPreconditions - Participant is in RM Start - RM state: STARTSteps 1. [receiver] Transition RM state from START to RECEIVED → Receiver RM state is RECEIVED 2. [receiver] Emit RK to acknowledge receipt of RS → RK queued for sender Postconditions - Receiver RM state is RECEIVED; RK emitted to sender |
Satisfies: VP-03-009 — RK acknowledgment of RS Satisfies: MSM-01-001 — RS shorthand wire-type mapping |
RMB-01-002 |
MUST | A Vendor Participant in RM Start receiving RS that introduces a previously unknown vulnerability (q^cs ∈ vfd···) MUST transition their CS to vendor-aware (q^cs vfd··· → Vfd···) and emit CV to announce the transition. RS is the event that makes a Vendor aware of the vulnerability. The CS V-transition records this awareness and CV announces it to other Participants. ECA DetailsPreconditions - Participant is in RM Start; Participant holds the Vendor role; CS matches pattern vfd... - RM state: START- Role: vendor- CS pattern: vfd...Steps 1. [receiver] Transition RM state from START to RECEIVED → Receiver RM state is RECEIVED 2. [receiver] Transition CS from vfd··· to Vfd··· (V event) → CS vendor-aware bit set to V 3. [receiver] Emit CV to announce the CS vendor-aware transition → CV queued for case participants 4. [receiver] Emit RK to acknowledge receipt of RS → RK queued for sender Postconditions - Receiver RM state is RECEIVED; CS is vendor-aware; CV and RK emitted |
Satisfies: VP-03-002 — Vendor MUST transition CS to Vfd on new RS Satisfies: VP-03-009 — RK acknowledgment of RS Satisfies: MSM-01-001 — RS shorthand wire-type mapping |
RMB-01-003 |
SHOULD | A Vendor Participant already in an active RM state (q^rm ∈ {R,I,V,D,A}) receiving RS for a vulnerability they were previously unaware of (q^cs ∈ vfd···) SHOULD transition CS to Vfd··· and emit CV, even though their RM state does not change. The CS V-transition captures vendor awareness regardless of when the Vendor first receives RS. A Vendor who joins a case mid-lifecycle must still record their own awareness event. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted); Participant holds the Vendor role; CS matches pattern vfd... - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTED- Role: vendor- CS pattern: vfd...Steps 1. [receiver] Transition CS from vfd··· to Vfd··· (V event) → CS vendor-aware bit set to V 2. [receiver] Emit CV to announce the CS vendor-aware transition → CV queued for case participants 3. [receiver] Emit RK to acknowledge receipt of RS → RK queued for sender Postconditions - CS is vendor-aware; CV and RK emitted; receiver RM state unchanged |
Satisfies: VP-03-002 — Vendor MUST transition CS to Vfd on receiving RS for new vulnerability Satisfies: VP-03-009 — RK acknowledgment of RS Satisfies: MSM-01-001 — RS shorthand wire-type mapping |
RMB-01-004 |
SHOULD | A Participant in an active RM state (q^rm ∈ {R,I,V,D,A}) receiving a duplicate RS (already Vendor-aware, q^cs ∈ V···) SHOULD emit RK to acknowledge receipt without changing RM or CS state. All R messages warrant acknowledgment unless the report is Closed. Duplicate RS does not re-trigger RM or CS transitions. ECA Details- Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Emit RK to acknowledge receipt of RS → RK queued for sender Postconditions* - RK emitted; RM and CS states unchanged |
Satisfies: VP-03-009 — RK acknowledgment of RS Satisfies: MSM-01-001 — RS shorthand wire-type mapping |
RMB-01-005 |
MAY | A Participant in RM Closed (q^rm ∈ C) receiving any R message MAY ignore it without sending a response. The same holds for non-R traffic (EM, CS, GI) on a closed case (VP-03-013); for the CASE_MANAGER once the case is fully closed, ignoring is not optional: it records nothing (CM-23-013). Closed cases are terminal. The protocol permits silent discard to avoid generating unnecessary traffic for completed work. The R scope is this group's trigger, not the limit of the rule: VP-03-013 covers every message class, and ADR-0085 makes owner close a hard kill switch at the CASE_MANAGER. ECA Details- Participant is in RM Closed - RM state: CLOSED |
Satisfies: VP-03-013 — Recipients MAY ignore messages received on Closed cases Satisfies: MSM-01-001 — RS shorthand wire-type mapping |
Receive RI (Report Invalid)
Behaviors triggered when a participant receives an RI (Report Invalid) message, updating the sender's RM state model.
Trigger: Message Received — RI
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-02-001 |
SHOULD | A Participant in an active RM state (q^rm ∈ {R,I,V,D,A}) receiving RI SHOULD update their record of the sender's RM state to Invalid. RI is a status announcement — it does not change the receiver's own RM state but informs them of the sender's validation result. Maintaining this record supports shared situation awareness. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Update local model of sender RM state to INVALID → Sender RM state recorded as INVALID in receiver's model Postconditions - Receiver RM state unchanged; sender's recorded RM state is INVALID |
Satisfies: VP-02-019 — Shared awareness of sender RM state change Satisfies: MSM-01-002 — RI shorthand wire-type mapping |
RMB-02-002 |
SHOULD | A Participant in RM Start (q^rm ∈ S) receiving RI SHOULD respond with both RE (to signal the unexpected message) and GI (to inquire what the sender expected). Receiving any R message other than RS while in RM Start means the sender believes the receiver is coordinating a report the receiver has no knowledge of. RE+GI is the protocol-specified error response. ECA Details- Participant is in RM Start - RM state: STARTSteps 1. [receiver] Emit RE to signal the unexpected message error → RE queued for sender 2. [receiver] Emit GI to inquire what the sender expected → GI queued for sender Postconditions* - RE and GI emitted; receiver RM state unchanged (START) |
Satisfies: VP-03-010 — RE+GI when R (other than RS) received while in RM Start Satisfies*: MSM-01-002 — RI shorthand wire-type mapping |
Receive RV (Report Valid)
Behaviors triggered when a participant receives an RV (Report Valid) message, updating the sender's RM state model.
Trigger: Message Received — RV
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-03-001 |
SHOULD | A Participant in an active RM state (q^rm ∈ {R,I,V,D,A}) receiving RV SHOULD update their record of the sender's RM state to Valid. RV announces the sender's validation result. Maintaining this record supports shared situation awareness across Participants. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Update local model of sender RM state to VALID → Sender RM state recorded as VALID in receiver's model Postconditions - Receiver RM state unchanged; sender's recorded RM state is VALID |
Satisfies: VP-02-019 — Shared awareness of sender RM state change Satisfies: MSM-01-003 — RV shorthand wire-type mapping |
RMB-03-002 |
SHOULD | A Participant in RM Start (q^rm ∈ S) receiving RV SHOULD respond with RE and GI to signal the error and inquire what the sender expected. Receiving any R message other than RS while in RM Start is an error condition indicating the sender has a mistaken expectation about the receiver's case knowledge. ECA Details- Participant is in RM Start - RM state: STARTSteps 1. [receiver] Emit RE to signal the unexpected message error → RE queued for sender 2. [receiver] Emit GI to inquire what the sender expected → GI queued for sender Postconditions* - RE and GI emitted; receiver RM state unchanged (START) |
Satisfies: VP-03-010 — RE+GI when R (other than RS) received while in RM Start Satisfies*: MSM-01-003 — RV shorthand wire-type mapping |
Receive RD (Report/Case Deferred)
Behaviors triggered when a participant receives an RD (Report/Case Deferred) message, updating the sender's RM state model.
Trigger: Message Received — RD
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-04-001 |
SHOULD | A Participant in an active RM state (q^rm ∈ {R,I,V,D,A}) receiving RD SHOULD update their record of the sender's RM state to Deferred. RD announces the sender's deferral decision. Maintaining this record supports shared situation awareness and coordination planning. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Update local model of sender RM state to DEFERRED → Sender RM state recorded as DEFERRED in receiver's model Postconditions - Receiver RM state unchanged; sender's recorded RM state is DEFERRED |
Satisfies: VP-02-019 — Shared awareness of sender RM state change Satisfies: MSM-01-004 — RD shorthand wire-type mapping |
RMB-04-002 |
SHOULD | A Participant in RM Start (q^rm ∈ S) receiving RD SHOULD respond with RE and GI to signal the error and inquire what the sender expected. Receiving any R message other than RS while in RM Start is an error condition. ECA Details- Participant is in RM Start - RM state: STARTSteps 1. [receiver] Emit RE to signal the unexpected message error → RE queued for sender 2. [receiver] Emit GI to inquire what the sender expected → GI queued for sender Postconditions* - RE and GI emitted; receiver RM state unchanged (START) |
Satisfies: VP-03-010 — RE+GI when R (other than RS) received while in RM Start Satisfies*: MSM-01-004 — RD shorthand wire-type mapping |
Receive RA (Report/Case Accepted)
Behaviors triggered when a participant receives an RA (Report/Case Accepted) message, updating the sender's RM state model to Accepted and acknowledging receipt.
Trigger: Message Received — RA
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-05-001 |
SHOULD | A Participant in an active RM state (q^rm ∈ {R,I,V,D,A}) receiving RA SHOULD update their record of the sender's RM state to Accepted. RA announces the sender's commitment to work the report. Recording this supports shared situation awareness and helps coordinate timing of follow-on actions such as embargo proposals. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Update local model of sender RM state to ACCEPTED → Sender RM state recorded as ACCEPTED in receiver's model Postconditions - Receiver RM state unchanged; sender's recorded RM state is ACCEPTED |
Satisfies: VP-02-019 — Shared awareness of sender RM state change Satisfies: MSM-01-005 — RA shorthand wire-type mapping |
RMB-05-002 |
SHOULD | A Participant in RM Start (q^rm ∈ S) receiving RA SHOULD respond with RE and GI to signal the error and inquire what the sender expected. Receiving any R message other than RS while in RM Start is an error condition indicating the sender has a mistaken expectation about the receiver's case knowledge. ECA Details- Participant is in RM Start - RM state: STARTSteps 1. [receiver] Emit RE to signal the unexpected message error → RE queued for sender 2. [receiver] Emit GI to inquire what the sender expected → GI queued for sender Postconditions* - RE and GI emitted; receiver RM state unchanged (START) |
Satisfies: VP-03-010 — RE+GI when R (other than RS) received while in RM Start Satisfies*: MSM-01-005 — RA shorthand wire-type mapping |
Receive RC (Report Closed)
Behaviors triggered when a participant receives an RC (Report Closed) message, updating the sender's RM state model to Closed and acknowledging receipt.
Trigger: Message Received — RC
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-06-001 |
SHOULD | A Participant in an active RM state (q^rm ∈ {R,I,V,D,A}) receiving RC SHOULD update their record of the sender's RM state to Closed. RC announces that the sender has completed their work on the report. Maintaining this record supports shared situation awareness and potential auto-close evaluation. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Update local model of sender RM state to CLOSED → Sender RM state recorded as CLOSED in receiver's model Postconditions - Receiver RM state unchanged; sender's recorded RM state is CLOSED |
Satisfies: VP-02-019 — Shared awareness of sender RM state change Satisfies: MSM-01-006 — RC shorthand wire-type mapping |
RMB-06-002 |
MAY | After receiving RC from all other active Participants, a Participant MAY evaluate whether local closure criteria are satisfied and, if so, initiate closure of their own RM state. When all other parties have closed, remaining open is often unnecessary. This auto-close judgment is based on local policy and current EM state. ECA DetailsPreconditions - Participant is in RM Received/Invalid/Deferred/Accepted - RM state: RECEIVED, INVALID, DEFERRED, ACCEPTED |
Satisfies: VP-02-035 — Participants MAY close Accepted, Deferred, or Invalid reports Satisfies: MSM-01-006 — RC shorthand wire-type mapping |
RMB-06-003 |
SHOULD | A Participant in RM Start (q^rm ∈ S) receiving RC SHOULD respond with RE and GI to signal the error and inquire what the sender expected. Receiving any R message other than RS while in RM Start is an error condition. ECA Details- Participant is in RM Start - RM state: STARTSteps 1. [receiver] Emit RE to signal the unexpected message error → RE queued for sender 2. [receiver] Emit GI to inquire what the sender expected → GI queued for sender Postconditions* - RE and GI emitted; receiver RM state unchanged (START) |
Satisfies: VP-03-010 — RE+GI when R (other than RS) received while in RM Start Satisfies*: MSM-01-006 — RC shorthand wire-type mapping |
Receive RE (Report Error)
Behaviors triggered when a participant receives an RE (Report Management Error) signal, which has no dedicated semantic dispatch value and is handled outside the standard AS2 pattern-matching pipeline.
Trigger: Message Received — RE
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-07-001 |
SHOULD | A Participant in an active RM state (q^rm ∈ {R,I,V,D,A}) receiving RE SHOULD emit GI (to inquire about the nature of the error). RE indicates the sender encountered an error related to the report. GI opens a dialogue to resolve the problem. No RK is sent: RK acknowledges only a report submission and never goes to the CASE_MANAGER (RMB-08-003). ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTEDSteps 1. [receiver] Emit GI to inquire about the nature of the error → GI queued for sender Postconditions - GI emitted; receiver RM state unchanged |
Satisfies: VP-03-011 — Inquire about the error with GI; RK is not sent (RMB-08-003) Satisfies: MSM-01-007 — RE shorthand wire-type mapping |
RMB-07-002 |
SHOULD | A Participant in RM Start (q^rm ∈ S) receiving RE SHOULD respond with RE (to signal the unexpected message) and GI (to inquire what the sender expected). Receiving any R message while in RM Start means the sender has a stale or incorrect case reference. RE+GI is the protocol-specified error response. Even RE itself, when received in START, signals a coordination mismatch that requires the RE+GI error response per VP-03-010. ECA Details- Participant is in RM Start - RM state: STARTSteps 1. [receiver] Emit RE to signal the unexpected message error → RE queued for sender 2. [receiver] Emit GI to inquire what the sender expected → GI queued for sender Postconditions* - RE and GI emitted; receiver RM state unchanged (START) |
Satisfies: VP-03-010 — RE+GI when R (other than RS) received while in RM Start Satisfies*: MSM-01-007 — RE shorthand wire-type mapping |
Receive RK (Report Acknowledgment)
Behaviors triggered when a participant receives an RK (Report Management Acknowledgment) message confirming receipt of a prior RM message.
Trigger: Message Received — RK
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-08-001 |
MAY | A Participant in an active RM state (q^rm ∈ {R,I,V,D,A}) receiving RK MAY take no further action; RK is an acknowledgment and requires no response. RK closes the acknowledgment loop. Emitting RK in response to RK would create an infinite acknowledgment chain. The protocol specifies no mandatory response to RK from an active-state receiver. ECA DetailsPreconditions - Participant is in an active RM state (Received/Invalid/Valid/Deferred/Accepted) - RM state: RECEIVED, INVALID, VALID, DEFERRED, ACCEPTED |
Satisfies: VP-03-009 — RK itself is never acknowledged with another RK Satisfies: MSM-01-008 — RK shorthand wire-type mapping |
RMB-08-002 |
SHOULD | A Participant in RM Start (q^rm ∈ S) receiving RK SHOULD respond with RE and GI to signal the unexpected message and inquire what the sender expected. RK arriving in START means the sender believes the receiver has already acknowledged a message the receiver has never seen. RE+GI is the protocol-specified error response for any R message received while in RM Start per VP-03-010. ECA Details- Participant is in RM Start - RM state: STARTSteps 1. [receiver] Emit RE to signal the unexpected message error → RE queued for sender 2. [receiver] Emit GI to inquire what the sender expected → GI queued for sender Postconditions* - RE and GI emitted; no RK in reply to RK (RMB-08-001); receiver RM state unchanged (START) |
Satisfies: VP-03-010 — RE+GI when R received while in RM Start Satisfies*: MSM-01-008 — RK shorthand wire-type mapping |
RMB-08-003 |
MUST | RK acknowledging a report submission MUST be sent by an actor the Offer(VulnerabilityReport) was addressed to, to the reporter (the Offer's actor), at any time after that actor has received the report, whether or not a case exists for it. RK MUST NOT be sent to the CASE_MANAGER, a receiver MUST NOT forward an RK it receives, and RK MUST NOT be ledgered.The reporter checks who may acknowledge its Offer against its own record of that Offer (RMB-08-004); an addressee's store holds one too. RK answers the reporter's original Offer and sits outside the case, which is why it is the stated exception to PCR-08-001. The CASE_MANAGER holds no record of the Offer's addressee, so it could not check an RK sent to it. Decided by the maintainer on #4325 and #4560. ECA DetailsSteps 1. [receiver of the report] Emit RK addressed to the Offer's actor → RK queued for the reporter only 2. [any receiver of RK] Record the RK at intake → No RK forwarded, no ledger entry written Postconditions - The reporter holds the RK; the CASE_MANAGER received none |
Refines: VP-03-009 — RK acknowledging RS Refines: RMB-01-001 — who sends the RK acknowledging RS, and to whom Refines: PCR-08-001 — the stated RK exception to CASE_MANAGER-only addressing |
RMB-08-004 |
MUST | A receiver of an RK acknowledging a report submission (RMB-08-003) MUST accept it only from an actor its own record of the acknowledged Offer(VulnerabilityReport) names as an addressee, and MUST refuse it (HP-01-006) when the sender is not one, or when it holds no record of that Offer, writing nothing past intake and sending nothing.The reporter sent the Offer, so its record of the Offer's to is the only trustworthy statement of who received it; the copy embedded in the RK was written by the sender. Decided by the maintainer on #4325.ECA DetailsSteps 1. [reporter] Check the RK sender against its recorded Offer's addressees → Admitted when the sender is an addressee; REFUSED otherwise Postconditions - A refused RK leaves only its intake record |
Refines: HP-01-006 — the RK sender clause |
Enter RM Received
Trigger: State Entered — RM.RECEIVED
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-09-001 |
SHOULD | A Participant entering RM Received SHOULD initiate a report validation process to determine whether the report is Valid or Invalid. A Participant that joined a case through an Invite validates the case instead, once it has joined, by replying to the full-case Invite (CM-11-011); an invitee that has not joined has nothing to validate. Received is the intake state that exists specifically to await validation. Failing to validate leaves the report stuck and blocks all subsequent coordination steps that depend on the validation outcome. ECA DetailsPreconditions - Participant is in RM Received - RM state: RECEIVEDSteps 1. [participant] Initiate validation process (credibility check followed by validity check) → Validation process underway Postconditions - Validation process initiated; report will transition to VALID or INVALID on completion |
Satisfies: VP-02-006 — Subject each Received report to a validation process Satisfies: VP-02-007 — Have a clearly defined process for validating Received reports Satisfies: VP-02-009 — Proceed only after validating received reports |
RMB-09-002 |
MAY | A Participant entering RM Received MAY temporarily hold the report pending additional information if the initial validation cannot be completed with available data. Reports may arrive with insufficient detail for immediate validation. Holding in Received preserves the option to revalidate when the Reporter provides more information. ECA DetailsPreconditions - Participant is in RM Received - RM state: RECEIVED |
Satisfies: VP-02-012 — MAY temporarily hold reports pending additional information Satisfies: VP-02-013 — SHOULD give Reporters an opportunity to update with additional info |
Enter RM Valid
Trigger: State Entered — RM.VALID
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-10-001 |
MUST | A Participant entering RM Valid MUST initiate a prioritization evaluation to determine whether to Accept or Defer the report. VP-02-002 requires that Participants prioritize Valid cases. Entering Valid without initiating prioritization blocks progression and violates the requirement. ECA DetailsPreconditions - Participant is in RM Valid - RM state: VALIDSteps 1. [participant] Initiate prioritization evaluation (e.g., SSVC scoring) → Prioritization process started; outcome is ACCEPTED or DEFERRED Postconditions - Prioritization underway; report will transition to ACCEPTED or DEFERRED |
Satisfies: VP-02-002 — Participants MUST prioritize Valid cases Satisfies: VP-02-014 — SHOULD perform a prioritization evaluation for Valid reports |
RMB-10-002 |
MUST NOT | A Participant in RM Valid MUST NOT close the report directly from the Valid state (q^rm V → C) without first transitioning through Accepted or Deferred (constraint C-03). Closing directly from Valid skips the mandatory prioritization step. Reports with no further action should be Deferred first and then Closed, preserving the prioritization record. ECA DetailsPreconditions - Participant is in RM Valid - RM state: VALID |
Satisfies: VP-02-004 — Participants MUST NOT close cases from the Valid state |
RMB-10-003 |
SHOULD | A Participant entering RM Valid SHOULD emit RV to announce the validation result to other Participants. State-change notifications support shared situation awareness. Other Participants tracking this report depend on RV to update their model of the sender's RM state. ECA DetailsPreconditions - Participant is in RM Valid - RM state: VALIDSteps 1. [participant] Emit RV to announce RM Valid transition → RV queued for case participants Postconditions - RV emitted; other Participants informed of Valid transition |
Satisfies: VP-03-004 — SHOULD send RV when validation ends in valid determination Satisfies: VP-03-012 — SHOULD send a message for each RM state transition |
RMB-10-004 |
SHOULD | A Participant entering RM Valid SHOULD create or update a case object to track the report's progress through the CVD process. Valid reports represent confirmed vulnerability coordination work. A case object enables participant management, embargo coordination, and state tracking across all involved parties. ECA DetailsPreconditions - Participant is in RM Valid - RM state: VALID |
Satisfies: VP-02-015 — SHOULD create a case for reports entering the Valid state Satisfies: VP-02-020 — SHOULD create a case for all Valid reports |
Enter RM Invalid
Trigger: State Entered — RM.INVALID
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-11-001 |
SHOULD | A Participant entering RM Invalid SHOULD emit RI to announce the invalid determination to other Participants. State-change notifications support shared situation awareness. Other Participants tracking this report depend on RI to update their model of the sender's validation outcome. ECA DetailsPreconditions - Participant is in RM Invalid - RM state: INVALIDSteps 1. [participant] Emit RI to announce RM Invalid transition → RI queued for case participants Postconditions - RI emitted; other Participants informed of Invalid transition |
Satisfies: VP-03-003 — SHOULD send RI when validation ends in invalid determination Satisfies: VP-03-012 — SHOULD send a message for each RM state transition |
RMB-11-002 |
SHOULD NOT | Participants SHOULD NOT mark duplicate reports as Invalid. A duplicate report is not invalid — it describes a real vulnerability already tracked in an existing case. Marking it Invalid misrepresents the validation outcome and may confuse other Participants. ECA DetailsPreconditions - Participant is in RM Invalid - RM state: INVALID |
Satisfies: VP-02-022 — Participants SHOULD NOT mark duplicate reports as Invalid Satisfies: VP-02-023 — Duplicate reports SHOULD pass through Valid state |
RMB-11-003 |
MAY | A Participant in RM Invalid MAY revalidate the report when new information becomes available, transitioning back through the validation process toward RM Valid. Invalid reports may become valid if additional information is provided by the Reporter or discovered independently. Revalidation preserves coordination options without requiring a new RS. ECA DetailsPreconditions - Participant is in RM Invalid - RM state: INVALID |
Satisfies: VP-02-033 — MAY periodically revalidate Invalid reports Satisfies: VP-02-013 — SHOULD give Reporters an opportunity to update with additional info |
RMB-11-004 |
MAY | A Participant in RM Invalid MAY close the report if no further action is expected and no active embargo is in force (q^em ∉ {A,R}). Invalid reports with no prospect of revalidation should eventually be closed. Active embargo constraints on closure still apply. ECA DetailsPreconditions - Participant is in RM Invalid - RM state: INVALID |
Satisfies: VP-02-035 — Participants MAY close Invalid reports Satisfies: VP-13-005 — SHOULD NOT close while an active embargo is in force |
Enter RM Deferred
Trigger: State Entered — RM.DEFERRED
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-12-001 |
SHOULD | A Participant entering RM Deferred SHOULD emit RD to announce the deferral decision to other Participants. State-change notifications support shared situation awareness. Other Participants depend on RD to update their model of the sender's prioritization decision and to calibrate their own coordination plans. ECA DetailsPreconditions - Participant is in RM Deferred - RM state: DEFERREDSteps 1. [participant] Emit RD to announce RM Deferred transition → RD queued for case participants Postconditions - RD emitted; other Participants informed of Deferred transition |
Satisfies: VP-03-005 — SHOULD send RD when prioritization ends in deferred decision Satisfies: VP-03-012 — SHOULD send a message for each RM state transition Satisfies: VP-02-024 — Vendors SHOULD communicate prioritization choices including deferral |
RMB-12-002 |
SHOULD | A Participant in RM Deferred SHOULD monitor for new information (updated threat intelligence, exploitation activity, or vendor progress) that could justify reprioritization toward Accepted. Deferred does not mean permanently shelved. Changed conditions may restore the urgency needed to resume active work on the report. ECA DetailsPreconditions - Participant is in RM Deferred - RM state: DEFERRED |
Satisfies: VP-02-017 — Reports SHOULD exit Deferred when work resumes or no further action planned |
RMB-12-003 |
MAY | A Participant in RM Deferred MAY set a policy timer to automatically move the report to Closed after a defined period of inactivity. Indefinitely deferred reports accumulate as open coordination debt. A timer enforces eventual cleanup consistent with local policy without requiring manual review of every dormant case. ECA DetailsPreconditions - Participant is in RM Deferred - RM state: DEFERRED |
Satisfies: VP-02-031 — MAY set a policy timer on Deferred reports |
Enter RM Accepted
Trigger: State Entered — RM.ACCEPTED
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-13-001 |
MUST | A Participant MUST be in RM Accepted (q^rm ∈ A) before sending RS to another Participant to initiate CVD coordination (constraint C-04). Sending RS from any other RM state is prohibited. Sending RS from a non-Accepted state would initiate coordination before the sender has committed to working the report. Accepted represents the sender's confirmed engagement and readiness to coordinate. ECA DetailsPreconditions - Participant is in RM Accepted - RM state: ACCEPTED |
Satisfies: VP-03-001 — MUST be in RM Accepted to send RS Satisfies: VP-02-003 — Participants initiating CVD with others MUST do so from Accepted state |
RMB-13-002 |
SHOULD | A Participant entering RM Accepted SHOULD emit RA to announce the accept decision to other Participants. State-change notifications support shared situation awareness. Other Participants depend on RA to know that the sender has committed to active work and may soon send RS to initiate coordination. ECA DetailsPreconditions - Participant is in RM Accepted - RM state: ACCEPTEDSteps 1. [participant] Emit RA to announce RM Accepted transition → RA queued for case participants Postconditions - RA emitted; other Participants informed of Accepted transition |
Satisfies: VP-03-006 — SHOULD send RA when prioritization ends in accept decision Satisfies: VP-03-012 — SHOULD send a message for each RM state transition Satisfies: VP-02-024 — Vendors SHOULD communicate prioritization choices including acceptance |
RMB-13-003 |
SHOULD | A Participant in RM Accepted SHOULD perform active work on the report (e.g., fix development, remediation guidance, coordination with other Participants via RS). Accepted is the active engagement state. Entering Accepted without performing work reduces coordination effectiveness and may violate implicitly agreed timelines within an active embargo. ECA DetailsPreconditions - Participant is in RM Accepted - RM state: ACCEPTED |
Satisfies: VP-02-002 — Participants MUST prioritize Valid cases; Accepted is the result of prioritization |
Enter RM Closed
Trigger: State Entered — RM.CLOSED
| ID | Priority | Requirement | Related |
|---|---|---|---|
RMB-14-001 |
SHOULD | A Participant entering RM Closed SHOULD emit RC to announce the closure to other Participants. State-change notifications support shared situation awareness. Other Participants depend on RC to know the sender has completed their work and to evaluate whether to initiate their own closure. ECA DetailsPreconditions - Participant is in RM Closed - RM state: CLOSEDSteps 1. [participant] Emit RC to announce RM Closed transition → RC queued for case participants Postconditions - RC emitted; other Participants informed of Closed transition |
Satisfies: VP-03-007 — SHOULD send RC when report is closed Satisfies: VP-03-012 — SHOULD send a message for each RM state transition |
RMB-14-002 |
MAY | A Participant in RM Closed (q^rm ∈ C) MAY ignore further R messages received on the closed report without sending a response. Non-R traffic (EM, CS, GI) on a closed case is covered by VP-03-013, and the CASE_MANAGER's kill switch once the case is fully closed by CM-23-013. Closed is a terminal state. The protocol permits silent discard to avoid generating traffic for completed work. ADR-0085 broadened the post-close rule beyond R: the CASE_MANAGER records no message of any class after owner close. ECA Details- Participant is in RM Closed - RM state: CLOSED |
Satisfies: VP-03-013 — Recipients MAY ignore messages received on Closed cases |
RMB-14-003 |
SHOULD NOT | A Participant SHOULD NOT close a report (q^rm ∈ {R,I,D,A} → C) while an active embargo is in force (q^em ∈ {A,R}) unless they first communicate their closure intent and continued embargo stance to other Participants. Closing while an embargo is active may signal withdrawal from the coordinated timeline without proper notification, potentially undermining the agreed disclosure date for all parties. ECA DetailsPreconditions - Participant is in RM Received/Invalid/Deferred/Accepted; EM state is Active or Revise - RM state: RECEIVED, INVALID, DEFERRED, ACCEPTED- EM state: ACTIVE, REVISE |
Satisfies: VP-13-005 — SHOULD NOT close while an active embargo is in force Satisfies: VP-13-007 — SHOULD communicate intent to adhere or terminate compliance when closing during an active embargo |
RMB-14-004 |
MUST | The RM transition function MUST permit closing from Received (q^rm R → C) in addition to I → C, A → C and D → C, and MUST NOT permit closing from Valid (VP-02-004). A Reject sent from RM Received — of a report Offer, a stub Invite (CM-11-007) or a full-case Invite (CM-11-011) — is the R → C transition.Reject means close and TentativeReject means invalid, so a hard no from Received needs its own edge rather than a bypass of the transition table. Valid keeps no close edge: a participant that found a report valid decides to engage or defer before it leaves (ADR-0114). |
|
RMB-14-005 |
MUST | Every RM closure MUST be written as transitions the RM transition function permits; no closure path may override the transition table. A participant at RM Valid that leaves the case (Leave(VulnerabilityCase), CM-23-012) MUST be recorded as V → D → C.Closing from Received and from Valid used to go through the force_rm_state override, which skips the table. With R → C in the table (RMB-14-004) and Valid routed through Deferred, every closure is an ordinary transition (ADR-0114). |
|
RMB-14-006 |
MUST NOT | The RM state machine MUST NOT define any transition out of RM.CLOSED to a different RM state. RM.CLOSED is terminal: once a participant's RM state reaches CLOSED, no further RM progression is possible for that participant on that case.RM.CLOSED being terminal is what gives the closure act finality. A participant that anticipates returning to the case SHOULD instead move to RM.DEFERRED (resumable via DEFERRED → ACCEPTED) rather than close; once closed, the participant has no path back in, because a re-invite to a participant at RM.CLOSED is refused (CM-11-015, ADR-0085). Rejoin mechanics are deferred to a future design; the interim workaround is defer-not-close (RMB-14-007). |
|
RMB-14-007 |
SHOULD | A participant that anticipates returning to active coordination on a case SHOULD move to RM.DEFERRED (the resumable parking state) rather than RM.CLOSED. RM.DEFERRED supports both DEFERRED → ACCEPTED (resume) and DEFERRED → CLOSED (abandon) transitions; RM.CLOSED supports neither. Because RM.CLOSED is terminal (RMB-14-006), a participant that closes and later wants back in has no protocol path. RM.DEFERRED preserves optionality: the participant can accept the case later or close it once it confirms there is nothing more to do (ADR-0085). Full rejoin mechanics (reactivation via re-invitation, multiple lifecycle arcs per actor) are deferred to a future design Idea. |
Semantic Extraction
The inbox handler extracts semantic meaning from ActivityStreams activities by matching activity type and object type patterns. This semantic type determines which handler function processes the activity.
File ID: SE
Target-Field Discriminators
| ID | Priority | Requirement | Related |
|---|---|---|---|
SE-08-003 |
SHOULD | When two semantics are discriminated only by a target-field value, a dedicated wire object type (e.g. as_CaseParticipantRole) SHOULD be preferred over target-field discrimination so that the wire shape is unambiguously self-describing without depending on registry ordering.Target-field discrimination is a latent correctness risk: a minimally-conformant or buggy sender omitting the target will be silently misrouted. A distinct object type eliminates this risk structurally and makes the wire format self-describing. See also notes/activitystreams-state-update.md § "Patterns Sharing a Verb and Object Need a Strict Discriminator". OFFER_CASE_PARTICIPANT_ROLE (Offer(CaseParticipantRole, context=VulnerabilityCase)) is the canonical example of applying this principle, introduced by ADR-0039. A CASE_MANAGER that receives one records it as a canonical ledger assertion (("Offer", "CaseParticipantRole") in _CANONICAL_PAYLOAD_SIGNATURES, CLP-07-001/CLP-07-005), with no replica state effect — a role offer is a proposal, not the grant, like Offer(CaseParticipant) (#3764); its acceptance grants the role to the target participant — the CASE_MANAGER applies it on its own copy at commit and every other replica from the ledger (CM-02-016). |
Refines: SE-08-001 |
State Machine
Requirements governing how the RM (Report Management), EM (Embargo Management), and CS (CVD Case State: VFD and PXA axes) protocol state machines are defined, represented, and executed at runtime. Each participant maintains its own RM state; the CASE_MANAGER maintains shared EM and Case State. Within CS, the PXA axis is shared across participants while the VFD axis is per-participant.
File ID: SM
State Enum Design
| ID | Priority | Requirement | Related |
|---|---|---|---|
SM-01-002 |
SHOULD | State enums SHOULD provide both a full descriptive name and a short single-character alias for each state |
State Machine Definitions
| ID | Priority | Requirement | Related |
|---|---|---|---|
SM-02-001 |
SHOULD | Each state axis SHOULD have a corresponding create_*_machine() factory function that produces a transitions library Machine instance for the axis |
Transition Definitions
State History
| ID | Priority | Requirement | Related |
|---|---|---|---|
SM-05-002 |
MUST | The current_status property (or equivalent) MUST return the single most-recently-timestamped entry from the history list |
Implements: CM-03-006 — case-management.md |
In-Memory (Transient) State
Wire and DataLayer Compatibility
| ID | Priority | Requirement | Related |
|---|---|---|---|
SM-08-001 |
MUST | State enum values MUST be stored and transmitted as their string value (not as Python enum names or as integers) |
CS Forced Transitions and Reachable States
| ID | Priority | Requirement | Related |
|---|---|---|---|
SM-09-002 |
MUST NOT | The CS state machine MUST NOT allow states in the vF* pattern (fix-ready, vendor-unaware) or *fD* pattern (fix-deployed, fix-not-ready). These 32 states are structurally impossible and MUST be rejected at validation time.A vendor cannot have deployed a fix ( D) without first having one ready (F); a fix cannot be ready (F) if the vendor is unaware (v) of the vulnerability. Together, SM-09-001 and this rule reduce the theoretical 64-state CS space to the 32 reachable CS states: 4 valid VFD combinations (vfd, Vfd, VFd, VFD) × 8 PXA combinations. See docs/reference/formal_protocol/states.md for the formal derivation. Note: the "40-state" figure in the formal protocol paper refers to the global case state space (8 PXA × 5 EM), not to CS alone. |
Refines: SM-09-001 |
State Scoping Rules
| ID | Priority | Requirement | Related |
|---|---|---|---|
SM-10-001 |
MUST | RM and VFD state transitions MUST update the sending actor's ParticipantStatus record for the affected CaseParticipant. These axes are participant-specific: each participant's RM and VFD state evolves independently. |
Refines: CM-04-001 Refines: CM-04-002 |
SM-10-002 |
MUST | EM and PXA state transitions MUST update the shared CaseStatus record for the associated VulnerabilityCase. These axes are participant-agnostic: a single shared value covers all participants in the case. |
Refines: CM-04-003 Refines: CM-04-004 |
SM-10-003 |
MUST NOT | Handlers MUST NOT apply a participant-specific axis transition (RM, VFD) to the shared CaseStatus, and MUST NOT apply a participant-agnostic axis transition (EM, PXA) to a participant's ParticipantStatus. Mixing axes is a protocol correctness violation. |
Refines: CM-04-006 |
Status Dimension Objects
Normative requirements for per-machine dimension objects decomposed from CaseStatus and ParticipantStatus. Each independent state machine (EM, PXA, RM, VF, D) is represented by a lightweight Pydantic BaseModel that holds its state enum and owns immutable transition() validation and guard methods.
File ID: SDO
transition() Contract
| ID | Priority | Requirement | Related |
|---|---|---|---|
SDO-02-001 |
MUST | Each dimension object except EmDimension MUST implement a transition(trigger) method that validates the trigger against the current state, applies the state machine rules, and returns a NEW dimension object with the updated state. EmDimension has none, because EM changes only by an embargo register step (ADR-0122).Immutable transitions are consistent with Pydantic's design philosophy and with the append-only status history model. Callers replace the embedded field rather than mutating the existing object. See ADR-0036. |
|
SDO-02-002 |
MUST | A dimension object's transition() MUST raise a descriptive exception (VultronInvalidStateTransitionError or a subclass) when the trigger is not valid for the current state. Fail-fast at the boundary; never return None or silently no-op on an invalid transition. See ADR-0032 and ARCH-15-001. |
Depends on: ARCH-15-001 |
SDO-02-004 |
MUST | Dimension objects MUST reject construction with an invalid (current_state → target_state) transition at construction time, regardless of call site. A same-state write (target == current) is always permitted; a None target (preserve current) requires no validation. Constructing a dimension object by target state bypasses the transition() validation contract established in SDO-02-001/SDO-02-002. BT write nodes that build snapshots this way must apply the same validity check themselves. See issue #1896 and BTND-10-001. |
Refines: SDO-02-002 — Extends the transition() contract to direct-construction call sites Refines: BTND-10-001 |
CaseStatus and ParticipantStatus Field Shape
Case Event Log Synchronization
Requirements for the append-only canonical recorded case ledger, replication transport, conflict handling, per-peer state tracking, and retry semantics for distributed case state synchronization across Participant Actors.
File ID: SYNC
Append-Only Log
Replication Transport
Conflict Handling
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-03-001 |
MUST | A receiver MUST reject a replication message whose predecessor hash does not match the receiver's current log tail hash | |
SYNC-03-002 |
MUST | On receiving a rejection, the sender MUST retry replication starting from the entry following the last accepted hash reported by the receiver | |
SYNC-03-003 |
MUST NOT | Replication MUST be idempotent: repeated delivery of the same log entry MUST NOT produce duplicate entries in the receiver's log | Implements: ID-02-001 — idempotency.md |
SYNC-03-004 |
SHOULD | When a Participant Actor sends any message to the CASE_MANAGER, it SHOULD include the hash of the last canonical recorded log entry it has applied as a parameter in the activity's context field | |
SYNC-03-005 |
MUST | The CASE_MANAGER MUST act on Reject(CaseLedgerEntry) only when the sender is an active participant in the case (CM-10-004). From any other sender it MUST refuse the message, record no replication state for the sender, and replay no entry and no case to it.A rejection with an empty last-accepted hash makes the CASE_MANAGER replay the whole ledger and announce the case, so an unchecked sender obtains embargoed content just by asking. The replay cooldown limits how often, not who. See #3733. |
Refines: HP-01-006 Refines: SYNC-03-002 |
SYNC-03-006 |
MUST | A receiver MUST recompute the hash of a received CaseLedgerEntry from its content and compare it with the entry's stated entryHash before buffering, applying or storing the entry. An entry that states no hash, or whose content does not reproduce it, MUST be refused, and MUST NOT be buffered, applied or stored.The predecessor check (SYNC-03-001) proves an entry claims the right place in the chain, not that its content is what was hashed. A refused entry is not answered with Reject(CaseLedgerEntry): a replay resends the same entry, and the receiver's unmoved tail already makes the next entry trigger one. The receiver reports the refusal with a processing fault instead (SYNC-03-007). See #3744. |
Refines: SYNC-01-003 Refines: SYNC-08-001 |
SYNC-03-007 |
MUST | A participant that refuses a CaseLedgerEntry under SYNC-03-006 MUST send Create(ProcessingFault) with failure class LedgerEntryRefused/HashMismatch to the case's CASE_MANAGER, naming the refused Announce(CaseLedgerEntry) by reference. The fault's id MUST be derived from the refused Announce and the failure class.The entry was received but could not be processed, which ASK-07-001 and MSM-05-001 answer with a processing fault. Without it only the replica's log shows the refusal, while the CASE_MANAGER's replay resends the same entry. A hash mismatch is not a divergence: a fresh case copy cannot repair it, because the replica still cannot store the entry, so it takes this fault and not SYNC-16-001's. Like that fault, it is replication signalling and is never ledgered (SYNC-16-002). |
Refines: SYNC-03-006 Refines: ASK-07-001 |
SYNC-03-008 |
MUST | When the CASE_MANAGER receives the SYNC-03-007 fault from an active participant of the case (CM-10-004), it MUST NOT send anything, re-seed the peer or record replication state because of the fault. From any other sender it MUST refuse the fault. A hash mismatch comes from deterministic content: a fault in rendering or parsing the wire form, or a broken stored entry the send-side check (SYNC-03-009) did not see because fan-out checked its in-memory copy. Neither a replay nor a re-seed can repair it; only a code fix or an operator can. The reject-and-replay loop it leaves is already bounded by the replay cooldown (SYNC-15-003, SYNC-15-013). |
Refines: SYNC-03-007 Refines: SYNC-16-002 |
SYNC-03-009 |
MUST | Before the CASE_MANAGER sends a CaseLedgerEntry in Announce(CaseLedgerEntry), whether by fan-out, replay or backfill, it MUST recompute the entry's hash from its content as SYNC-03-006 does. An entry that states no hash, or whose content does not reproduce it, MUST NOT be sent and MUST be logged at ERROR with the case, the entry, its log_index and the recipients, and the CASE_MANAGER MUST NOT send any later entry of the same send. This takes precedence over SYNC-03-002, SYNC-17-004 and CM-10-006, which otherwise oblige it to send every entry in a range.A broken entry in the CASE_MANAGER's own store is found where it starts, not after a replica refuses it (SYNC-03-006) and reports it (SYNC-03-007), and broken content never leaves the CASE_MANAGER. Holding the entry back stalls the replica at it, as sending it would. The replica's Reject loop stays bounded without the replay cooldown: a replay held back at the broken entry sends nothing, so it records no position (SYNC-15-011), and the replica sends one Reject per later fan-out, with nothing replayed to amplify it. A later entry is not sent because the replica could only hold it as a forward gap (SYNC-14-001). The receiver's check stays, because a fault in rendering or parsing the wire form still shows up first at the receiver. |
Refines: SYNC-03-006 Refines: SYNC-08-001 Constrains: SYNC-03-002 Constrains: SYNC-17-004 Constrains: CM-10-006 |
Per-Peer Replication State
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-04-001 |
MUST | The replication leader MUST track per-peer state including at minimum the last acknowledged log entry hash for each peer |
Retry and Backoff
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-05-001 |
SHOULD | The replication sender SHOULD implement retry with exponential backoff on delivery failure |
Leadership and Ownership
Commit Discipline
System Invariants
Catch-Up Gate Before New Actions
Effects-Before-Persist Invariant
Ledger Write Ownership and Layer Boundary
SYNC-12's already-stored gate (SYNC-12-003) uses the presence of a CaseLedgerEntry in an actor's DataLayer as the sole evidence that the entry's effects were already applied. That inference is only sound if the ONLY thing that ever writes a CaseLedgerEntry to the DataLayer is the core write path that also applies the effects. If any other layer (e.g. a wire/adapter parse or rehydration helper) creates a CaseLedgerEntry record, the gate fires SUCCESS on first delivery and the effects are silently skipped. These requirements make the write-ownership boundary that SYNC-12 depends on explicit.
Out-of-Order Entry Buffering and Drain
Announce(CaseLedgerEntry) is delivered over a transport with no ordering guarantee, and Vultron may not be the only implementation on the wire, so a replica can receive an entry before its hash-chain predecessor. Dropping such an entry and relying on a reject/replay round-trip does not converge under adversarial reordering, because replay re-announces entries individually over the same unordered transport (they can reorder and be dropped again). These requirements make replica convergence independent of delivery order by holding the received Announce of a forward-gap entry in a non-ledger holding area and replaying it, in hash-chain order, once the entry that closes the gap is committed. What is held and replayed is the Announce that was received, never one the replica builds. Rationale: ADR-0037 (docs/adr/0037-buffer-out-of-order-ledger-entries.md), ADR-0135.
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-14-001 |
MUST | When a participant receives a valid Announce(CaseLedgerEntry) whose prev_log_hash does not match its current chain tail but whose log_index is strictly greater than tail_index + 1 (a forward gap), the participant MUST NOT permanently discard the entry; it MUST retain the Announce(CaseLedgerEntry) that carried it, as the body that arrived (VM-08-002), in a holding area pending arrival of its predecessor. It MUST NOT retain the entry apart from that Announce. The hash chain makes indefinite retention safe: a held entry is applied only when an entry whose entry_hash equals the held entry's prev_log_hash is committed, so a fork/rewrite cannot masquerade as the predecessor. Holding the Announce rather than the entry is what lets the drain apply what was received: an entry held alone can only reach the inbox again inside an Announce the replica makes up, attributed to a sender that never sent it (ISSUE-5012). |
|
SYNC-14-002 |
MUST | A forward-gap entry whose Announce is held MUST still trigger resynchronization (SYNC-08-005) — the participant MUST send Reject(CaseLedgerEntry) carrying its last accepted hash so the CASE_MANAGER replays entries that are genuinely lost (never delivered) rather than merely reordered. | |
SYNC-14-003 |
MUST | After a participant commits a CaseLedgerEntry, it MUST drain the holding area: for each newly committed tail, it MUST locate the held Announce (if any) whose entry's prev_log_hash equals the new tail's entry_hash, apply that entry, and repeat, cascading until no held Announce extends the current tail. | |
SYNC-14-004 |
MUST | Draining a held entry MUST apply the entry's domain effects before persisting it (SYNC-12-001). | Depends on: SYNC-12-001 — The drain MUST preserve the effects-before-persist invariant per applied entry |
SYNC-14-007 |
MUST NOT | The drain path MUST NOT re-apply effects for an entry already present in the local ledger (SYNC-12-003). Re-applying effects for a previously committed entry would cause duplicate state transitions and violate the idempotency guarantee established by SYNC-12-003. A replayed Announce reaches the same already-stored check as any delivery (SYNC-14-008), so an entry committed between its hold and its replay is skipped, not applied again. |
|
SYNC-14-008 |
MUST | Applying a drained entry MUST follow the same apply-then-persist rules as in-order delivery, starting from the held Announce as in-order delivery starts from the received one. Using a separate drain path risks diverging from the in-order path over time, reintroducing the ordering bugs that SYNC-12 was designed to prevent. |
Genesis-Unavailable Buffer-and-Reject
A participant replica receives Announce(CaseLedgerEntry) activities from the CASE_MANAGER over an unordered transport. If the CASE_MANAGER delivers a ledger entry before the Create/Announce(VulnerabilityCase) activity has been processed by the participant (a delivery race under HTTP BackgroundTasks), the replica cannot reconstruct the per-case genesis hash (CLP-08-005) and must not silently drop the entry. Silently dropping causes the entry to be permanently lost because the CASE_MANAGER never learns that delivery failed and never replays. A pre-genesis entry is therefore held in the same non-ledger holding area used for forward gaps (SYNC-14) and drained once the case seed anchors the genesis hash, making convergence independent of whether the case or its ledger entries arrive first (issue #2186, #2180). A Reject is still sent as the loss backstop, triggering the same replay path as the hash-mismatch case (SYNC-08-005, SYNC-14-002) for an entry that is genuinely lost rather than merely early. The forward-gap hold of SYNC-14 alone does not cover this window: it only holds entries for a case whose genesis anchor is already known, so a distinct pre-genesis hold and a drain triggered by the case seed are required. Rationale for the buffer-and-drain path (SYNC-15-004, SYNC-15-005): ADR-0059 (docs/adr/0059-buffer-pre-genesis-ledger-entries.md).
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-15-001 |
MUST | When a participant receives an Announce(CaseLedgerEntry) but cannot reconstruct the per-case genesis hash because it does not yet hold the VulnerabilityCase (CLP-08-005), the participant MUST NOT silently discard the entry; it MUST send a Reject(CaseLedgerEntry) with last_accepted_hash="" to the CASE_MANAGER so that the CASE_MANAGER replays all entries from the beginning once the case is delivered. last_accepted_hash="" is the conventional signal for "I have no entries; replay from genesis." The CASE_MANAGER's replay path (SYNC-03-002) replays all entries after the supplied hash; an empty string matches no recorded entry, so it replays the full log. Without the Reject the participant is permanently stuck (issue #1873). |
Depends on: CLP-08-005 — The missing-case failure originates from the genesis-hash requirement in CLP-08-005 Depends on: SYNC-08-005 — Reject-on-divergence applies equally to the genesis-unavailable case |
SYNC-15-002 |
MUST NOT | A participant MUST NOT persist a CaseLedgerEntry when the per-case genesis hash is unavailable. |
Depends on: SYNC-12-001 — Effects-before-persist invariant applies; an entry whose genesis is unknown cannot be safely committed |
SYNC-15-006 |
MUST NOT | A CaseLedgerEntry received when the per-case genesis hash is unavailable MUST NOT be treated as committed.Treating a pre-genesis entry as committed would populate the already-stored gate (SYNC-12-003) prematurely, causing effects to be skipped when the entry is later drained after the genesis is seeded. |
|
SYNC-15-007 |
MUST NOT | The domain effects of a CaseLedgerEntry received when the per-case genesis hash is unavailable MUST NOT be applied (SYNC-12-001).Applying effects before the genesis hash is known means the effects cannot be attributed to a verified chain position; if the genesis hash later reveals the entry is a fork, already-applied effects cannot be rolled back. |
|
SYNC-15-003 |
MUST | The CASE_MANAGER MUST bound the rate at which it replays entries to a peer whose acknowledged replication position has not advanced. | |
SYNC-15-008 |
MUST | The CASE_MANAGER MUST record, per peer, the position (the entry_hash a replay resumed from, or "" for genesis) of the last replay in which at least one entry was actually sent, together with the time it was sent.Recording the position and timestamp of the last non-empty replay is the data the cooldown check needs to determine whether a subsequent Reject at the same position falls within the cooldown window. |
|
SYNC-15-009 |
MUST NOT | A Reject reporting an unchanged replication position within the cooldown MUST NOT trigger another replay. A peer that cannot anchor its hash chain Rejects every entry replayed to it, producing a self-sustaining amplification loop. Suppressing replays within the cooldown window for an unchanged position prevents the ~185x amplification observed in issue #1989. |
|
SYNC-15-010 |
MUST | A Reject reporting an advanced replication position MUST always trigger a replay. An advanced position means the peer successfully committed at least one entry since the last replay; withholding replay at that point would stall convergence. |
|
SYNC-15-011 |
MUST NOT | A replay in which zero entries were sent MUST NOT update the recorded position, because no delivery was attempted at that position. Reject-triggered replay (SYNC-08-005, SYNC-14-002) is unconditional by default, and a peer that cannot anchor its hash chain Rejects every entry replayed to it. Each Reject replays the full missing suffix and each replayed entry produces another Reject — a self-sustaining amplification loop, observed in CI as 4825 Announce(CaseLedgerEntry) activities for a 26-entry ledger (~185x), starving the actor until unrelated DataLayer reads timed out (issue #1989). |
Depends on: SYNC-08-005 — Bounds the replay this rule triggers; the reject-on-divergence contract itself is unchanged Depends on: SYNC-03-002 — The replay-from-hash path whose rate this rule limits Refines: SYNC-15-001 — The genesis Reject that SYNC-15-001 mandates is the highest-volume source of no-progress replays |
SYNC-15-004 |
MUST | When a participant receives a valid Announce(CaseLedgerEntry) but cannot reconstruct the per-case genesis hash because it does not yet hold the VulnerabilityCase (CLP-08-005), the participant MUST NOT rely solely on the reject/replay round-trip; it MUST retain the Announce that carried the entry, as received, in the same non-ledger holding area used for forward gaps (SYNC-14-001), keyed by the entry's prev_log_hash, pending arrival of the case seed. Unlike a forward gap (SYNC-14-001) this hold applies without a tail_index comparison — no chain tail exists in the pre-genesis window — so every entry for the missing case is held, including the genesis entry (log_index 0) whose prev_log_hash equals the per-case genesis hash. The reject/replay backstop (SYNC-15-001) does not converge under adversarial reordering for the same reason forward gaps are held (SYNC-14): replay re-announces entries individually over the same unordered transport and each pre-genesis entry Rejects again, amplifying churn (issue #2169) and, for the rvcv-handoff demo, dropping the add_report_to_case entry so no VultronOfferRecord is ever created (issue #2180). Holding the entry lets it drain deterministically once the case is seeded. |
Depends on: SYNC-14-001 — Reuses the same non-ledger holding area and prev_log_hash keying as the forward-gap hold Depends on: SYNC-14-005 — A pre-genesis entry MUST NOT be persisted as a CaseLedgerEntry while held Refines: SYNC-15-001 — The Reject is retained as the loss backstop; holding is the primary convergence path |
SYNC-15-005 |
MUST | After a participant seeds the VulnerabilityCase (via Create or Announce(VulnerabilityCase)), which anchors the deterministic per-case genesis hash (CLP-08), it MUST drain the holding area for that case: it MUST locate the held Announce (if any) whose entry's prev_log_hash equals the per-case genesis hash, replay it (SYNC-14-009) so its entry is applied-then-persisted via the same path as in-order delivery (SYNC-14-004), and cascade in hash-chain order (SYNC-14-003) until no held Announce extends the current tail. The genesis hash is derivable from the case object alone (deterministic from case_id, created_at, and the CASE_MANAGER id per CLP-08), so seeding the case is sufficient to anchor the chain — the participant need not wait for the genesis ledger entry to be re-delivered. Draining on seed is what turns the pre-genesis hold (SYNC-15-004) into convergence. |
Depends on: SYNC-14-003 — Reuses the cascading drain defined for forward gaps Depends on: SYNC-14-004 — The drained entry MUST apply effects before persisting, on the same path as in-order delivery Depends on: SYNC-15-004 — Drains exactly the entries that SYNC-15-004 held during the pre-genesis window |
SYNC-15-012 |
MUST NOT | The CASE_MANAGER MUST NOT enqueue a replay Announce(CaseLedgerEntry) for an (entry, peer) pair while an earlier Announce of that same entry to that same peer is still pending in its outbox.A Reject that arrives after the peer's gap has already drained still reports an advanced position, so SYNC-15-010 replays — and that suffix is usually still in the outbox from the previous replay. Ten such Rejects queued 116 duplicate Announces ahead of the next entry's fan-out to every other peer (#3602). SYNC-15-010 stands; this only stops the same row being queued twice. See ADR-0112. |
Refines: SYNC-15-003 |
SYNC-15-013 |
MUST | Each consecutive non-empty replay the CASE_MANAGER sends to a peer from an unchanged replication position MUST lengthen the cooldown before the next one (doubling it, up to a fixed ceiling), and a replay from a different position MUST reset the cooldown to its base value. A fixed cooldown ends the Reject/replay loop only when one round trip finishes inside it. A loaded host, or a peer that can never anchor its chain, kept the loop running for as long as the peer kept rejecting (#4882). A growing cooldown bounds a stuck peer to a logarithmic number of replays over any period, whatever the round-trip time. The ceiling keeps it a rate limit rather than suppression (SYNC-15-011 note). |
Refines: SYNC-15-003 Depends on: SYNC-15-008 — The per-peer record carries the count of replays at its position |
SYNC-15-014 |
MUST NOT | The CASE_MANAGER MUST NOT send the genesis Announce(VulnerabilityCase) pre-seed (SYNC-15-002) in answer to a genesis Reject whose replay the cooldown suppresses.The pre-seed exists to anchor the replay that follows it; while that replay is rate-limited the peer already holds the case sent with the last one. Sending the case once per Reject anyway kept one message per Reject flowing in the loop SYNC-15-003 bounds (#4882). |
Refines: SYNC-15-003 |
Divergence Recovery
Under the copy model (ADR-0124) a replica applies each entry through one apply function that refuses an entry whose starting state does not match the replica's case file. When the refused entry already extends the replica's stored chain, no replay can fill the gap: the replica's case file has diverged from the CASE_MANAGER's, and the reject and replay path resends the same refused entry forever. A divergence is a defect, not a steady state, so the replica reports it and the CASE_MANAGER repairs it with a fresh case copy that names the ledger position it reflects. The fault is replication signalling, like the gap Reject, and is never ledgered. Rationale: ADR-0142.
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-16-001 |
MUST | When a participant refuses a CaseLedgerEntry whose prev_log_hash matches its chain tail because the entry's starting state does not match its case file, and its case file is not already past the entry, it MUST send Create(ProcessingFault) with failure class LedgerEntryRefused/CaseDiverged to the case's CASE_MANAGER, naming the refused Announce(CaseLedgerEntry) by reference. It MUST keep holding the refused entry as SYNC-14-001 holds a forward gap.The entry was received but could not be processed, which MSM-05-001 assigns to a processing fault. Without it the CASE_MANAGER sees only gap Rejects, and its replay resends the entry the replica refuses. An entry whose content is invalid in itself, such as the impossible participant status of RSH-09-006, is not a divergence: a fresh copy cannot repair it, so it takes that requirement's fault instead of this one. |
Refines: ASK-07-001 |
SYNC-16-002 |
MUST NOT | A processing fault that reports a failure to apply a CaseLedgerEntry MUST NOT be committed to the case ledger, by the participant or by the CASE_MANAGER. The CASE_MANAGER's response to it is per-peer replication state (SYNC-04), except for the hash-mismatch fault, which changes nothing (SYNC-03-008).It reports on replication, as Reject(CaseLedgerEntry) does. Ledgering it loops: the diverged replica cannot apply the entry that records its own fault, and a failure to apply that entry would be another fault to ledger. |
Refines: CLP-04-007 Constrains: RSH-09-006 |
SYNC-16-003 |
MUST | When the CASE_MANAGER receives the SYNC-16-001 fault from an active participant of the case (CM-10-004), it MUST send that participant Announce(VulnerabilityCase) of its current case. From any other sender it MUST refuse the fault, send nothing and record no replication state for the sender.The CASE_MANAGER's case is authoritative (PCR-03-001), and a copy carrying its ledger position (SYNC-16-005) lets the replica replace its case file and backfill (SYNC-16-006, SYNC-16-007). The sender gate is the one the gap Reject uses (SYNC-03-005). |
Depends on: SYNC-16-005 |
SYNC-16-004 |
MUST | The CASE_MANAGER MUST bound the rate at which it re-seeds a peer under SYNC-16-003. It MUST record, per peer, the time and ledger tail of the last re-seed, and a fault received within the cooldown MUST NOT trigger another re-seed; a fault after it MUST. A diverged replica faults once per replayed entry it refuses. A rate limit stops a re-seed storm without stranding a peer whose re-seed was lost, which a permanent suppression would (SYNC-15-003 makes the same choice for replay). |
Refines: SYNC-16-003 |
SYNC-16-005 |
MUST | Each VulnerabilityCase MUST store its ledger position (ledgerPosition), the position its case file is a projection of: the log_index and entry_hash of the last entry applied to it, or, before any, index -1 and the per-case genesis hash. Committing an entry, or storing one as a replica, MUST move the position to that entry atomically with storing the entry, unless the position is already past it or names it, which MUST leave the position unchanged. A ledger entry whose payloadSnapshot carries the case MUST carry it at the position before that entry. The position MUST travel with the case on its wire form, so a copy names the position it reflects, and a received case that carries none MUST be refused.A case copy is the case as applying the ledger up to some entry leaves it (ADR-0124); the position names that entry, so a receiver can tell a current copy from a stale one. Stored on the case, it is right on a replica too, whose case can be ahead of its own chain after it replaces its case with a copy. Moving it with the entry it names keeps the two from disagreeing, and a snapshot taken before the entry keeps any entry's hash from depending on itself. Never moving it back keeps a case a copy put ahead of its chain at the copy's position while the chain catches up (SYNC-16-007); a different entry at the position's index is a conflict, not one it already reflects. |
Refines: PCR-02-001 |
SYNC-16-006 |
MUST | A participant that receives Announce(VulnerabilityCase) from the case's CASE_MANAGER for a case it already holds, a repeat of an earlier Announce included, MUST compare the copy's ledger position (SYNC-16-005) with its own: the later of its chain's tail and its case's stored position. If its own is already past the copy's, it MUST leave its case file unchanged, as for a repeat delivery. If it holds an entry at the position's index, or its case names one there, whose hash differs, it MUST leave its case file unchanged and refuse the copy. Otherwise it MUST replace its case file with the copy, taking the copy's position.The transport is unordered and the outbox retries, so an old copy can arrive late; replacing with it would drop the effects of the entries the replica already holds past the copy. A different entry at the same index means the chains themselves disagree, which no copy repairs. The case's stored position counts as well as the chain, because after a replacement the case can be ahead of the chain, and a copy older than the replacing one must not move it back. |
Refines: PCR-02-004 |
SYNC-16-007 |
MUST | After replacing its case file under SYNC-16-006 with a copy whose position is ahead of its chain, a participant MUST send Reject(CaseLedgerEntry) carrying its own tail. A participant whose case is at a position ahead of its chain, after such a replacement or a first seed from a copy (PCR-02-003), MUST store each entry it receives at or before that position, whether replayed or drained from its holding area (SYNC-14-003), as an idempotent no-op, not refuse it, and MUST NOT move the case's position back for it (SYNC-16-005). It MUST refuse, not store, an entry at the position's index whose hash differs from the one the position names, and log the conflict at ERROR. When its chain stores no entry, the Reject MUST name no rejected entry, MUST carry an empty last-accepted hash and MUST name the case as its target, asking for a replay from genesis; a receiver MUST refuse a Reject that names no entry yet carries a last-accepted hash, and the CASE_MANAGER MUST answer the empty-hash form as it answers any replay from genesis (SYNC-15-002).Those entries are already reflected in the copy. The refused entry the replica held under SYNC-16-001 drains first, before any replay arrives, so a rule that covered only replayed entries would refuse it again. Storing them advances the chain to the copy's position, after which the entries that follow apply normally, so the replica ends where seed-plus-replay leaves it (ADR-0114). A first seed is the same case: the copy already reflects the entries the joining replica's backfill brings. The entries before the position carry no hash the case names, so a chain that diverged among them shows only at the position's index; refusing the entry there keeps the chain from naming one entry and the case another. A replica with no entry has none for the Reject to name; waiting for the next fan-out would stall it on a quiet case, so it names the case instead and asks for everything. |
Depends on: SYNC-16-006 Refines: SYNC-03-002 |
Participant Position Statements
A participant's replica can fall behind with nothing to show it: when the last Announce(CaseLedgerEntry) before a quiet period is lost, no later entry arrives with a gap, so no Reject(CaseLedgerEntry) is sent and the reject-and-replay loop never starts. The CASE_MANAGER cannot see the lag either, because nothing tells it how far a participant has got. The participant therefore states its position, when it chooses, in a Read(CaseLedgerEntry), and the CASE_MANAGER answers a position behind its tail by replaying. A position stated on every case activity is planned separately (#5109, #5110) and composes the same answer. This is how the LedgerReconciliation phase detects a gap and retries until convergence (SYNC-00-003, SYNC-00-007, SYNC-00-008). Rationale: ADR-0155.
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-17-001 |
MAY | A participant MAY send the case's CASE_MANAGER Read(CaseLedgerEntry) whose object is the last canonical entry it has applied for that case, by reference. A participant that has applied no entry for the case has no entry to name and sends no Read(CaseLedgerEntry).ActivityStreams defines Read as the actor having read the object, so the activity states a fact and asks for nothing. A Reject(CaseLedgerEntry) would claim a refusal that did not happen. |
|
SYNC-17-002 |
MUST | The CASE_MANAGER MUST act on a participant's stated ledger position in Read(CaseLedgerEntry) (SYNC-17-001) only when the sender is an active participant in the case (CM-10-004). From any other sender it MUST record no replication state for the sender, send nothing in answer, and report REFUSED.A removed, unconsented or closed participant's replica stops on purpose, so its stated position always lags. Answering it would send withheld case content, as an unchecked Reject would (SYNC-03-005). Catch-up for a participant that becomes active again is CM-10-006's backfill. |
Refines: CM-10-004 Refines: HP-01-006 |
SYNC-17-003 |
MUST | When the CASE_MANAGER acts on a stated position (SYNC-17-002) that names an entry in its ledger, it MUST record that entry's hash as the sender's acknowledged position in its per-peer replication state (SYNC-04-001). | Refines: SYNC-04-001 |
SYNC-17-004 |
MUST | When the position the CASE_MANAGER acts on (SYNC-17-002) is behind its ledger tail, it MUST send the sender every canonical entry after that position, in log order, as Announce(CaseLedgerEntry), under the same replay rate bound (SYNC-15-003, SYNC-15-013) and outbox de-duplication (SYNC-15-012) as a Reject-triggered replay. When the position is the tail it MUST send nothing.This is the retry that SYNC-00-008 requires. It runs whenever the participant reads, so a lost last entry is recovered without waiting for a new commit. A Read can predate entries already on their way to the sender, so a stated lag can be transient; the replay rate bound, not the outbox de-duplication, is what limits the replays such a race causes. |
Refines: SYNC-00-007 Refines: SYNC-00-008 Refines: SYNC-03-002 |
SYNC-17-005 |
MUST | When a stated position names an entry the CASE_MANAGER's ledger does not hold, or an index at which its entry has a different hash, the CASE_MANAGER MUST log it at WARNING with the sender and the position, record no replication state and send nothing. Such a position is not a lag, and replaying from a guessed point would send a suffix the replica cannot anchor. A replica whose case file has diverged reports it by SYNC-16-001. |
|
SYNC-17-006 |
MUST NOT | The CASE_MANAGER MUST NOT commit a ledger entry for a Read(CaseLedgerEntry) or for its answer to one.Stating a position changes nothing in the case file, so it is an observation and not a case event (ADR-0152). Ledgering it would add an entry every time a participant checks, which itself would leave every other participant behind. |
Sender Ledger Position on Case Activities
Every case activity states the ledger position its sender held when it built the activity, so the case ledger records what each actor knew when it acted. The position is a record, not a gate: an actor that lags the CASE_MANAGER's latest entry may still act (SYNC-10-005), and a stale answer to a replaced object is already refused by the object it names (CM-18-003). A lagging position is answered as a Read(CaseLedgerEntry) is (SYNC-17), and a position the CASE_MANAGER's ledger does not hold is a split chain, which it repairs. Rationale: ADR-0156.
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-18-001 |
MUST | An activity about a case whose ledger its sender holds, sent by any actor, MUST carry the sender's ledger tail for that case as the activity's ledgerPosition: an object whose logIndex and entryHash name the last entry the sender had committed, or had stored as a replica, when it built the activity; for an empty ledger, index -1 and the per-case genesis hash. An activity sent before its sender holds the case's ledger, and an activity whose object already states a ledger position (Announce(CaseLedgerEntry), Announce(VulnerabilityCase)), MUST NOT carry one.One rule for every sender and message type gives every ledgered act its provenance, and a new message type needs no decision about whether it counts. A stub-Invite reply or a report Offer comes from an actor with no ledger to state; an entry and a case copy already state their own positions. The property is the ledgerPosition term a case copy already carries (SYNC-16-005), so one fact keeps one wire form. |
Refines: SYNC-16-005 |
SYNC-18-002 |
MUST | ledgerPosition MUST be the sender's ledger tail for the case at the moment the activity is built, and MUST be produced only by vultron.wire.as2.factories. A resent activity MUST carry the position it was first sent with.The position describes the moment of the act, so a later resend that restamped it would misstate what the sender knew. The outbox already relays the sealed body (OX-07-001), which keeps the first position. |
Refines: SYNC-18-001 |
SYNC-18-003 |
MUST | A receiver MUST parse ledgerPosition as the core LedgerPosition at the parse edge, and MUST refuse an activity that SYNC-18-001 requires to carry one when its ledgerPosition is missing or does not parse, naming the activity and writing nothing.Core code then receives a non-optional position (ADR-0032), and every ledgered act carries one. |
Refines: SYNC-18-001 |
SYNC-18-004 |
MUST NOT | The CASE_MANAGER MUST NOT refuse, hold or defer an activity because its ledgerPosition is behind the CASE_MANAGER's own ledger tail, and MUST log such a lag at DEBUG with the activity and actor. A reply to the full-case Invite is the one exception, judged against its Invite's floor (CM-11-012).Under asynchronous fan-out an active participant is routinely a few entries behind, and SYNC-10-005 lets it act while it lags. A stale answer to a replaced embargo is already refused, because its consent row accepts no trigger (CM-18-003), so refusing on the position alone would refuse ordinary acts and catch nothing new. |
Refines: SYNC-10-005 |
SYNC-18-005 |
MUST | When an activity's ledgerPosition names an index the CASE_MANAGER's ledger holds with a different entry hash, or an index past its own tail, the CASE_MANAGER MUST judge the activity as it would without the position, MUST log the split at ERROR, and, when the sender is an active participant of the case (CM-10-004), MUST send it Announce(VulnerabilityCase) of its current case under the rate bound of SYNC-16-004. It MUST NOT re-seed any other sender.The CASE_MANAGER is the single writer, so no participant can hold an entry it did not commit; such a position means the sender's chain has split. The fresh copy repairs it with the path SYNC-16-003 already uses for a reported divergence. A Read(CaseLedgerEntry) naming such a position is answered with nothing instead (SYNC-17-005); that difference, and whether the act itself should be refused, are open (notes/sync-ledger-replication.md). |
Refines: SYNC-16-003 |
SYNC-18-006 |
MUST | The ledgerPosition of an admitted activity MUST be recorded in its ledger entry as part of the received body (CLP-07-011), and MUST NOT be removed from it.The entry is the received envelope with a postmark (ADR-0107), so the position is ledgered with the act it describes and replicas receive it unchanged. |
Refines: CLP-07-011 |
SYNC-18-007 |
MUST | When an activity's ledgerPosition names an entry in the CASE_MANAGER's ledger and the sender is an active participant of the case (CM-10-004), the CASE_MANAGER MUST answer the position as it answers one stated in Read(CaseLedgerEntry): it MUST record the entry's hash as the sender's acknowledged position (SYNC-17-003) and, when the position is behind its tail, send the entries after it under the replay rate bound and de-duplication of SYNC-17-004. From any other sender it MUST record no replication state and send nothing in answer to the position. Neither the record nor the replay changes the activity's own outcome.A stamp states the same fact as a Read(CaseLedgerEntry), so one answer serves both (ADR-0155): a participant that sends anything also tells the CASE_MANAGER how far it has got, and one that lost the last entry is repaired by its next act. A removed, unconsented or closed participant lags on purpose, so answering it would send withheld case content (SYNC-17-002). |
Extends: SYNC-17-003 Extends: SYNC-17-004 |
Testability
The Vultron inbox handler must be thoroughly testable at unit, integration, and system levels. Testability enables confidence in correctness, supports refactoring, and documents expected behavior.
File ID: TB
Integration Test Coverage
Triggerable Behaviors
Requirements for the trigger API that allows an actor to initiate protocol behaviors based on its own internal state, rather than reacting to an inbound ActivityStreams message. Triggerable behaviors are the outgoing counterpart to the inbound handler pipeline and are required to demonstrate the full CVD protocol.
File ID: TRIG
Candidate RM Behaviors
BT Integration
| ID | Priority | Requirement | Related |
|---|---|---|---|
TRIG-05-001 |
SHOULD | Trigger endpoint implementations SHOULD reuse existing BT trees rather than duplicating behavior logic |
Outbox Activity
| ID | Priority | Requirement | Related |
|---|---|---|---|
TRIG-07-001 |
MUST | A successfully executed trigger MUST produce an outgoing ActivityStreams activity added to the actor's outbox | Depends on: OX-02-001 |
Generalized Object Triggers and Type-Specific Wrappers
Case Ownership Transfer Triggers
| ID | Priority | Requirement | Related |
|---|---|---|---|
TRIG-11-001 |
MUST | The system MUST expose an offer-case-ownership-transfer trigger at POST /actors/{actor_id}/trigger/offer-case-ownership-transfer that accepts a case_id and transferee_id in its request body and emits the requesting Case Owner's proposal, Offer(Offer(VulnerabilityCase)), to the case's CASE_MANAGER, which offers the case to the specified transferee (CM-21-005). The response's activity is the proposal.Without this trigger, demo scripts must inject ownership-transfer offers directly into the transferee's inbox, which bypasses the offering actor's BT execution and violates DEMOMA-05-001. A trigger endpoint ensures the full hexagonal stack is exercised for both sides of the handshake. |
Refines: TRIG-01-001 Refines: TRIG-02-004 |
TRIG-11-002 |
MUST | The system MUST expose an accept-case-ownership-transfer trigger at POST /actors/{actor_id}/trigger/accept-case-ownership-transfer that accepts an offer_id in its request body and emits an Accept(Offer(VulnerabilityCase)) (ownership transfer variant) from the requesting actor, addressed to the CASE_MANAGER (CM-21-006).Symmetry with TRIG-11-001: the accepting actor must also be puppeteered through a trigger so that its BT execution is exercised, consistent with DEMOMA-05-001. |
Refines: TRIG-01-001 Refines: TRIG-02-004 Refines: CM-21-006 |
TRIG-11-003 |
SHOULD | The offer-case-ownership-transfer trigger request body SHOULD accept an optional content field (free-text message) to include in the offer activity, consistent with the factory function signature for offer_case_ownership_transfer_activity. |
Refines: TRIG-11-001 |
TRIG-11-005 |
MUST | The system MUST expose a reject-case-ownership-transfer trigger at POST /actors/{actor_id}/trigger/reject-case-ownership-transfer that accepts an offer_id in its request body and emits a Reject(Offer(VulnerabilityCase)) (ownership transfer variant) from the requesting actor, addressed to the CASE_MANAGER (CM-21-010).Symmetry with TRIG-11-002. Without it the only way to decline a transfer is to build the Reject by hand and post it into the CASE_MANAGER's inbox, which is what the transfer-ownership exchange demo does and DEMOMA-05-001 forbids. |
Refines: TRIG-01-001 Refines: TRIG-11-002 Refines: CM-21-010 |
TRIG-11-006 |
MUST | The system MUST expose a withdraw-case-ownership-transfer trigger at POST /actors/{actor_id}/trigger/withdraw-case-ownership-transfer that accepts an offer_id in its request body, naming the requesting Case Owner's own proposal (the activity TRIG-11-001 returned), and emits Undo(Offer(Offer(VulnerabilityCase))) of that proposal, addressed to the CASE_MANAGER (CM-21-015).Symmetry with TRIG-11-002 and TRIG-11-005: the offerer withdraws through a trigger, so the withdrawal runs the same behavior tree a real client would (DEMOMA-05-001). |
Refines: TRIG-01-001 Refines: CM-21-015 |
TRIG-11-007 |
MUST | The offer-case-ownership-transfer trigger (TRIG-11-001) MUST accept an optional end_time in its request body and, when given, set it as the end_time of the proposal's inner Offer, which the CASE_MANAGER carries to the Offer it makes as the deadline to answer (CM-21-016). Without it the Offer carries no end_time.The owner is the party that knows how long it can wait for an answer, so the deadline is set where the transfer is proposed. |
Refines: TRIG-11-001 Refines: CM-21-016 |
Vocabulary Model
Requirements governing how ActivityStreams 2.0 vocabulary types are defined, registered, configured, and deserialized in the Vultron wire layer. The vocabulary model is the foundation of the inbound pipeline: every activity must be deserialized into a known vocabulary type before semantic extraction can assign it a domain meaning.
File ID: VM
Static Object Integrity
| ID | Priority | Requirement | Related |
|---|---|---|---|
VM-08-001 |
SHOULD | Objects intended to be static once created (e.g., vocabulary registry entries, canonical example objects) SHOULD use immutable (frozen) configuration so that any attempt to modify them at runtime raises an exception | |
VM-08-003 |
MUST | The copy of an activity recorded in the case ledger (the entry's payloadSnapshot) MUST be identical to the activity delivered to participants.Ledger integrity requires that what is recorded and what is delivered are identical. A participant that holds both the delivered activity and the ledger entry can check this through the hash chain, so a ledger that records one form while recipients receive another is visible to them. |
Refines: VM-08-001 |
Unknown Message Handling
| ID | Priority | Requirement | Related |
|---|---|---|---|
VM-09-001 |
MAY | Messages that cannot be parsed or whose semantics are unknown MAY still be forwarded to the case event log to create an entry for human or agent review |
Vocabulary Namespace and JSON-LD Context
| ID | Priority | Requirement | Related |
|---|---|---|---|
VM-10-001 |
MUST | Outbound Vultron wire messages MUST declare the Vultron JSON-LD context by setting @context to "https://certcc.github.io/Vultron/ns/context.jsonld". Using only the ActivityStreams namespace (https://www.w3.org/ns/activitystreams) is not sufficient: Vultron-specific types (VulnerabilityCase, EmbargoEvent, etc.) are not defined in that namespace and cannot be resolved by receivers without the Vultron context declaration.Derived from ADR-0069 (docs/adr/0069-vultron-namespace-uri.md). The Vultron vocabulary extends ActivityStreams with CVD-specific types; those types must be declared in a dereferenceable JSON-LD context so that external implementations can resolve them. The context document at the Vultron namespace URI imports the AS2 namespace, so implementations need only cite the Vultron context URI. |
Refines: VM-02-002 |
VM-10-002 |
MUST | Wire-layer classes whose _vocab_ns is VocabNamespace.VULTRON MUST default context_ to the Vultron context URI (https://certcc.github.io/Vultron/ns/context.jsonld) rather than the ActivityStreams namespace URI.as_VultronObject is the base class for all Vultron-specific wire types. Overriding context_ there ensures that every Vultron object serializes with the correct @context without requiring call-site changes. |
Implements: VM-10-001 |
Vultron Semantic–AS2 Mapping
Defines the authoritative mapping from each Vultron semantic message type (MessageSemantics enum value) to its ActivityStreams 2.0 wire representation. This is the single source of truth for how Vultron domain semantics are encoded as AS2 activity structures, isolating transport and wire-encoding concerns from the core domain logic and case-state machines.
File ID: VAM
General Mapping Conventions
| ID | Priority | Requirement | Related |
|---|---|---|---|
VAM-01-008 |
MUST | Inbound activities represent state-change notifications, not commands; the AS2 activity type used for each semantic MUST reflect the completed state transition, not a requested action | |
VAM-01-011 |
MUST | A CaseStatus MUST be brought into a case only by the Add(CaseStatus) that attaches it (VAM-08-002), a ParticipantStatus only by the Add(ParticipantStatus) that attaches it (VAM-08-004), a proposed EmbargoEvent only by the Invite that proposes it (VAM-05-005), and a Note only by the Add(Note) that attaches it (VAM-07-002); each carries its object inline or by reference. A CaseParticipant MUST come into existence only as the CASE_MANAGER's consequence of an act it records. A received Create whose object is a CaseStatus, a ParticipantStatus, an EmbargoEvent, a CaseParticipant or a Note is not a protocol message, and its receiver MUST NOT change the case file because of it.These records mean nothing apart from their case, so a separate Create would add a "created but not attached" state needing its own ledger entry and replica step (CLP-07-013, RSH-08-004) for no gain (ADR-0137). A Create(Note), with or without a case context, is a deferred form recorded as an open question (MS-18-001). These records get one attaching message each. The Create a CASE_MANAGER ledger entry carries as its payload snapshot records a change; it is not a received message. |
Constrains: VAM-08-002 — the Add(CaseStatus) is the only way a case status enters a case Constrains: VAM-08-004 — the Add(ParticipantStatus) is the only way a participant status enters a case Constrains: VAM-05-005 — the Invite is the only way a proposed embargo event enters a case Constrains: VAM-07-002 — the Add(Note) is the only way a note enters a case |
Report Management Messages
Case Management Messages
| ID | Priority | Requirement | Related |
|---|---|---|---|
VAM-03-001 |
MUST | CREATE_CASE MUST be represented as Create(VulnerabilityCase) |
|
VAM-03-003 |
MUST | ENGAGE_CASE MUST be represented as Join(VulnerabilityCase) |
|
VAM-03-004 |
MUST | DEFER_CASE MUST be represented as Ignore(VulnerabilityCase) |
|
VAM-03-005 |
MUST | ADD_REPORT_TO_CASE MUST be represented as Add(VulnerabilityReport)[target=VulnerabilityCase] |
|
VAM-03-006 |
MUST | CLOSE_CASE MUST be represented as Leave(VulnerabilityCase) |
|
VAM-03-007 |
MUST | UPDATE_CASE_OWNER MUST be represented as Update(VulnerabilityCase), carrying the case with its new owner in attributedTo. Only the CASE_MANAGER sends it, as its own act on an accepted ownership transfer (CM-21-002), and no received use case handles it (SE-05-002, ADR-0140).An entry's semantic type is the registry's label for its payload (ADR-0140), so the CASE_MANAGER's own case-field change needs a registry entry of its own. |
Satisfies: CM-21-002 |
Actor Suggestion and Invitation Messages
| ID | Priority | Requirement | Related |
|---|---|---|---|
VAM-04-001 |
MUST | OFFER_ACTOR_TO_CASE MUST be represented as Offer(Actor)[target=VulnerabilityCase]This is the first hop of the two-offer actor-suggestion exchange: a participant suggests an Actor to the CASE_MANAGER. Corrected from an earlier revision that named SUGGEST_ACTOR_TO_CASE, which is not and never was a MessageSemantics member. Note the crossed spelling in the code — SuggestActorToCasePattern is the pattern bound to OFFER_ACTOR_TO_CASE, while OfferActorToCasePattern is bound to OFFER_CASE_PARTICIPANT (VAM-04-010). |
|
VAM-04-002 |
MUST | ACCEPT_OFFER_CASE_PARTICIPANT MUST be represented as Accept(Offer(CaseParticipant)[target=VulnerabilityCase])The accept wraps the second offer of the exchange, not the first: the Case Owner accepts the CASE_MANAGER's Offer(CaseParticipant) (VAM-04-010), not the original Offer(Actor). Corrected from an earlier revision that named a nonexistent ACCEPT_SUGGEST_ACTOR_TO_CASE and wrapped Offer(Actor) — wrong in both the semantic name and the wire form. See CM-16-003, CM-16-004 and ADR-0026. |
|
VAM-04-003 |
MUST | REJECT_OFFER_CASE_PARTICIPANT MUST be represented as Reject(Offer(CaseParticipant)[target=VulnerabilityCase])Symmetric with VAM-04-002: the rejection wraps the CASE_MANAGER's Offer(CaseParticipant), not the originating Offer(Actor). Corrected from an earlier revision that named a nonexistent REJECT_SUGGEST_ACTOR_TO_CASE and wrapped the wrong inner object. |
|
VAM-04-004 |
MUST | INVITE_ACTOR_TO_CASE (the stub Invite) MUST be represented as Invite(Actor)[target=VulnerabilityCaseStub], with the stub shaped as CM-11-013 requires. |
|
VAM-04-005 |
MUST | ACCEPT_INVITE_ACTOR_TO_CASE MUST be represented as Accept(Invite(Actor)[target=VulnerabilityCaseStub]) |
|
VAM-04-006 |
MUST | REJECT_INVITE_ACTOR_TO_CASE MUST be represented as Reject(Invite(Actor)[target=VulnerabilityCaseStub]) |
|
VAM-04-007 |
MUST | OFFER_CASE_OWNERSHIP_TRANSFER MUST be represented as Offer(VulnerabilityCase): the one Offer of a transfer, which the CASE_MANAGER makes from the Case Owner's proposal (CM-21-005). |
|
VAM-04-008 |
MUST | ACCEPT_CASE_OWNERSHIP_TRANSFER MUST be represented as Accept(Offer(VulnerabilityCase)) |
|
VAM-04-009 |
MUST | REJECT_CASE_OWNERSHIP_TRANSFER MUST be represented as Reject(Offer(VulnerabilityCase)) |
|
VAM-04-010 |
MUST | OFFER_CASE_PARTICIPANT MUST be represented as Offer(CaseParticipant)[target=VulnerabilityCase]The second hop of the actor-suggestion exchange, and the one VAM previously omitted entirely: having received Offer(Actor) (VAM-04-001), the CASE_MANAGER offers case participation to the Case Owner as a CaseParticipant carrying the proposed roles. This hop is what VAM-04-002 and VAM-04-003 accept and reject, so leaving it unspecified made those two entries look like they wrapped Offer(Actor). See CM-16-003, CM-16-004 and ADR-0026. |
|
VAM-04-011 |
MUST | The full-case Invite (CM-11-010) MUST be represented as Invite(Actor)[target=VulnerabilityCase], with the case referenced by its plain URI in target (AKM-02-003) and the CASE_MANAGER's ledger position carried in the Invite's content. The content string MUST be the JSON dump, under the model's wire aliases, of the core LedgerPosition model, for example {"logIndex":3,"entryHash":"<hash>"}, and MUST be produced only by vultron.wire.as2.factories. The position of an empty ledger is logIndex -1 with the case's genesisHash as entryHash (CLP-08-004). A receiver MUST parse content with LedgerPosition.model_validate_json at the parse edge and MUST refuse an activity whose content is missing, blank or does not parse, naming the activity and writing nothing (ADR-0032).The participant already holds the case from the replica, so the Invite references it by ID. Because the stub is its own type (VAM-04-004, CM-11-013), this shape and the stub Invite differ in their target's type and are classified without a lookup. content is the existing AS2 slot, used here for a machine-readable record, so no AS2 verb gains a field. |
|
VAM-04-012 |
MUST | The RV reply to the full-case Invite MUST be represented as Accept(Invite(Actor)[target=VulnerabilityCase]), carrying the replier's ledger position in content, in the form VAM-04-011 defines (CM-11-011). |
|
VAM-04-013 |
MUST | The RI reply to the full-case Invite MUST be represented as TentativeReject(Invite(Actor)[target=VulnerabilityCase]), carrying the replier's ledger position in content, in the form VAM-04-011 defines (CM-11-011). |
|
VAM-04-014 |
MUST | The RC reply to the full-case Invite MUST be represented as Reject(Invite(Actor)[target=VulnerabilityCase]), carrying the replier's ledger position in content, in the form VAM-04-011 defines (CM-11-011). |
|
VAM-04-015 |
MUST | UNDO_CASE_OWNERSHIP_TRANSFER MUST be represented as Undo(Offer(Offer(VulnerabilityCase))): the Case Owner's withdrawal of its own proposal, and with it of the open Offer the CASE_MANAGER made from it (CM-21-015). |
Refines: CM-21-015 |
VAM-04-016 |
MUST | ACCEPT_ACTOR_RECOMMENDATION MUST be represented as Accept(Offer(Actor)[target=VulnerabilityCase]): the CASE_MANAGER's answer to the recommender, wrapping the recommender's own Offer(Actor) (VAM-04-001), with inReplyTo naming that Offer (CM-16-006, CM-16-009).This is the third hop of the exchange, addressed back to the recommender. It wraps the first offer, where the Case Owner's decision (VAM-04-002) wraps the second, so the inner object type tells the two Accepts apart. |
Refines: CM-16-006 |
VAM-04-017 |
MUST | REJECT_ACTOR_RECOMMENDATION MUST be represented as Reject(Offer(Actor)[target=VulnerabilityCase]): the CASE_MANAGER's answer to the recommender after the Case Owner declines, with inReplyTo naming the recommender's Offer(Actor) (CM-16-007).Symmetric with VAM-04-016; the Case Owner's Reject (VAM-04-003) wraps Offer(CaseParticipant) instead. |
Refines: CM-16-007 |
VAM-04-018 |
MUST | PROPOSE_CASE_OWNERSHIP_TRANSFER MUST be represented as Offer(Offer(VulnerabilityCase)) with the case as the proposal's context: the Case Owner's proposal to the CASE_MANAGER, whose inner Offer is the template of the Offer the CASE_MANAGER makes (CM-21-005).The proposal and the CASE_MANAGER's Offer(VulnerabilityCase) (VAM-04-007) are told apart by shape, an Offer of an Offer against an Offer of a case, so neither pattern needs a sender check and no new vocabulary type is needed (ADR-0148). |
Refines: CM-21-005 |
Embargo Management Messages
| ID | Priority | Requirement | Related |
|---|---|---|---|
VAM-05-003 |
MUST | REMOVE_EMBARGO_EVENT_FROM_CASE MUST be represented as Remove(Event) |
|
VAM-05-004 |
MUST | ANNOUNCE_EMBARGO_EVENT_TO_CASE MUST be represented as Announce(Event)[context=VulnerabilityCase] |
|
VAM-05-005 |
MUST | INVITE_TO_EMBARGO_ON_CASE MUST be represented as Invite(Event)[context=VulnerabilityCase] |
|
VAM-05-006 |
MUST | ACCEPT_INVITE_TO_EMBARGO_ON_CASE MUST be represented as Accept(Invite(Event)[context=VulnerabilityCase]) |
|
VAM-05-007 |
MUST | REJECT_INVITE_TO_EMBARGO_ON_CASE MUST be represented as Reject(Invite(Event)[context=VulnerabilityCase]) |
|
VAM-05-008 |
MUST | ACTIVATE_EMBARGO_ON_CASE MUST be represented as Accept(Event)[target=VulnerabilityCase], whose object is the proposed EmbargoEvent itself, carried inline or by reference, and not the Invite that proposed it (ADR-0122).The case owner's activation is a decision about the embargo for the case, so it accepts the embargo; an Accept of the Invite is each sender's own consent (VAM-05-006). Giving the two different objects keeps them apart on the wire. |
|
VAM-05-009 |
MUST | REJECT_EMBARGO_PROPOSAL_ON_CASE MUST be represented as Reject(Event)[target=VulnerabilityCase], whose object is the proposed EmbargoEvent itself, carried inline or by reference, and not the Invite that proposed it (ADR-0122).The case owner's rejection is a decision about the embargo for the case; a Reject of the Invite is each sender's own consent (VAM-05-007). |
|
VAM-05-011 |
MUST | WITHDRAW_FROM_EMBARGO_ON_CASE MUST be represented as Leave(Event)[context=VulnerabilityCase], whose object is the embargo in force, carried inline or by reference, and not an Invite (ADR-0122).Withdrawal answers no Invite, so it names the embargo itself; it is told apart from Leave(VulnerabilityCase), which leaves the case, by its object (MSM-07-010). |
|
VAM-05-013 |
MUST | UNDO_INVITE_TO_EMBARGO_ON_CASE MUST be represented as Undo(Invite(Event)[context=VulnerabilityCase]), whose object is an Invite(EmbargoEvent) the sender itself sent. Only the CASE_MANAGER sends it, when it abandons an open proposal (EMB-16-001, ADR-0152).An actor undoes only an activity it performed, so withdrawing an invitation one sent is an Undo and declining one received is a Reject (ADR-0152). The shape is told apart from the ownership-transfer Undo (VAM-04-015) by its object, an embargo Invite. |
Satisfies: EMB-16-001 |
VAM-05-014 |
MUST | CREATE_EMBARGO_CONSENT MUST be represented as Create(EmbargoConsent), carrying the new consent row in full. Only the CASE_MANAGER sends it, as its own act when it brings a consent row into existence (ADR-0122, CLP-07-013), and no received use case handles it (SE-05-002, ADR-0140).An entry's semantic type is the registry's label for its payload (ADR-0140). The record is a case-file change, not a PEC transition (MSM-07-001). |
Refines: MSM-07-001 |
VAM-05-015 |
MUST | UPDATE_EMBARGO_CONSENT MUST be represented as Update(EmbargoConsent), carrying the changed consent row in full. Only the CASE_MANAGER sends it, as its own act when it changes a consent row, its own consent included (MSM-07-004, CLP-07-013), and no received use case handles it (SE-05-002, ADR-0140).An entry's semantic type is the registry's label for its payload (ADR-0140). An expired invitation is this Update carrying the invitee's TIMED_OUT row (ADR-0140 item 4). |
Refines: MSM-07-004 |
VAM-05-016 |
MUST | CREATE_EMBARGO_EVENT MUST be represented as Create(EmbargoEvent), carrying the embargo in full, its start and end times included. Only the CASE_MANAGER commits it, as a ledger entry and never as a sent message, once for each embargo a case's creation writes, such as the default embargo (EP-04-002), right after the case's genesis entry and before the case's other creation entries (CLP-07-013). No received use case handles it (SE-05-002, ADR-0140): a replica stores the embargo from the entry.The genesis entry carries only the case's active embargo inline, and a replica copies each record from its own entry (ADR-0124); no other entry carries a creation-time embargo until it is removed, so a replica built from the ledger alone could not read the end time its embargo decisions need (#5122). An entry's semantic type is the registry's label for its payload (ADR-0140). |
Refines: CLP-07-013 |
Case Participant Management Messages
| ID | Priority | Requirement | Related |
|---|---|---|---|
VAM-06-002 |
MUST | ADD_CASE_PARTICIPANT_TO_CASE MUST be represented as Add(CaseParticipant)[target=VulnerabilityCase]The message is the Case Owner's request to reinstate a removed participant into the case's activeParticipants (CM-31-011, ADR-0116); it does not seat a new member, which joins by accepting a stub Invite. |
|
VAM-06-003 |
MUST | REMOVE_CASE_PARTICIPANT_FROM_CASE MUST be represented as Remove(CaseParticipant)[target=VulnerabilityCase]The message removes a participant from the case's activeParticipants, not from caseParticipants: the record stays and the participant stops receiving case content (CM-31-001, CM-31-004, ADR-0116). |
|
VAM-06-004 |
MUST | CREATE_CASE_PARTICIPANT MUST be represented as Create(CaseParticipant), carrying the record in full. Only the CASE_MANAGER sends it, as its own act when it brings a participant record into existence (ADR-0114, CLP-07-013), and no received use case handles it (SE-05-002, ADR-0140).An entry's semantic type is the registry's label for its payload (ADR-0140). A received Create(CaseParticipant) is still not a protocol message (VAM-01-011): it is recognized only so that it is reported, not acted on. |
Refines: VAM-01-011 |
VAM-06-005 |
MUST | UPDATE_CASE_PARTICIPANT MUST be represented as Update(CaseParticipant), carrying the changed record in full. Only the CASE_MANAGER sends it, as its own act when it changes a participant record, such as moving the CASE_OWNER role (CM-21-002, CLP-07-013), and no received use case handles it (SE-05-002, ADR-0140).An entry's semantic type is the registry's label for its payload (ADR-0140), so each participant-record change the CASE_MANAGER commits needs a registry entry. |
Satisfies: CM-21-002 |
Note Management Messages
Status Tracking Messages
Vultron Protocol
Requirements extracted from the Vultron Protocol documentation, covering participant state tracking, Report Management (RM), Embargo Management (EM), CVD Case State (CS) messaging, model interactions, and implementation guidance. This is the normative source for protocol behavior requirements that implementations must satisfy.
File ID: VP
Participant State Tracking
Report Management (RM): Receiving Reports
RM Messaging
Embargo Management (EM) Process
| ID | Priority | Requirement | Related |
|---|---|---|---|
VP-04-001 |
MUST | Every accepted embargo MUST specify an end time (endTime); an embargo without one MUST NOT be accepted. |
Refines: VP-05-001 |
VP-04-002 |
MUST | A CVD case SHALL NOT have more than one active embargo at a time | |
VP-04-003 |
MAY | An embargo MAY be proposed | |
VP-04-004 |
MAY | Once proposed, an embargo MAY be accepted or rejected | |
VP-04-005 |
MAY | Once accepted, revisions MAY be proposed, which MAY in turn be accepted or rejected |
Embargo Principles
Embargo Negotiation
| ID | Priority | Requirement | Related |
|---|---|---|---|
VP-06-001 |
MUST NOT | CVD Participants MUST NOT propose or accept a new embargo negotiation when information about the vulnerability is already known to the public (\(q^{cs} \in \cdots P \cdots\)), an exploit is publicly available (\(q^{cs} \in \cdots X \cdot\)), or the vulnerability is being actively exploited (\(q^{cs} \in \cdots A\)) | |
VP-06-002 |
SHOULD NOT | CVD Participants SHOULD NOT propose or accept a new embargo when the fix for a vulnerability has already been deployed (\(q^{cs} \in VFDpxa\)) | |
VP-06-003 |
SHOULD | Receivers SHOULD accept any embargo proposed by Reporters | |
VP-06-004 |
SHOULD | Participants SHOULD explicitly accept or reject embargo proposals within the period defined in their VDP or within 14 calendar days of receipt, whichever is shorter. | |
VP-06-004b |
SHOULD NOT | Embargo agreement or rejection SHOULD NOT be tacit. | Refines: VP-06-004 |
VP-06-005 |
SHOULD | Participants SHOULD make reasonable attempts to retry embargo negotiations when prior proposals have been rejected or otherwise failed to achieve acceptance | |
VP-06-006 |
SHOULD | Participants SHOULD remain flexible in adjusting embargo terms as the case evolves | |
VP-06-007 |
MUST | Submitting a report when an embargo proposal is pending (\(q^{em} \in P\)) SHALL be construed as the Sender's acceptance of the proposed terms | |
VP-06-008 |
MAY | CVD Participants MAY propose or accept a new embargo when \(q^{cs} \in VFdpxa\). | |
VP-06-008b |
SHOULD | Any embargo accepted when \(q^{cs} \in VFdpxa\) SHOULD be brief and used only to allow Participants to prepare for timely publication or deployment. | Refines: VP-06-008 |
VP-06-009 |
MAY | CVD Participants MAY propose or accept an embargo in all other case states (\(q^{cs} \in \cdots pxa\)) | |
VP-06-010 |
MAY | Participants MAY accept or reject any proposed embargo as they see fit | |
VP-06-011 |
MAY | Receivers MAY propose embargo terms they find more favorable | |
VP-06-012 |
MAY | Participants MAY withdraw their own unaccepted Proposed embargo | |
VP-06-013 |
MAY | Participants MAY interpret another Participant's failure to respond to an embargo proposal in a timely manner as a rejection of that proposal | |
VP-06-014 |
MAY | In the absence of an explicit accept or reject response from a Receiver in a timely manner, the Sender MAY proceed consistent with an EM state of None (\(q^{em} \in N\)) | |
VP-06-015 |
MAY | In a case where the embargo state is None and for which an embargo has been proposed and either explicitly or tacitly rejected, Participants MAY take any action they choose with the report, including immediate publication | |
VP-06-016 |
MAY | Participants MAY withhold a report from a Recipient until an initial embargo has been accepted | |
VP-06-017 |
MAY | Participants MAY use short embargo periods to cover their report validation process and subsequently revise the embargo terms pending the outcome of their validation or prioritization processes |
Embargo Default Policies
| ID | Priority | Requirement | Related |
|---|---|---|---|
VP-07-001 |
MUST | If neither Sender nor Receiver proposes an embargo and no actor default applies, the protocol default embargo of EP-04-005 SHALL apply. No embargo SHALL exist only where the case is not embargo-eligible, that is where any of P/X/A is set (EP-04-008). | Refines: EP-04-005 Refines: EP-04-008 |
VP-07-002 |
MUST | A Receiver's default embargo specified in its vulnerability disclosure policy SHALL be treated as an initial embargo proposal | |
VP-07-003 |
MUST | If the Receiver has declared a default embargo in its vulnerability disclosure policy and the Sender proposes nothing to the contrary, the Receiver's default embargo SHALL be considered as an accepted proposal | |
VP-07-004 |
MUST | If the Sender proposes an embargo longer than the Receiver's default, the Receiver's default SHALL be taken as accepted and the Sender's proposal taken as a proposed revision | |
VP-07-005 |
MUST | If the Sender proposes an embargo shorter than the Receiver's default, the Sender's proposal SHALL be taken as accepted and the Receiver's default taken as a proposed revision | |
VP-07-006 |
SHOULD | Report Recipients SHOULD post a default embargo period as part of their Vulnerability Disclosure Policy to set expectations with potential Reporters | |
VP-07-007 |
SHOULD | If the Sender proposes an embargo and the Receiver has no default embargo specified by policy, the Receiver SHOULD accept the Sender's proposal | |
VP-07-008 |
SHOULD | When two or more embargo proposals are open and no embargo has yet been accepted (\(q^{em} \in P\)), Participants SHOULD accept the shortest one and propose the remainder as revisions | |
VP-07-009 |
SHOULD | When two or more embargo revisions are open and an embargo is active (\(q^{em} \in R\)), Participants SHOULD accept or reject them individually in earliest-to-latest expiration order | |
VP-07-010 |
MAY | Participants MAY include a default embargo period as part of a published Vulnerability Disclosure Policy | |
VP-07-011 |
MAY | When the Receiver has no default, the Receiver MAY propose a revision after accepting the Sender's proposal | |
VP-07-012 |
MAY | When the Sender proposes longer than the Receiver's default, the Receiver MAY accept or reject the proposed extension | |
VP-07-013 |
MAY | When the Sender proposes shorter than the Receiver's default, the Sender MAY accept or reject the Receiver's proposed revision |
Multi-Party Coordination
Embargo Early Termination
Embargo Case Splits and Merges
EM Messaging
CVD Case State (CS) Messaging
| ID | Priority | Requirement | Related |
|---|---|---|---|
VP-12-001 |
SHOULD | Vendor Awareness (\(CV\)) messages SHOULD be sent only by Participants with direct knowledge of the notification (i.e., either the Participant who sent the report to the Vendor or the Vendor upon receipt of the report) | |
VP-12-002 |
SHOULD | Once attacks are observed (\(q^{cs} \in \cdots A\)), fix development SHOULD accelerate. | |
VP-12-002b |
SHOULD | Once attacks are observed (\(q^{cs} \in \cdots A\)), the embargo teardown process SHOULD begin immediately. | Refines: VP-12-002 |
VP-12-002c |
SHOULD | Once attacks are observed (\(q^{cs} \in \cdots A\)), publication and deployment SHOULD follow as soon as is practical. | Refines: VP-12-002 |
RM-EM Interactions
RM-EM-CS Interactions
Implementation Guidance
| ID | Priority | Requirement | Related |
|---|---|---|---|
VP-15-004 |
MAY | Vultron Protocol implementations MAY use end-to-end encryption to protect sensitive data in transit |
Embargo Representation Privacy
CASE_MANAGER Write Authority
| ID | Priority | Requirement | Related |
|---|---|---|---|
VP-17-001 |
MUST | All changes to the canonical case state MUST go through the CASE_MANAGER; no participant MAY change canonical case state directly. This single-writer axiom prevents concurrent-write races and ensures that all shared state changes flow through the actor's consistency checks and persistence layer. The CS participant-specific axes (RM, VFD) are owned by each participant's CaseParticipant record and are intentionally excluded from this restriction. |
Refines: VP-02-001 |
VP-17-002 |
MUST NOT | Participant-local case replicas MUST NOT be updated by direct mutation from inbound protocol messages; replicas MUST be updated exclusively via ledger replay from Announce(CaseLedgerEntry) fan-out.Ledger replay is the intended propagation mechanism for keeping participant replicas consistent with canonical case state. Direct mutation from inbound messages (e.g., Add(Note, Case)) would bypass the ledger ordering guarantees and create divergence. This path is distinct from VP-17-001 and is not a bypass of the CASE_MANAGER write constraint. |
Refines: VP-17-001 Constrains: SM-10-002 |
Routing Topology
| ID | Priority | Requirement | Related |
|---|---|---|---|
VP-18-001 |
MUST | Outbound messages from a participant MUST follow the routing path: participant → CASE_MANAGER → CaseLedgerEntry → broadcast → participants.The ledger record provides the audit trail and ordering guarantees for all case activity. Direct peer-to-peer delivery bypasses the ledger and the broadcast idempotency controls. |
Refines: PCR-08-001 — participant-case-relationships.yaml — PCR-08 covers ledger-based broadcast |
VP-18-002 |
MUST NOT | A participant MUST NOT deliver messages directly to another participant's inbox. Direct peer-to-peer delivery bypasses the case ledger, removing the audit trail and broadcast idempotency guarantees. |
Refines: VP-18-001 |
VP-18-003 |
MUST | Delivery of outbound participant messages MUST be mediated by the CASE_MANAGER and recorded in the case ledger before being fanned out to participants. Mediation through the CASE_MANAGER ensures all case activity is ledger-ordered and consistently broadcast to all participants. |
Refines: VP-18-001 |