Case Ledger and Replication
This page is normative
This page is considered a core part of the Vultron Protocol. This is a normative section of the documentation.
The whole specification on one page
The specification is also published as a single page that holds every section and annex in order, for printing or for searching the whole text at once.
This page holds section 14.5 of the Vultron Protocol Specification, part of its Requirements Catalog. Each row is one requirement, and the rest of the documentation links a requirement to its row here.
14.5 Case Ledger and Replication
The requirements on the case ledger and its copies: how the CASE_MANAGER records case events, how the ledger is synchronized, how a participant keeps its case replica, and the trust a new replica starts from.
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.
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 CaseLedgerEntryThe ledger is the case history that every replica copies. An admitted assertion with no entry changes the case at the CASE_MANAGER only, and no replica can learn of it. One entry for each admitted assertion keeps the replicas complete. |
|
CLP-02-002 |
MUST | CaseLedgerEntry MUST be a canonical content object distinct from any transport envelope used to replicate itThe transport envelope changes from one delivery to the next. It has a different id, recipient and time each time. The hash chain must cover content that is the same for every holder. If the entry and its envelope are one object, a redelivery changes the content that the hash covers. |
|
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).A replica rebuilds the case only from what the entry carries. If the entry holds a reference or a summary in place of the activity, a replica must find the rest somewhere else, and two replicas can find different things. The full activity lets every replica replay the same change. |
|
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).A refusal never becomes an entry (CLP-04-007), so a disposition field could only ever hold one value. A field like this invites code to write refused entries and readers to filter them. Without it, every entry is a change that every replica must apply. |
|
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.The refused participant must learn that it was refused and why, so it can stop waiting for its entry. A refusal changes nothing in the case, and a ledgered refusal would put a non-event into every replica's hash chain. A sender that is not entitled to make the assertion is owed no answer (HP-01-006, ADR-0141). A closed case's CASE_MANAGER acts like an exited process, and a Reject of a late activity could read as a protocol decision (CM-23-013, ADR-0131). |
|
CLP-02-006 |
MUST | CaseLedgerEntry MUST include a log_index field corresponding to SYNC-01-002.A replica uses the index to find its position in the ledger and to see a missing entry. Without an index on each entry, a replica cannot tell a gap from the end of the ledger. |
|
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.In Raft, each entry carries the term of the leader that wrote it, and nodes compare terms to find entries from an old leader. A reserved field lets a cluster add that value without a change to the entry format that single-node deployments also read. |
|
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 cannot name both the ledger verdict and an AS2 activity. If it does, a reader takes "a refusal is not ledgered" to mean "a Reject is not ledgered". A participant's decline closes a request and reply pair, and the ledger must record that (CLP-11-001). If the decline has no entry, every replica keeps the request open. |
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.A closed list makes each activity with no entry a recorded decision, not the choice of one handler. A coverage test then has a fixed set of grounds for an 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. An entry for it would record a fact that every holder of the ledger can already derive. |
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.Each entry is one fact that every replica applies once. An entry that records no admitted assertion, or that records more than one, makes replicas apply something the CASE_MANAGER never decided, or lose part of what it decided. A change the CASE_MANAGER writes itself is also a fact, so it needs an entry like any other. |
|
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.An empty payload snapshot 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).A replica applies each entry. Two entries for one change make it apply the change twice, and the ledger then shows two facts where there is one. An act and the transitions it causes are different facts, so each one gets its own entry. |
|
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=VulnerabilityCase] 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 participant record it creates for the invitee. If only the act is recorded, each replica must create the record itself, with its own ids and clock times. Full records are ledger content that the hash chain must cover (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 "same ledger position, same case file" covers only the case file. If bookkeeping is stored on the case or read to compute a case-file value, replicas at the same position can disagree on a value that no entry carried. |
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 one entry for each change it causes (CLP-07-013). If they commit one at a time, a failure part-way keeps the act and loses some of its consequences, a state no single act produces. Nothing commits the missing parts later: a repeated trigger starts a new act, and an idempotency guard stops a redelivered one. So an act succeeds or fails as a whole (ADR-0146). A received act's own entry is in the same transaction (ADR-0151), so a failed received act keeps nothing and a later delivery runs it again. |
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.Replicas apply each entry as the act of the actor that the snapshot names. If the CASE_MANAGER puts its own identity there, the act is attributed to the wrong participant, and its effects land on the wrong record. Authority is a role, not an identity (ADR-0088), so a CASE_MANAGER that also holds a participant role can assert as that participant. |
|
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. The ledger replicates to every participant and the hash chain covers it. Diagnostics and checkpoints depend on one actor's runtime. If they enter the ledger, replicas must filter them, and the chain changes when the timing of a run changes. An actor's own log can hold any detail with no effect on the protocol. |
|
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). The CASE_MANAGER is the only writer of the ledger, and it writes only what it receives or sends. An act that sends nothing to the CASE_MANAGER has no entry, and no replica learns of it. When the CASE_MANAGER originates an act, it commits the entry in the tree that sends the activity. A copy to itself would come back through its inbox and could be committed a second time. Only the container that hosts the CASE_MANAGER may act as it (ADR-0109). |
|
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.The Announce(CaseLedgerEntry) envelope only delivers entries. An entry whose payload is the envelope would itself need an envelope to deliver it. Each delivery would then add one more entry with no end, and the hash chain would change with every delivery. |
|
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 subject of a message are the same in the usual single-recipient case. They differ when a message is relayed on a participant's behalf, sent from the command line, or replayed from the log. If the receiver uses its own identity as the subject, it records consent for the wrong participant, or sends an action to the wrong one. A conforming peer then sees a change on an actor that the message never named. |
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.The order of the to: list carries no meaning. A pick by position resolves a message with several recipients to the same subject in every replica. Each recipient after the first would then apply the message to the record of another participant. |
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 the ordering checks impossible, and it breaks any consumer that orders entries by time. An entry carries two timestamps. A rule for the envelope stamp that is applied to the claimed stamp, or the reverse, gives a check that never fires or that refuses valid entries, so the rule names its field. |
|
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 that goes backward contradicts the causal order. If entry N has a later log index than entry N-1 but an earlier stamp, observers cannot use the stamps to support the order that the CASE_MANAGER recorded. The rule covers only the envelope stamp, because one writer stamps every entry from one clock. On the claimed stamps it would compare the clocks of two participants, which ADR-0079 rejects as option C, and it would refuse a valid assertion when two clocks disagree. |
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 find a missing entry by index arithmetic. An entry whose log index is greater than tail_index + 1 shows a forward gap (SYNC-14-001), and a replica that lags is valid only while its local prefix has no gaps. Both tests work only if the CASE_MANAGER never leaves a gap. CLP-14-001 requires only that indices increase, and CLP-14-005 requires only that they are unique. Neither excludes a sparse ledger. |
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. A receiver that fills in the field cannot later tell a time the sender claimed from one it made. Each check that reads the claimed time then compares the receiver's clock with itself and never fires (CLP-14-007, CLP-14-008). A refusal at message validation keeps the difference. A receiver that creates its own outbound activity stamps its own clock, which 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 value that the receiver makes is later indistinguishable from the sender's claim. For example, each receiver would derive its own genesis hash for one case, and order received statuses by arrival. An absent time harms only a decision that needs it, so that decision refuses the message early and clearly. Every other consumer accepts the absence. |
Refines: CLP-15-006 |
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.
Append-Only Log
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-01-001 |
MUST | The canonical recorded case ledger MUST be append-only; log entries MUST be immutable once appended The ledger is the record of what happened in the case. If an entry can change or go away, the hash chain breaks, replicas disagree, and nobody can audit the case history. Append-only entries let every replica apply the same history and reach the same case state. |
Refines: SYNC-00-005 — Restates the immutability of SYNC-00-005 for the canonical case ledger |
SYNC-01-002 |
MUST | Each log entry MUST carry a monotonically increasing, gapless index scoped to its case The index gives every entry one position in the case's causal order (ADR-0079). Because the index has no gaps, a receiver knows that an entry is missing when the next index it gets is too high (SYNC-14-001). A duplicate or skipped index would hide a lost entry or let two entries claim one position. |
|
SYNC-01-003 |
MUST | Each log entry MUST include a cryptographic hash of its own content and MUST reference the hash of its immediate predecessor, forming a forward-linked hash chain (Merkle chain style) The content hash lets a receiver check that an entry's content matches the hash the entry states. The link to the predecessor's hash fixes the entry's place in the chain. A receiver can then detect an entry whose content does not match its hash and an entry in the wrong place. |
|
SYNC-01-004 |
MUST | Canonical recorded log entries MUST be written only by the CASE_MANAGER. One writer gives one canonical order of entries. If two actors could write canonical entries, their ledgers could fork, and no rule could say which fork is the case history. |
|
SYNC-01-005 |
MUST | The hash-chain log entry format MUST be designed to be forward-compatible with a future Merkle Tree implementation A Merkle tree would let a peer prove that one entry is in the ledger without sending the whole chain. If the entry format blocks that change, a migration would have to rewrite every stored ledger, which an append-only ledger forbids. |
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 An entry that does not extend the receiver's tail is in the wrong place for this replica. If the receiver applies it, the replica holds a chain that the CASE_MANAGER never wrote. The rejection tells the CASE_MANAGER where the replica's chain ends, so it can send the missing entries. |
|
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 The rejection names the last entry that the replica holds. Sending from the entry after it fills the gap with the fewest messages. Sending from any other point either skips entries the replica needs or sends entries it already has. |
|
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 The outbox retries and replays send entries again, so a receiver often gets one entry more than once. A duplicate in the log would break the gapless index and the hash chain, and its effects would apply twice (ID-02-001). |
Implements: ID-02-001 — idempotency.md |
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 that states no ledger position, or the position of an empty chain, makes the CASE_MANAGER replay the whole ledger and send the case. Without this check, any sender could get embargoed case content just by asking. The replay cooldown limits how often a replay occurs, not who gets it. |
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 only that an entry claims the right place in the chain. It does not prove that the content matches the entry's hash. A refused entry gets no Reject(CaseLedgerEntry), because a replay would send the same entry again. The receiver's tail does not move, so the next entry does not extend it and causes a Reject in the usual way. The receiver reports the refusal with a processing fault instead (SYNC-03-007). |
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 that starts at the broken entry, as the replica's stalled Reject makes it, 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
Retry and Backoff
Leadership and Ownership
Commit Discipline
System Invariants
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-08-001 |
MUST | Append-only integrity: log entries MUST be immutable once committed and MUST be uniquely identified by their content hash Replays and the drain find an entry by its hash. If two different entries could have the same hash, or an entry could change after commit, a replica could apply the wrong entry and still pass the chain check. |
Refines: SYNC-00-005 — Restates the immutability of SYNC-00-005 as a system invariant |
SYNC-08-002 |
MUST | Deterministic projection: given an identical canonical recorded log prefix, all compliant implementations MUST derive identical state Each replica gets its case state from the ledger. The ledger keeps replicas in step only if the same entries always give the same state, on every implementation. Any other input, such as the local clock or the arrival order, would let two replicas with the same ledger disagree. |
|
SYNC-08-003 |
MUST NOT | Idempotent replay: reprocessing any canonical recorded log prefix (including duplicates) MUST NOT change the resulting state Replays, retries and restarts make an actor process some entries more than once. If a second pass changed the state, recovery itself would corrupt the case. |
|
SYNC-08-004 |
MUST NOT | Monotonic visibility: participants MUST NOT regress their acknowledged log position The CASE_MANAGER uses each peer's acknowledged position to decide what to send and when to replay. A position that moves back would make it send again entries the peer already holds, and could reset the replay cooldown. An acknowledged entry stays in the peer's append-only ledger, so a lower position is never true. |
|
SYNC-08-005 |
MUST | Reject-on-divergence: entries that do not extend the current hash chain MUST be rejected and MUST trigger resynchronization "Rejected" means "not applied and not persisted as the current tail"; it does NOT require discarding the entry. A forward-gap entry (a valid entry whose predecessor has not yet arrived) MAY be retained in the non-ledger holding area of SYNC-14 pending its predecessor, and MUST still trigger resynchronization (the Reject remains the backstop for genuinely lost entries). See ADR-0037 (docs/adr/0037-buffer-out-of-order-ledger-entries.md). |
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.
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-13-005 |
MUST | CaseLedgerEntry records for a case's authoritative ledger MUST be authored only by the case's CASE_MANAGER; no other actor may write authoritative case ledger entries.The CASE_MANAGER's single authoritative write path is the enforcement mechanism for the ledger's append-only integrity and the write-ownership guarantee required by SYNC-12. |
Refines: SYNC-01-004 — Restates the single-writer rule of SYNC-01-004 for CaseLedgerEntry records |
SYNC-13-006 |
MUST | Participants MUST obtain replica CaseLedgerEntry records solely from received Announce(CaseLedgerEntry) activities.Channelling all replica writes through the receive behavior tree ensures that effects are applied in the correct order and that the already-stored gate (SYNC-12-003) is the sole idempotency mechanism. |
|
SYNC-13-004 |
MUST | A receiver MUST treat an inbound Announce(CaseLedgerEntry) as the announcement of a ledger entry on first delivery, without needing any stored record of the entry; whether the entry is already stored (SYNC-12-003) MUST be the only test that distinguishes first delivery from repeat delivery. On first delivery the receiver has no stored record of the entry, so routing cannot depend on one. One test for repeat delivery keeps all implementations in agreement about when effects run. A second test could disagree with it and skip or repeat effects. |
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 it safe to hold an entry for any length of time. The replica applies a held entry only after it commits an entry whose entry_hash equals the held entry's prev_log_hash, so a fork or a rewrite cannot act as the predecessor. The replica holds the Announce and not the entry alone, so the drain applies what the replica received. An entry held alone can only reach the inbox again inside an Announce that the replica makes up, which names a sender that never sent it. |
|
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. Holding an entry covers an entry that arrives out of order. It does not cover an entry that was lost. The Reject tells the CASE_MANAGER where the replica's chain ends, so the lost entry is sent again. Without it, a replica with a lost entry would hold its later entries permanently. |
|
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. A held entry can apply only after its predecessor. Each commit can make one held entry appliable, and that entry can make the next one appliable. If the drain stopped after one step, entries that arrived early would stay held until some other delivery came. |
|
SYNC-14-004 |
MUST | Draining a held entry MUST apply the entry's domain effects before persisting it (SYNC-12-001). A drained entry becomes a stored entry like any other. The already-stored check relies on every stored entry having its effects applied, so the drain must keep the same order (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. A separate drain path can drift away from the in-order path. A drained entry could then apply its effects after it is stored, or skip a check that in-order delivery makes. One shared path keeps the effects-before-persist rule true for every entry (SYNC-12). |
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 backstop is asked once per wait, not once per held entry, and the CASE_MANAGER answers once per need; both limits are rate limits, never stops (SYNC-15-016 to SYNC-15-025, ADR-0168). 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 tell the CASE_MANAGER by sending it a Reject(CaseLedgerEntry) with last_accepted_hash="", so that the CASE_MANAGER replays all entries from the beginning once the case is delivered, unless an earlier such Reject for the same case lets it withhold one (SYNC-15-016). An empty last_accepted_hash means "I hold no entries; replay from genesis." The CASE_MANAGER's replay path (SYNC-03-002) replays every entry after the hash it receives. An empty string matches no entry, so it replays the full ledger. Without the Reject, the CASE_MANAGER never learns that the delivery failed, and the replica stays without the entry permanently. One Reject asks for the whole ledger, so a second one asks for nothing new (ADR-0168). |
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.Without the genesis hash, the replica cannot check that the entry belongs to this case's chain. A stored entry also means that its effects were applied (SYNC-13-001). Storing an unchecked entry would put an entry in the ledger that may not be canonical, and its effects would never apply. |
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 |
SHOULD | The CASE_MANAGER SHOULD bound the rate at which it replays entries to a peer whose acknowledged replication position has not advanced. A peer that cannot anchor its hash chain rejects every entry replayed to it. Each Reject causes a replay, and each replayed entry causes another Reject. Without a bound, this loop grows without limit and can starve the CASE_MANAGER's other work. The bound is a rate limit and not a stop, because a later Reject must still get a replay when a replayed entry is lost. |
Depends on: SYNC-08-005 — Bounds the replay that the SYNC-08-005 resynchronization 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 and replay recovery (SYNC-15-001) does not converge when deliveries arrive out of order, for the same reason that forward gaps are held (SYNC-14). The replay sends entries one by one over the same unordered transport, so each early entry is rejected again. This adds load, and each round can lose an entry. Holding the entry lets it drain in order 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. A participant can compute the genesis hash from the case object alone: it comes from the case id, the creation time and the Case Owner's actor id (CLP-08-002). Seeding the case therefore anchors the chain, and the participant need not wait for a second delivery of the genesis entry. Draining on seed 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-015 |
MUST | When the CASE_MANAGER answers a Reject(CaseLedgerEntry) with last_accepted_hash="" with a replay, it MUST first send Announce(VulnerabilityCase) to the rejecting peer.A peer that rejects from genesis does not hold the case, so it cannot compute the per-case genesis hash (CLP-08-005). Without the case, the peer fails the chain check on each replayed entry and rejects it again. The case lets the peer anchor its chain and drain the entries it holds (SYNC-15-005), so the replay that follows can apply. |
|
SYNC-15-016 |
SHOULD NOT | A participant that has sent the CASE_MANAGER a Reject(CaseLedgerEntry) with last_accepted_hash="" for a case it still does not hold SHOULD NOT send another for that case until a bounded interval has passed since the last one.One genesis Reject asks for the case and the whole ledger. The participant holds every early entry and drains them when the case arrives (SYNC-15-004, SYNC-15-005), so a Reject for each further early entry asks for nothing the first did not. On a loaded host the CASE_MANAGER answered each of them with a full replay, and the duplicates delayed unrelated messages by about 20 seconds (#5552). |
Refines: SYNC-15-001 |
SYNC-15-017 |
MUST | A participant that withholds genesis Rejects under SYNC-15-016 MUST send one for the first entry of the still-missing case it receives after the interval has passed, as a new activity rather than a redelivery of an earlier Reject. The genesis Reject is the only recovery for a participant whose replay was lost, so withholding it may delay recovery but never end it. A redelivery of an earlier Reject carries that Reject's id, and the CASE_MANAGER drops a known id as a duplicate (ID-02-001), so a request after the interval must be its own activity. |
Refines: SYNC-15-016 |
SYNC-15-018 |
MUST | Once a bounded interval has passed since an actor's rate limit last let through a replay, or a replay request, about the same case and position, that limit MUST let the next one through. Every limit in this group trades recovery time for quiet. A limit that never lets a later request or replay through would turn a lost replay into a replica that never converges. |
Refines: SYNC-15-003 |
SYNC-15-019 |
MAY | The CASE_MANAGER MAY decline to answer a Reject(CaseLedgerEntry) with last_accepted_hash="" whose rejected entry was included in a replay from genesis it sent the same peer within a bounded interval; declining sends neither the case nor any entry.A Reject the CASE_MANAGER handles after it already replayed that entry from genesis to the same peer asks for a replay that is already on its way. The CASE_MANAGER cannot rely on every participant to limit its own requests (SYNC-15-016), because other implementations share the wire, so it may limit them itself. |
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 active-participant standing (CM-10-004, CM-23-020), not the gap Reject's (SYNC-03-005): that one admits a closed participant, but a copy of the current case would cross the closure cutoff such a participant is held to (CM-23-004), so its diverged replica is not repaired this way. |
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 state the empty chain's ledgerPosition (index -1, SYNC-18-001) and MUST name the case as its target, asking for a replay from genesis; a receiver MUST refuse a Reject that names no entry and states no position, or a position other than the empty chain's, and the CASE_MANAGER MUST answer the empty-chain form as it answers any replay from genesis (SYNC-15-015).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. The position stated on every case activity (SYNC-18) 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.An inert or closed participant's replica stops on purpose, so its stated position always lags, and answering it would send withheld case content (SYNC-03-005). CM-10-006's backfill catches up a participant that becomes active again. The Invite resend is no answer to the position and runs apart from this check (SYNC-20-001, ADR-0163): an unconsented participant on the roster is resent the Invites it was already sent, which carry no withheld content, and its statement is otherwise refused. |
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). The CASE_MANAGER has no other record of how far a quiet participant has got (SYNC-04-001). The stated entry is a fact the participant reports about itself, so recording it gives the replay a correct start and the cooldown a correct position. |
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. |
|
SYNC-17-008 |
SHOULD | Once an Announce(VulnerabilityCase) has seeded a participant's replica of a case it did not hold (PCR-02-003), the participant SHOULD send the case's CASE_MANAGER one Read(CaseLedgerEntry) (SYNC-17-001) naming the entry at the ledger position the seeded case is at (SYNC-16-005).A joiner that dropped an Invite before its case copy arrived (CBT-03-003) may have nothing else to send, so the CASE_MANAGER never learns that its replica is ready. The statement tells it, and the CASE_MANAGER can then resend each Invite the participant has not answered (ADR-0163). The seeded case reflects every entry up to the copy's position, and the replica stores none of them yet (SYNC-16-007), so that position is the replica's tail. A copy that updates a held replica tells the CASE_MANAGER nothing new, and SYNC-16-007 already asks for what it lacks. |
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. A Read(CaseLedgerEntry) carries it as the position of the entry it names, which is that tail (SYNC-17-001). An activity sent before its sender holds the case's ledger, an activity whose object already states a ledger position (Announce(CaseLedgerEntry), Announce(VulnerabilityCase), Create(VulnerabilityCase), Update(VulnerabilityCase)), and a Create(ProcessingFault) 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, and Create(VulnerabilityCase) and Update(VulnerabilityCase) are copies too. Join, Ignore, Leave and Offer of a case name the case and act on it, so they carry the sender's position. A Create(ProcessingFault) is about the processing of one activity, not about a case. Any receiver sends one, including for an activity it could not parse and so cannot place in any case (ASK-07-001), and it names no case (ASK-07-004). This applies to the replica's faults too. The property is the ledgerPosition term that 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. A Reject(CaseLedgerEntry) that names an entry and carries no position MUST NOT be refused for it: it comes from a peer that does not hold the case yet, which the CASE_MANAGER seeds and replays from genesis (SYNC-15-015).Core code then receives a non-optional position (ADR-0032), and every ledgered act carries one. A receiver cannot tell from a Reject(CaseLedgerEntry) alone whether its sender holds the case, and the peer that does not is the one the replay from genesis exists to repair. |
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 |
Arrival-Order Independence and Eventual Consistency
The transport promises no delivery order (ADR-0037), and other implementations may share the wire, so every message a case actor receives can arrive before, between or after the messages it relates to. The protocol's guarantee is eventual consistency: the case ledger's log_index is the causal order (CLP-14-001), and hash-chain apply plus gap repair bring every replica to the CASE_MANAGER's case file. No decision may lean on an arrival order the ledger does not define. Rationale: ADR-0161.
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-19-001 |
MUST NOT | A coordination decision MUST NOT depend on an arrival order that the case ledger does not define. The CASE_MANAGER's observation order is the case's causal order (CLP-14-001): when it takes concurrent assertions from different senders in the order it observes them, its ledger records that order and every replica learns the same outcome. Every other decision MUST be the same whatever order its messages arrive in: what a participant applies, stores or answers, given a message the CASE_MANAGER sent outside the ledger and the ledger entries it relates to; and what the CASE_MANAGER records, given two assertions one sender sent before the first was committed. Arrival order differs between recipients, between implementations and between runs. A decision that reads it gives different coordination outcomes for the same messages, and the ledger cannot explain the difference. The CASE_MANAGER cannot observe the order one sender sent two messages in, so that order is lost unless the decision does not need it. |
Depends on: CLP-14-001 |
SYNC-19-002 |
MUST | Eventual consistency MUST be the ordering guarantee the protocol makes: every active participant's replica that receives the CASE_MANAGER's committed ledger entries, in any order, MUST reach the CASE_MANAGER's case file through hash-chain apply (SYNC-03-001, SYNC-14) and gap repair (SYNC-00-007, SYNC-00-008, SYNC-16, SYNC-17). A message the CASE_MANAGER sends outside Announce(CaseLedgerEntry) MUST NOT leave a replica in a final state that the ledger entries it relates to do not determine.Replicas see the case at different times, so a stronger guarantee would need a delivery order the transport does not give. A direct message that wrote state the ledger later cannot correct would let two replicas holding the same ledger disagree for good. |
Refines: SYNC-08-002 |
SYNC-19-003 |
MUST NOT | Per-recipient delivery order MUST NOT be a protocol guarantee. A receiver MUST NOT rely on a sender delivering its messages in the order it sent them, and a sender MUST NOT rely on its own ordered delivery for a receiver's correctness. An implementation MAY deliver in order per recipient as an optimization (OX-01-005). Another implementation may share the wire with no ordered outbox, and even an ordered outbox loses that order when an Announce is lost and replayed later, so a rule that holds only under ordered delivery fails in exactly the cases recovery exists for. |
Constrains: OX-01-005 |
Resending Unanswered Invites
An Invite can be lost before its addressee can answer it. A joiner's relayed Invite(EmbargoEvent) or full-case Invite that arrives before its case copy waits in the pre-bootstrap queue, and is dropped when the queue's window ends (CBT-03-003); any Invite can be lost in delivery. A participant that states its ledger position tells the CASE_MANAGER its replica holds the case, which is the moment a resent Invite can be answered. So the CASE_MANAGER resends each Invite the participant still owes an answer to, as first sent and rate-bounded per Invite and participant. Rationale: ADR-0163.
| ID | Priority | Requirement | Related |
|---|---|---|---|
SYNC-20-001 |
MUST | When a participant on the case's roster that is not removed (CM-31-001) and whose closure is not recorded states a ledger position that names an entry in the CASE_MANAGER's ledger, in Read(CaseLedgerEntry) (SYNC-17-001) or as an activity's ledgerPosition (SYNC-18-001), the CASE_MANAGER MUST resend each Invite that participant owes an answer to (SYNC-20-002), whether or not the participant is active (CM-10-004).A position statement is the first sign that the replica holds the case, so a resent Invite can now be answered (ADR-0163). The resend is not an answer to the position, so it runs apart from SYNC-17-002 and SYNC-18-007: a joiner that lost its Invite to the embargo in force has not consented, so it is inert, and is the one to ask again. A resent Invite was already sent and is not case content (CM-10-007), so nothing withheld leaks. |
Refines: SYNC-17-002 Refines: SYNC-18-007 Depends on: CM-10-007 |
SYNC-20-002 |
MUST | An Invite the CASE_MANAGER sent a participant MUST be treated as owed an answer while that answer can still count, and at no other time: an Invite(EmbargoEvent) whose embargo is still a proposal or is the embargo in force, whose consent row for the participant is INVITED (CM-18-001), and whose RSVP deadline has not passed (CM-28-001); and the full-case Invite (CM-11-010) until the CASE_MANAGER's ledger records the participant's reply to it. Of several Invites for one embargo, only the last one sent is owed, and the Invite that the activity stating the position answers is not owed.Only an answer that can still change something is worth asking for again; an answered Invite, a decided or withdrawn proposal and a passed deadline cannot. An Invite for the embargo in force is still owed: a reinstated participant (CM-31-040), or a joiner whose proposal was activated first, is asked to sign it. The stating activity may be the answer itself, committed after its position is read, so its Invite is excluded by name. |
Refines: SYNC-20-001 |
SYNC-20-003 |
MUST | A resent Invite MUST be the activity as first sent: the same id, ledgerPosition and RSVP deadline (SYNC-18-002), delivered from the body sealed when it was first sent (OX-07-001), never a new Invite built for the resend. So an Invite whose sealed body the CASE_MANAGER does not hold is not resent.A receiver that already handled the Invite treats a redelivery as a no-op (IE-10-001), and one that dropped it handles it as new. Resending never extends a deadline, so silence before the deadline is still a pocket veto, and the full-case Invite's ledger floor (CM-11-012) is unchanged. |
Refines: SYNC-18-002 Depends on: OX-07-001 |
SYNC-20-004 |
MUST NOT | The CASE_MANAGER MUST NOT resend an Invite to a participant while an earlier send of that Invite is still pending in its outbox; such a skipped resend does not count toward its cooldown (SYNC-20-005). An Invite still waiting in the outbox has not been lost, so a second copy would only queue behind it. Not counting the skip keeps a later, real resend from waiting out a cooldown no delivery started. This is the replay's rule for a pending Announce (SYNC-15-012). |
Refines: OX-06-002 |
SYNC-20-005 |
MUST | Each consecutive resend of one Invite to one participant MUST double the cooldown before the next resend of that Invite to that participant, from a base value up to a fixed ceiling; a position stated inside the cooldown resends nothing. A participant that leaves an Invite unanswered on purpose states its position again and again. A doubling cooldown soon makes the resends rare, and the ceiling still resends at a fixed interval until the deadline passes, as the replay bound does (SYNC-15-013). |
Refines: SYNC-15-013 |
SYNC-20-007 |
MUST NOT | A resent Invite MUST NOT be committed as a ledger entry, nor change the outcome of the activity that stated the position. The Invite's emission was ledgered when it was first sent, so a resend changes nothing in the case file and is not a case event (ADR-0152). The activity that stated the position is judged on its own merits, as SYNC-18-007 requires of the rest of the answer to its position. |
Refines: SYNC-17-006 |
SYNC-20-008 |
MUST NOT | The CASE_MANAGER MUST NOT resend an Invite to a sender that is not on the case's roster, that was removed (CM-31-001), or whose closure the case records. A removed participant was taken out by the Case Owner's decision, and inviting it again would undo that decision (CM-31-013). A closed participant may act on nothing further (CM-23-018), and an actor off the roster was never sent an Invite to answer. |
Refines: SYNC-20-001 Depends on: CM-31-013 |
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.
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. Other actors address an Actor, and decide whether to trust it, by its one id. A separate identity for each case would need its own discovery and its own trust anchor for every case, and a message sent to the Actor's main id would not reach the case. Keeping each replica inside the Actor gives every case the same inbox and the same identity. |
Refines: CM-01-001 Refines: AKM-01-002 |
PCR-01-002 |
MUST | There is exactly one CASE_MANAGER per case, acting on the Case Owner's behalf. The CASE_MANAGER is the single writer of the case ledger. Two CASE_MANAGERs could each commit a different entry at the same index, and the replicas would then disagree with no rule to pick one. A participant also needs exactly one sender whose case updates it accepts (PCR-03-001). |
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 choice. Code that reads another Actor's store, or waits for a side channel, works only while two Actors share a host. Per-actor stores (ADR-0073) prevent direct reads and writes, but not code that expects the case to be visible early. The report receiver does not create the case (ADR-0041), so it holds the case only after the CASE_MANAGER's Create(VulnerabilityCase) arrives. A tree that acts on the case sooner has nothing to act on. |
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.A participant that gets every full case copy in one activity type applies each copy through one path, with one sender check (PCR-03-001) and one ledger-position check (SYNC-16-006). Two delivery types would need two sets of checks, and a gap in either set would let a copy in that the other set refuses. |
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.A participant cannot act on a case it does not hold, and only the CASE_MANAGER holds the canonical copy. Sending the copy when the participant becomes active gives it the case at the moment it is entitled to it. An inert participant is not entitled to case content (CM-10-004), so it gets no copy. |
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.The first case copy a participant receives is its only source for the case. If the participant did not store it, every later ledger entry for the case would name a case the participant does not hold. |
|
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.Create(VulnerabilityCase) says that a case exists. If it also delivered full copies, a receiver would have two kinds of copy to apply, and each would need the sender check (PCR-03-001) and the ledger-position check (SYNC-16-006). One delivery type for copies (PCR-02-001) keeps those checks in one place. |
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. The CASE_MANAGER is the single writer of the case. A replica that accepted a case-state update from any other sender would let that sender change the participant's view of the case without the CASE_MANAGER's checks and without a ledger entry, and the replicas would then disagree. |
Implements: CM-06-002 |
PCR-03-002 |
MUST NOT | The Case Owner actor MUST NOT write directly to its own local case copy. When the Case Owner is not the CASE_MANAGER, its copy of the case is a replica like every other, and it changes only after the CASE_MANAGER has checked the change and recorded it in the ledger. A direct write skips both steps, so the other replicas never learn of the change and the owner's copy then disagrees with the canonical case. |
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.The sender check of PCR-03-001 compares the activity's actor id, and a sender writes that id itself. Without authentication, any sender can write the CASE_MANAGER's id there and have its copy applied. Actor signatures are not yet available, so this rule applies to production only. |
|
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.An Actor holds replicas of many cases behind one inbox (PCR-01-001), so each case-scoped activity must say which case it is about. One standard field for that lets every receiver find the replica the same way. |
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.A participant finds the replica an activity belongs to from its context (PCR-04-001). An activity from the CASE_MANAGER with no case context also skips the hold for unknown cases (PCR-06-002), so the participant can process it before it holds the case. |
|
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. Other actors reach a participant at its one Actor identity (PCR-01-001). A separate inbox for each case would be a second address that senders must discover and trust for every case, and a message sent to the wrong address would not reach the replica. |
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.Activities can arrive before the case they name, because the transport does not keep order (ADR-0161). The receiver has already taken delivery, so the sender has no reason to send the activity again, and a discarded activity is lost. Keeping it, or reporting the drop, keeps the participant from losing a case change without a record. |
|
PCR-06-002 |
SHOULD | When an activity with an unknown case context arrives, the actor SHOULD queue the activity briefly. An activity for an unknown case usually arrives before the case copy that would let the participant apply it. Holding it for a short time gives that copy time to arrive. The participant does not ask for the copy: the CASE_MANAGER sends it when the participant joins (CM-11-008), and resends an Invite the participant dropped once the participant states its ledger position (SYNC-17-008, ADR-0163). |
|
PCR-06-003 |
MUST NOT | Activities with unknown case context MUST NOT update any local state until a valid case replica has been established. Until a replica exists, the participant has nothing to check an activity against: not the case, not its CASE_MANAGER, not its roster. A write made then could seed case state from an untrusted sender, which PCR-03-004 forbids for the case itself. |
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), and the case arrives only after it accepts. An Invite held until the case arrives would wait for the Announce that only its own Accept can bring, so the invitee could never answer it, except from a copy stored under the sender's id, which DL-06-006 forbids. The Invite writes only its intake archive and its trust anchor, so it sets no case state before the replica exists (PCR-06-003). |
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).A participant sends each case-scoped message to the CASE_MANAGER, so that the CASE_MANAGER can check the sender, record the change in the case ledger and send it on to the other participants (PCR-08-003). A message sent straight to a peer gets none of these steps, so the peer's replica and the canonical case disagree. RK is not a case message: it answers the reporter's Offer, which the reporter sent to that receiver before the case existed, so the answer goes back to the reporter (RMB-08-003). |
|
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.Only the CASE_MANAGER's Announce(CaseLedgerEntry) tells a participant what the case ledger holds. A participant that sends its message to other participants as well gives them a change that the CASE_MANAGER has not yet checked or recorded, and they may act on a change that the CASE_MANAGER later refuses. |
|
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 the case's active participants (CM-10-004) as the standard cascade. This cascade is the only mechanism by which participants learn of accepted case-state changes.Participants learn of case changes only from the ledger. If the CASE_MANAGER applied a change without an entry, or recorded an entry without sending it, the other replicas would never learn of the change and would disagree with the canonical case. |
|
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.If the commit and the broadcast needed a separate request, a CASE_MANAGER that did not get the request would apply a change that no replica learns about. Making them part of processing the message means every processed change reaches the ledger and the participants. |
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.The case's full participant list includes actors that are not entitled to every message (CM-10-004), and it reaches peers that the CASE_MANAGER has not yet checked the message for. Taking the CASE_MANAGER from the participant's own replica gives one recipient that the participant already trusts. |
|
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.A receiver records the Invite's actor as its trust anchor (PCR-03-004), and it later accepts the case copy only from that actor. If the Case Owner were the actor, the invitee would anchor its trust on the wrong actor and answer the wrong actor. attributedTo keeps a record of who asked for the Invite. |
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.Only the CASE_MANAGER records the invitee's join in the case and in the ledger. When the Case Owner is not the CASE_MANAGER, an Accept addressed to the Case Owner reaches an actor that cannot record the join, so the join is never recorded. |
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. The invitee's replies are the only source of its join and its judgment of the case. If the CASE_MANAGER did not record them, the case would show the invitee as not joined, and the invitee would get no case content. |
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. RM state belongs to each participant, and it is the participant's own judgment (ADR-0121). Outside the bootstrap record of CBT-01-008, only that participant declares it. An RM change that the Case Owner emits for the invitee records a decision the invitee never made, and the case then shows the invitee engaged when it is not. |
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).Receivers trust the actor field to name the actor that made the decision, and the sender checks depend on that. An activity that names another actor forges that actor's decision, and every receiver applies it as if that actor had sent it. |
Refines: PCR-08-001 |
PCR-08-012 |
MUST | The invitee's Accept(Invite(Actor)[target=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 |
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.
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, rather than a reference, a minimal stub or a partial trust-only payload.The receiver seeds its Participant Case Replica from this activity. A stub or a partial case leaves it nothing complete to seed from. A reference leaves it nothing to read: it has no trusted source to fetch the case from, because it trusts the CASE_MANAGER only after it accepts this activity (ADR-0170). |
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 (CBT-01-013, CBT-01-014). The CASE_MANAGER creates the case (ADR-0041) and the report receiver asks for it, so the two roles can belong to two actors. CASE_OWNER is the standing of the party whose disclosure decisions the case serves, and it is never delegated. CASE_MANAGER is the right to write the canonical case ledger, held by the actor whose store holds the canonical case (ADR-0073). If the snapshot merged the roles, the ledger writer could also make the Case Owner's decisions. |
Refines: CM-02-001 Refines: CM-02-019 Refines: PCR-01-002 |
CBT-01-013 |
MUST | The bootstrap case snapshot MUST record the actor whose CaseProposal caused the case to exist as a participant with the CASE_OWNER role. That actor is the report receiver, whatever other roles it also holds.CASE_OWNER is the standing of the party whose disclosure decisions the case serves. The report receiver asked for the case, so the snapshot names it as the Case Owner for every replica seeded from it. |
Extends: CBT-01-003 |
CBT-01-014 |
MUST | The bootstrap case snapshot MUST record the actor that created the case as a participant with the CASE_MANAGER role.CASE_MANAGER is the right to write the canonical case ledger, held by the actor whose store holds the canonical case (ADR-0073). Every participant finds the CASE_MANAGER in the snapshot's participant records. |
Extends: CBT-01-003 |
CBT-01-015 |
SHOULD | In the bootstrap case snapshot, the participant that holds CASE_MANAGER SHOULD also carry the COORDINATOR role.The actor that manages the case also coordinates it. Carrying the role lets checks that look for a coordinator find the CASE_MANAGER. |
Extends: CBT-01-014 |
CBT-01-016 |
MUST NOT | The CASE_OWNER role MUST NOT pass to the CASE_MANAGER by delegation.CASE_OWNER is never delegated. If the snapshot merged the two roles by delegation, the ledger writer could also make the Case Owner's decisions. |
Extends: CBT-01-003 |
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. The CASE_MANAGER shares case content only with active participants (CM-10-004). A submitter that is not on the starting roster gets no updates until it is invited, although it is the party that brought the report. On the roster from the start, it can follow the case from its first entry. |
|
CBT-01-005 |
MUST NOT | A report submitter MUST NOT trust a newly introduced CASE_MANAGER unless its bootstrap Create(VulnerabilityCase) comes from the report's receiver and references the submitted report.The report receiver is an actor the submitter chose when it sent the report. The record the submitter wrote when it sent the report ties the report to that actor, so the sender cannot forge it (PCR-05-001). The sender writes the roster in the snapshot, so any actor can name itself CASE_MANAGER there. The report reference also stops an actor from attaching an unrelated case to the submitter. |
Refines: PCR-05-001 Refines: AKM-02-001 |
CBT-01-017 |
MUST | The report submitter MUST check the bootstrap of CBT-01-005 against a record it wrote itself before the case arrived, never against the snapshot's roster. The sender writes the roster in the snapshot, so any actor can name itself CASE_MANAGER there. The record the submitter wrote when it sent the report ties the report to the actor it chose, so the sender cannot forge it (PCR-05-001). |
Extends: CBT-01-005 |
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.The receiver checks later CASE_MANAGER traffic, such as Announce(VulnerabilityCase), against this record (CBT-02-001, CBT-02-003). Without it, the receiver could judge each later message only by what the message claims about itself. The report-to-case link also lets the submitter find the case for its report. |
Refines: PCR-05-001 |
CBT-01-007 |
MUST | The bootstrap Create(VulnerabilityCase) payload MUST carry each CaseParticipant record the case lists inline.The receiver seeds its replica from this payload, and later guards read the participant records it seeds, such as the participant checks on a sender. A record the payload leaves out is one the replica lacks when a guard needs it. A record named only by reference is left out too: the receiver does not fetch it (AKM-05-007), because before it accepts this activity it has no trusted source to fetch it from (ADR-0170). |
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.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-018 |
MUST | The CASE_MANAGER MUST set the reporter's RM.ACCEPTED directly, never inserting intermediate transitions (RM.START, RM.RECEIVED, RM.VALID) it never observed for the reporter.The CASE_MANAGER cannot observe the reporter's internal state progression. Intermediate RM records written on the reporter's behalf would place unobserved events into the canonical case ledger. |
Extends: CBT-01-008 |
CBT-01-009 |
MUST | The CASE_MANAGER MUST generate its Create(VulnerabilityCase) only after the VulnerabilityCase is fully initialized, so the activity records a completed initialization, not an intent. Fully initialized means every participant is at its correct initial RM state and every role is assigned.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-019 |
MUST | The report receiver's introduction to the submitter (CBT-01-001) MUST relay the completed snapshot of CBT-01-009. The submitter seeds its replica from the introduction. A relay that carried a different or earlier snapshot would seed the submitter's replica with a state the CASE_MANAGER never recorded. |
Extends: CBT-01-009 |
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 a sender no trust anchor it wrote names as the expected sender, and store nothing from it. A snapshot naming that sender as CASE_MANAGER does not change this (CBT-01-017).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.Two activity types let a receiver tell a first introduction from a later sync. It checks the Create against the record it wrote when it sent the report (CBT-01-005). It checks each Announce against the CASE_MANAGER that the accepted Create named (CBT-01-006). |
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. Transport authentication shows which actor sent a message. It does not show that the actor may manage the case. Only a Trust Anchor the receiver wrote, from a relationship it had before the case arrived, ties the case to a CASE_MANAGER (CBT-01-005, CBT-04-001). |
Refines: AKM-01-002 |
CBT-02-003 |
MUST | A participant that trusts a CASE_MANAGER for a case MUST replace that identity only through an explicit transfer workflow, never through ordinary case-update traffic. If ordinary case traffic could replace the trusted CASE_MANAGER, a case snapshot that names a different manager would move trust with no decision by the Case Owner. The receiver would then accept updates from an actor it never checked. An explicit workflow makes the change one step that participants can see. |
Refines: CM-02-010 |
Out-of-Order Handling
| 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. The transport does not promise delivery order (SYNC-19-003), so a message from the real CASE_MANAGER can arrive before the bootstrap that makes it trusted. If the receiver drops the message at once, the message is lost. If the receiver applies it at once, it trusts a sender it has not checked. A short hold keeps the message until the bootstrap arrives, and the limit stops untrusted messages from staying in storage with no end. |
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. Before the bootstrap is validated, the receiver cannot know that the sender is the CASE_MANAGER. A state change from that message can come from any actor, and the receiver would then hold case state that no trusted source sent. |
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. A message the receiver cannot yet trust must not wait without limit for a case copy that may never arrive. Dropping it when the interval ends limits what the receiver stores. ADR-0163 gives the recovery of a dropped Invite to the CASE_MANAGER, which knows which Invites each participant has not answered. |
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_MANAGER rather than Create(VulnerabilityCase).A late joiner submitted no report, so it holds no record that ties a case to an actor it chose. The stub Invite gives it one: the joiner chooses to accept it, and the accepted Invite is its Trust Anchor for the case (PCR-03-004). A Create(VulnerabilityCase) from an actor the joiner never dealt with gives it nothing to check the sender against. |
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.The joiner checks each later Announce(VulnerabilityCase) against the Trust Anchor the Invite left. If the Invite does not name both the case and the CASE_MANAGER, the joiner cannot tell which sender to trust for which case. |
|
CBT-04-003 |
MUST | After the late joiner has accepted the stub Invite (Accept(Invite(Actor)[target=VulnerabilityCaseStub])), the CASE_MANAGER MAY send Announce(VulnerabilityCase) without a separate Create(VulnerabilityCase) bootstrap.The accepted stub Invite already gives the joiner its Trust Anchor (CBT-04-001). A further Create(VulnerabilityCase) adds a second trust step with nothing new to check, and it uses Create for a case the joiner did not see created. |
Refines: PCR-07-007 |
Verification
| ID | Priority | Requirement | Related |
|---|---|---|---|
CBT-05-008 |
MUST | This rule covers a receiver that finds required participant data absent from a bootstrap Create(VulnerabilityCase) (CBT-01-007). It MUST reject the bootstrap as a protocol error, never skipping the participant, inferring its state, or synthesizing a record from data the payload lacks.Silent reconstruction of missing protocol data masks protocol bugs: a fallback that fills in missing data "from domain knowledge" makes the protocol look as if it works when it does not, so the failure must be visible to be diagnosed and fixed. Replicas are built only from ledger content; data not in the ledger did not happen. A participant the payload names only by reference is absent too: CBT-01-007 requires it inline, and the receiver does not fetch it (AKM-05-007). |
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.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. |
Refines: HP-09-001 |
CBT-06-003 |
MUST | Each Offer(VulnerabilityReport) a reporter sends MUST carry an activity id distinct from every other Offer of the same report.With one recipient per Offer (CBT-06-001) and an id of its own, each submission attempt is identified by at least the pair (report, recipient). The cases that follow from two attempts can then be told apart. |
Extends: CBT-06-001 |
CBT-06-002 |
MUST | This rule covers a reporter that has offered one report to several recipients. It MUST accept a valid bootstrap Create(VulnerabilityCase) from each, checking each against its sender (CBT-01-005) and recording its case and CASE_MANAGER for that recipient alone.A reporter often offers an unanswered report again to a Coordinator, and either recipient may then start a case. A reporter that trusts only one sender per report refuses the bootstrap from the second recipient and cannot join that case. How the reporter stores trust for each recipient is an implementation choice. This requirement controls only which bootstraps it accepts. |
Refines: CBT-01-006 Depends on: CBT-01-005 |
CBT-06-004 |
MUST NOT | This rule covers a reporter that has offered one report to several recipients. It MUST NOT let a further offer, or its acceptance of one bootstrap, change how it treats another recipient's bootstrap. That covers whether it accepts the bootstrap and which case it records for that recipient. Each recipient's case stands on its own trust anchor. If one recipient's bootstrap could displace another's, the order in which the bootstraps arrive would decide which case the reporter can join. |
Extends: CBT-06-002 |