Skip to content
Reference

Architecture Specifications

Recommended structure that carries over to other languages but is not required for Vultron compliance. Behavior trees, and keeping protocol behavior inside the behavior tree, are architecture, as are hexagonal boundaries, event-driven dispatch, port and adapter patterns, and fail-fast principles.

Behavioral Specifications

The following specifications define Event-Condition-Action (ECA) behavioral requirements with typed preconditions, ordered steps, and postconditions.

Actor Knowledge Model

Defines the Actor Knowledge Model: a Vultron Actor's knowledge of the world is strictly bounded by the ActivityStreams 2.0 activities it has received. Actors must not assume recipients hold objects that were not explicitly included in prior activities; DataLayer access is private and non-shareable, and all inter-Actor communication occurs exclusively through the AS2 wire protocol.

File ID: AKM

Actor Addressing and Discovery

ID Priority Requirement Related
AKM-05-002 SHOULD When an actor is first named by URI and no local record exists, the implementation SHOULD attempt resolution via the injected ActorDiscoveryCallOutBundle (Retriever call-out, ADR-0024, ADR-0025) before minting a minimal record. A FAILURE result from the backend SHOULD produce an explicit WARNING rather than a silent default.

Agentic Readiness

Requirements for API and CLI design that enable reliable interaction by automated agents and AI systems. Covers machine-parseable interfaces, structured response formats, and observable behavior that supports agentic workflows.

File ID: AR

Long-Running Operations

ID Priority Requirement Related
AR-04-001 SHOULD Long-running operations SHOULD return a job or task object immediately with a stable id and status field
AR-04-002 SHOULD A separate polling endpoint or webhook mechanism SHOULD be available to report operation completion

Pagination

ID Priority Requirement Related
AR-05-001 SHOULD Collection endpoints SHOULD expose limit and offset (or cursor) query parameters with documented defaults and maximums

Bulk Operations

ID Priority Requirement Related
AR-06-001 MAY Resources that agents may need to create, update, or delete in quantity MAY expose batch endpoints (e.g., POST /v1/items/batch)

Architecture

Defines the architectural constraints for the Vultron codebase. The system follows hexagonal architecture (Ports and Adapters): the core domain is isolated from all external systems, which interact through defined ports via thin adapters. Covers layer separation rules, the core object hierarchy, domain model richness, and the fail-fast validation boundary.

File ID: ARCH

Layer Separation

ID Priority Requirement Related
ARCH-01-002 MUST Core functions MUST accept and return domain types only
ARCH-01-003 MUST NOT The wire/ layer (AS2 parser, semantic extractor) MUST NOT contain handler logic, case management, or journal management

Wire format concerns are structurally distinct from domain concerns; mixing them prevents future wire format substitution. This is the substantive rule that ARCH-22-001 was a proxy for; ADR-0099 keeps this constraint while repealing that proxy. The import ban forbade the harmless thing — naming a core type in a field annotation — while harmful coupling passed through as duck-typing. Enforce no domain logic in wire; do not enforce no imports.

Driven Adapter Injection

ID Priority Requirement Related
ARCH-04-001 MUST Driven adapters (persistence, delivery queue, DNS resolver, HTTP delivery) MUST be injected into core services via port interfaces

Enables testing core logic with in-memory ports and swapping production backends without touching domain code

Wire Layer Replaceability

ID Priority Requirement Related
ARCH-06-001 SHOULD The wire layer (wire/as2/) SHOULD be replaceable as a unit without touching the core or adapter layers

Handler Isolation

ID Priority Requirement Related
ARCH-07-001 MUST NOT Handler functions MUST NOT re-invoke semantic extraction

Driving Adapter Boundary

ID Priority Requirement Related
ARCH-08-001 MUST Driving adapters (HTTP inbox, CLI, MCP server) MUST invoke the wire pipeline before calling into the core

Any driving adapter that needs to ingest AS2 can reuse the wire pipeline; there is no duplication
ARCH-08-002 MUST A driving adapter MUST promote an incoming wire object to its most-specific domain type at the adapter boundary, before passing the object into the core. The core MUST NOT receive a bare BaseObject or generic BaseModel when a more specific type is determinable from the wire payload.

Promoting at the boundary ("validate at edge") ensures that core handlers receive fully typed objects and never need to downcast or call isinstance() to recover type information that was present on the wire. Deferring promotion into the core violates the hexagonal principle that adapters adapt, cores compute.
Refines: ARCH-08-001
Implements: CS-10-001

Domain Model Richness Constraint

ID Priority Requirement Related
ARCH-09-001 MUST Core domain models MUST be as rich as or richer than their wire-layer counterparts

A thinner core model forces piecemeal additions as handlers are implemented, causing boundary leakage and implementation drift

Fail-Fast Domain Objects

ID Priority Requirement Related
ARCH-10-001 MUST Domain events and domain models MUST validate required fields at construction time and fail immediately if required invariants are not satisfied

Late validation masks missing data and makes debugging harder; fail-fast ensures errors surface at the point of construction

Fail-Fast at Domain Operation Boundaries

Governs relational invariants enforced at runtime entry points — use-case execute(), BT node update(), and helper functions that require non-None resolved values. Extends the construction-time fail-fast rule to cover checks that can only be performed at the point of invocation, not at object construction.

ID Priority Requirement Related
ARCH-15-004 MUST Each domain helper function MUST have exactly one canonical copy. Layer-neutral helpers (no dependencies above models/) MUST live in vultron/core/models/_helpers.py. Use-case-specific helpers belong in vultron/core/use_cases/_helpers.py. Duplicate implementations of the same logic are prohibited.

Duplicate helper copies diverge silently over time. Two copies of _as_id() in use_cases/_helpers.py and services/embargo_lifecycle.py had already diverged in docstring accuracy. Three copies of the case-manager lookup produced inconsistent None-handling behavior across callers. See notes/domain-validation.md and ADR-0032.
Refines: ARCH-10-001 — Canonical-copy discipline is a prerequisite for consistent fail-fast enforcement.

Port Inbound/Outbound Discrimination

ID Priority Requirement Related
ARCH-11-001 SHOULD Core ports SHOULD be organized into inbound (driving) and outbound (driven) categories

Outbound Delivery Pipeline

ID Priority Requirement Related
ARCH-19-001 MUST Outbound delivery MUST be modelled as a three-stage pipeline consisting of Emission, Transport delivery, and Recipient acceptance.

Conflating stages (e.g., treating a transport timeout as a recipient rejection) leads to incorrect retry logic and protocol-state divergence. The three-stage model is the foundation for correct at-least-once delivery. Source: notes/architecture-adapters.md.
ARCH-19-006 MUST NOT Each stage of the outbound delivery pipeline has distinct error semantics and MUST NOT be conflated with another stage.

Treating a transport timeout (stage 2) as a recipient rejection (stage 3) leads to incorrect retry logic and protocol-state divergence.
Refines: ARCH-19-001
ARCH-19-002 MUST Emission (stage 1) MUST be idempotent with respect to the outbox: writing the same activity ID twice MUST NOT create a duplicate outbox entry. Refines: ARCH-19-001
ARCH-19-003 MUST Transport delivery (stage 2) MUST be retried on transient network failures. The retry mechanism MUST be implemented in the outbox worker, not in the use-case or handler. Refines: ARCH-19-001
ARCH-19-004 MUST NOT A transport delivery failure (stage 2 error) MUST NOT be interpreted as recipient rejection (stage 3 semantics). Delivery failure means the activity did not reach the recipient; it carries no information about whether the recipient would accept or reject the activity. Refines: ARCH-19-001
ARCH-19-005 MUST The outbox worker MUST record the delivery outcome (success, transient failure, or permanent failure) in the per-actor process log (Python logging) at the level prescribed in specs/structured-logging.yaml. Delivery outcomes MUST NOT be written to the canonical case ledger; the case ledger is exclusively for CASE_MANAGER-authored protocol-significant assertions (ADR-0019).

ADR-0019 establishes strict separation: the canonical case ledger contains only CASE_MANAGER-authored protocol assertions; per-actor transport observability goes to the process log. Delivery outcomes (whether a network call succeeded) are operational diagnostics, not protocol-significant assertions.
Refines: ARCH-19-001

Core-to-Wire Rendering Port

Governs how core domain objects are rendered into wire-shaped (AS2 camelCase) JSON. Core legitimately needs a wire representation in one place — the CaseLedgerEntry.payloadSnapshot, which CLP-07-011 requires to be receiver-reconstitutable AS2 — and this group makes a driven port the single seam that produces it. Under one object model (ADR-0099) the spelling lives on the core class (ARCH-25-002) and a dimension serializes to a bare state value (ARCH-25-005), so the port's adapter is a plain by_alias dump of the core object; what the group still guards is that core code reaches that dump only through the port.

ID Priority Requirement Related
ARCH-20-001 MUST Core code that requires a wire-shaped representation of a core-branch object MUST obtain it through a driven port declared in vultron/core/ports/. Core MUST NOT produce that shape itself, whether by calling model_dump(by_alias=True) on a core-branch instance or by rewriting dumped keys into wire spellings. The alias_generator a core object inherits from CoreObject (ARCH-12-003) is the mechanism behind the port, not a licence for core to dump directly. Dumping an already-wire-shaped object that core holds (an instance of an as_Base wire vocabulary class with no core counterpart, such as as_Link; a domain type's as_* name is its core class) is unaffected by this requirement; the activity a received handler holds is not one, since the extractor rebuilds it as a core-branch VultronActivity.

A single rendering seam keeps the decision of what goes on the wire (which object, with which @context, failing closed on anything without an AS2 shape) in one adapter instead of at every call site that happens to need camelCase. The requirement is scoped to core-branch objects because core legitimately handles wire objects on the verbatim-capture path; conflating the two would make this requirement unverifiable.
Refines: ARCH-01-004

Post-Construction Mutation Safety

Requirements governing validation of core domain objects after construction. Pydantic v2 validates at construction only, so attribute assignment and in-place collection mutation bypassed every type guarantee, allowing a wire-shaped object to occupy a core-typed field and be silently misread as absent (#2232, #2264). Codified by ADR-0064. Source: CONCERN-2261.

ID Priority Requirement Related
ARCH-21-001 MUST Every core domain model (a pydantic.BaseModel subclass defined under vultron/core/models/) MUST have validate_assignment=True in effect, whether set directly or inherited, so that attribute assignment is validated to the same standard as construction.

Construction-time-only validation means the same value the constructor rejects is accepted by assignment. A wire-shaped object in a core-typed field does not raise when read — it reads as absent, and readers substitute an initial state, silently resetting a participant's ladder (#2232, #2264).
ARCH-21-002 MUST NOT The wire branch MUST NOT inherit validate_assignment from core. The flag is set on the core record root CoreRecord (through ValidatedAssignmentMixin), so as_Base MUST NOT subclass CoreRecord or CoreObject (ARCH-12-001), and inbound wire data stays lenient.

While the shared root existed, setting the flag on it was the one-line fix that looked correct and was not: it reached the wire branch and produced the largest measured blast radius (747 failed, 423 errors) because inbound wire data is legitimately loose. With the wire base on its own root (ARCH-25-004) the flag belongs on the core root, and this requirement keeps the two roots from being rejoined.
Refines: ARCH-12-001
ARCH-21-003 MUST NOT No wire-layer class (under vultron/wire/) MUST have validate_assignment enabled. Strictness belongs at the wire-to-core projection, not on the wire types themselves.

Inbound wire data is loose by contract. Enforcing strictness on the wire types would reject legitimate inbound payloads instead of projecting them (ADR-0062, ARCH-12-001).
Refines: ARCH-21-002

Case-Authority Determination

ID Priority Requirement Related
ARCH-24-001 MUST Determining which actor holds single-writer authority for a case MUST have exactly one implementation — a single role-authority resolver that reads CaseParticipant.case_roles — composed by both behaviors/ and use_cases/. No duplicate or twin resolver may exist.

The authority check was re-derived per site (twin _resolve_case_manager_id / resolve_case_manager_id resolvers kept in sync by convention), backstopped only by ratchet tests that fire when a site forgets to check. One shared implementation replaces the convention with a structural guarantee.
ARCH-24-002 MUST The role-authority resolver MUST reside in a neutral layer below behaviors/ and use_cases/, depending only on models, ports, and enums. Resolving case authority MUST NOT require a behaviors → use_cases import.

The behaviors twin existed to dodge a behaviors → use_cases import (BTND-04-003), yet other nodes performed that import anyway and use_cases/_helpers reached back into behaviors for the URL-shape gate. A neutral home removes the cross-layer reach entirely.
Refines: ARCH-24-001
Implements: BTND-04-003
ARCH-24-003 MUST Exactly one authority condition node MUST answer "is the executing actor the case authority", and it MUST call the single role-authority resolver.

CheckIsCaseManagerNode, CheckIsOwnCaseActorNode, and CheckIsNotOwnCaseActorNode each resolved authority independently; collapsing them onto one condition node (plus a negation decorator) that calls the resolver removes the divergence.
Depends on: ARCH-24-001
ARCH-24-004 MUST NOT No protocol-logic module MUST branch on the case-actor name/URL shape (an is_case_actor_identity predicate or the /actors/case-actor substring) or on Service-hosting location to determine authority, recognition, or routing. Authority derives from the CVDRole.CASE_MANAGER role alone.

Hosting location and URL shape are incidental, and one is provably fragile: _find_case_actor_id's own docstring records a bootstrap window in which the actor enacting CVDRole.CASE_MANAGER fails its own hosting test and would take the participant arm on its own ledger. Role membership is stable from replica-seed onward.
Refines: ARCH-24-001
ARCH-24-005 MUST NOT The store-consistency / anti-fork guard that gates which store may append canonical ledger entries (DeclineForeignLedgerCommitNode / store_for_actor) MUST NOT be conflated with authority determination; the two are distinct concerns.

Once role-authority says "you may commit," the store guard refuses, by raising, to mint a canonical index in a store that is not the log's home (which would fork the hash chain, #2626; CLP-10-014). Authority (role) and "where the write may land" (store) are different jobs.
Depends on: ARCH-24-001

Behavior Tree Integration

Requirements for integrating py_trees behavior trees into Vultron handler functions. Covers the BT execution model, state management via the DataLayer, handler integration patterns, actor isolation, concurrency constraints, and failure diagnosis. Complex protocol workflows such as report validation, case creation, and embargo management must be implemented as BT nodes or subtrees.

File ID: BT

Workflow-Specific Trees

ID Priority Requirement Related
BT-06-005 MUST Cascades from a parent subtree to a child subtree MUST be expressed as subtrees within the use case's behavior tree, not as procedural calls made after the tree has run.
BT-06-006 MUST NOT Protocol-observable actions and state transitions MUST NOT be performed as procedural code outside the BT.

BT Layer Dependency Model

Rules governing the dependency direction between BT nodes and use-case classes. Violations of these rules create circular dependency risk, un-auditable call chains, and excessively large update() methods.

ID Priority Requirement Related
BT-22-001 MUST A BT leaf node's update() method MUST NOT directly instantiate or call a use-case class. Use-case classes are the handlers that create BTs; they are not work items to be called from within a BT node. Calling a use case from inside a BT node creates a BT→UseCase→BT→UseCase call chain that cannot be audited or preempted. Multi-step workflows needed by a BT leaf MUST be modeled as a child subtree of the current tree, not as a use-case call embedded in update().

The BT tree structure is the source of truth for protocol behavior; any logic that executes outside it is invisible to analysis and audit. An in-tree child subtree keeps the full behavior visible in the tree and lets it be preempted by a parent Selector. Identified as BT-IDM-01 in notes/bt-canonical-reference.md.
Refines: BT-06-001
BT-22-002 MUST NOT BT nodes in vultron/core/behaviors/ MUST NOT import private helpers or functions from vultron/core/use_cases/.

Importing a use-case helper into a BT node inverts the hexagonal layer boundary and creates circular dependency risk. The import direction is the enforcement mechanism for the layer rule. Identified as BT-IDM-02 in notes/bt-canonical-reference.md.
Refines: BT-06-001
BT-22-004 MUST The dependency direction between BT nodes and use cases MUST be use_cases/ → behaviors/ (use cases create BTs), never behaviors/ → use_cases/ (BT nodes importing from use cases).

The one-way dependency from use cases into behaviors is the structural enforcement of the hexagonal layer boundary; reversing it creates circular dependency risk.
BT-22-005 MUST If a helper is needed by both a use case and a BT node, it MUST be extracted to a shared utility module (e.g., vultron/core/behaviors/shared/ or a domain-model method) that neither layer owns.

A shared utility module is the canonical resolution when a helper is needed at both layers; placing it in either layer would invert or entangle the layer boundary.
BT-22-003 SHOULD A BT leaf node's update() method SHOULD NOT exceed approximately 30 lines of logic. Nodes whose update() grows to 60–100+ lines typically encode multiple responsibilities (read, validate, create, persist, broadcast) that are better expressed as distinct leaf nodes in a named subtree. Smell signals include more than 3–4 distinct responsibilities, multiple dl.save() calls, Boolean constructor parameters that silently activate optional branches, and internal if/else chains that should be separate Sequence/Selector children.

Long update() methods defeat the core purpose of behavior trees — simple, auditable, composable leaves whose complexity is visible in tree structure rather than in individual nodes. Decomposing a god node into a named subtree factory makes the workflow readable by inspection. Identified as BT-IDM-03 in notes/bt-canonical-reference.md; see also specs/behavior-tree-node-design.yaml BTND-02-001.
Refines: BTND-02-001

Behavior Tree Node Design

Formal requirements for BT node parameterization, composability, and reuse in the core behaviors package. Covers node context injection, blackboard hygiene, module ownership, and the fractal composability pattern that applies at every depth of the tree hierarchy.

File ID: BTND

BT Status-Write Node Transition Validation

ID Priority Requirement Related
BTND-10-002 MUST The per-dimension transition rules, the role gates and the cross-machine entailments applied to a ParticipantStatus write MUST be composed into a single evaluator that returns every violated rule. Every writer of such a status — not only every node that validates one — MUST call that evaluator rather than invoking the individual is_valid_*_transition() or violation_* predicates directly, and MUST report every violation the evaluator returns.

ValidateTriggerTransitionsNode and CreateParticipantStatusNode each implemented the VF, D and PXA transition checks and the role gates independently, with byte-identical message text (ARCH-15-004 violation, live drift hazard), and each enforced a subset the other did not. ISSUE-2906 established that composing the rule set once — not sharing the individual predicates — is what keeps two paths from drifting; composite_state_violations() is the existing instance of that shape. Amended by ADR-0089: the original wording bound "every node that validates such a write", and three writers escaped through it by validating nothing at all (_get_or_create_accepted_status, _build_owner_initial_status, _build_bootstrap_statuses) while a fourth escaped by not being a node (CaseParticipant.append_rm_state). Binding the writer rather than the validator closes both gaps, and the ratchet keys on constructing a dimension rather than on naming a predicate — so validating less no longer confers invisibility. A dimension-construction gate does not by itself reach the wire projection (as_CaseParticipant.append_rm_state), which builds as_ParticipantStatus from flat fields; that shape needs its own trigger.
Refines: BTND-10-001
Implements: ARCH-15-004 — One canonical copy of each transition and role predicate
Implements: EH-07-001 — Reporting every violation at a ParticipantStatus write boundary
BTND-10-003 MUST A ParticipantStatus write node MUST retain its own call to the shared evaluator even when an upstream guard node validates the same write. It MUST NOT be reduced to a delegate that relies on the guard having run.

BTND-10-001's fail-closed-regardless-of-guard-coverage rule is not hypothetical here: CreateParticipantStatusNode is reached from develop_fix.py, deploy_fix.py, close_case_effect.py and two sites in leave.py without passing through ValidateTriggerTransitionsNode, so for those paths its checks are the only validation. Both nodes validating cannot double-report on the trigger path: the guard fails first and the Sequence aborts before the write node ticks.
Refines: BTND-10-001
Depends on: BTND-10-002
BTND-10-004 MUST The ParticipantStatus write node MUST be reached as a child of a behavior tree. A node MUST NOT construct the write node inside its own update() and invoke that node's update() directly.

Five sites did exactly that (develop_fix.py, deploy_fix.py, close_case_effect.py and twice in leave.py), producing six nested update() calls because develop_fix.py ticks one shared helper-built node from two places. A nested update() call skips setup() and the tick cycle, so declared ports are never resolved and the enclosing composite cannot observe the child's status; deploy_fix.py and develop_fix.py additionally wrapped the call in try/except, which is the failure-swallowing shape notes/bt-pitfalls.md documents under "Always Check BTBridge.execute_with_setup Return Value". Six further nodes bypassed the write node entirely, giving three distinct ways to perform one write. One way is the point: two mechanisms for one purpose must both be maintained and tested, and they drift (CS-22-001).
Depends on: BTND-10-002
Implements: ARCH-15-004 — One canonical way to reach the single writer
BTND-10-005 MUST The ParticipantStatus write node MUST obtain case_id from its declared blackboard port and MUST NOT accept it as a constructor argument. It MUST obtain the subject actor as an explicit argument, and MUST NOT fall back to the executing actor on the blackboard when that argument is absent.

A port is the only mechanism for case_id that works everywhere: in received trees the case is discovered at tick time by dereferencing the report, so it cannot be supplied at build time. Trees that do know it at build time seed /case_id through BTBridge.execute_with_setup(**context_data), so one mechanism suffices for both. The subject actor is the opposite: the blackboard actor_id is the executing actor and the subject of the write is frequently someone else — the deferring actor, the sender, the leaver — and conflating the two was bug #2300. _ReportPhaseRMTransition._acting_actor_id()'s sender_actor_id or actor_id fallback is the two-path shape this rule removes: a fallback means both branches need their own tests and the wrong one is selected silently.
Depends on: BTND-10-004

Case Management

Requirements for VulnerabilityCase lifecycle management, CASE_MANAGER responsibilities, and the RM/EM/CS/VFD state model. Covers actor isolation at the protocol level, the distinction between participant-specific and participant-agnostic state, case initialization sequencing, embargo acceptance ownership, and the invitation acceptance lifecycle.

File ID: CM

CASE_MANAGER Lifecycle

ID Priority Requirement Related
CM-02-011 MUST An actor is the single-writer authority for a case if and only if it enacts CVDRole.CASE_MANAGER in that case's participant roster (CaseParticipant.case_roles). Authority MUST be determined from the role, never from hosting location or from the actor's name or URL shape.

Authority was determined several ways, some keyed off where an actor was hosted or the shape of its URL rather than the role. The role is the only protocol-salient signal; the case-actor name is a demo convenience.
Refines: CM-02-002
CM-02-012 MUST The actor enacting CVDRole.CASE_MANAGER MUST recognize its own canonical ledger entries as authoritative from replica-seed onward, including during the bootstrap window before any co-located Service object carries the case context.

The hosting-based check had a bootstrap window in which the real CASE_MANAGER failed its own hosting test and would take the participant arm on its own ledger. Role membership is stable from replica-seed onward and has no such window.
Depends on: CM-02-011
CM-02-013 MUST NOT The case-actor name and any .../actors/case-actor URL MUST NOT be treated by any protocol logic as evidence of authority, recognition, or routing; they are provisioning conveniences with no protocol meaning.

ADR-0041 gave the enacting actor a stable inbox URL, which stands as a provisioning fix. ADR-0088 narrows that identity narrative: the stable URL is a convenience, and no protocol logic may treat the case-actor name or URL shape as evidence of authority.
Refines: CM-02-011

Domain Model Architecture

ID Priority Requirement Related
CM-08-001 SHOULD The system SHOULD maintain a clear separation between the wire representation, domain model, and persistence model for CVD objects

Per-Participant Embargo Acceptance

ID Priority Requirement Related
CM-10-007 MUST The active-participant check (CM-10-004) MUST live in the shared recipient selection that every case-content send uses, not at individual send sites. Invites addressed to a participant so that it can join (the stub Invite) or consent (an embargo Invite, EP-09) are not case content and MUST reach an inert participant that has not been removed (CM-31-013). The embargo-ending notices owed to a bound participant that is not active (CM-31-009) are likewise not case content.

Nearly every case-content send selected its recipients from the roster through one helper (case_addressees, retired by #4046). A per-site check would be missed by the next send site written; one shared check (vultron/core/participants/recipients.py) cannot be.

CASE_MANAGER-Delegated Activity Authorship Contract

Requirements for how trigger use cases assign actor and attributed_to when the CASE_MANAGER emits an Activity on behalf of a requesting participant (delegated message). The reference implementation is delegated_authorship() (vultron/core/behaviors/delegated_authorship.py). Codified from CONCERN-2170.

ID Priority Requirement Related
CM-24-004 MUST Trigger use cases that emit delegated Activities MUST queue the constructed Activity in the CASE_MANAGER's outbox, not in the requesting participant's outbox. The emit MUST sit inside a composite gated on the executing actor holding CVDRole.CASE_MANAGER for the case; code MUST NOT instead compare actor_id against a computed case_actor_id. The delegated emit MUST execute on the container that hosts the CASE_MANAGER: a requesting participant whose container does not host it reaches the CASE_MANAGER by sending it the participant's own activity, and the CASE_MANAGER's received tree performs the delegated emit. A container MUST NOT execute a tree, or enqueue an activity, as an actor it does not host (ADR-0109).

*The CASE_MANAGER's outbox is the delivery path to other participants. Queuing a delegated Activity in the requesting actor's outbox bypasses the CASE_MANAGER as the authoritative intermediary and breaks the CaseLedgerEntry audit trail. Emitting in the CASE_MANAGER's name from a container that does not host it is the same bypass in a different coat: the activity leaves a store that is not the ledger's home, the canonical entry can only be minted by mailing the CASE_MANAGER a copy of its own emission, and no such container could ever sign the activity (OX-10).
The mechanism changed in ADR-0073. This entry previously required setting self._actor_id = case_actor_id in _prepare(). That is a convention, and it conflated three separate ideas CM-24 otherwise keeps apart: the activity's actor (CM-24-001), its attributed_to (CM-24-002), and whose store this is. Spelling the first and third with one variable made a local write look like a foreign one, and it left the identity comparison as the only guard — but the authority is a role held in the case, and its holder may be any Actor type. Once the emit is role-gated the executing actor is the case manager, so both the activity and its outbox entry land in one store by construction (BT-05-006). Ungated, the helper is identity spoofing: any actor reaching it could emit an Announce authored as the CASE_MANAGER to every participant.* Refines: CM-24-001

Participant Removal and Reinstatement

Removing a participant withdraws its entitlement to case content and keeps its record. The Case Owner requests removal and reinstatement; the CASE_MANAGER applies them, records each as one ledger entry, and tells the affected participant directly (ADR-0116).

ID Priority Requirement Related
CM-31-002 MUST Whether a participant is active MUST be computed by one case-level check from stored facts — the CM-10-004 definition: joined (seated by case initialization or accepted the stub Invite), not removed, and a signatory to the active embargo (its consent row for it is AGREED) when an embargo is active — and MUST NOT be stored. RM CLOSED is not an input (#4100). A participant-level property MUST NOT compute a partial answer under a name that reads as "active".

An inferred status that is stored goes stale whenever one of its inputs changes without rewriting it; the precedent is signatory and lapsed consent (CM-18-001). Whether an embargo is active is case state a participant record cannot see, so the full answer lives on the case. A partial participant-level property would read as "active" at a send site and leak embargoed content to a non-signatory.
Refines: CM-10-007

Code Style Specifications

Defines code formatting, import organization, and coding style standards for Python code in the Vultron project.

File ID: CS

Circular Import Prevention

ID Priority Requirement Related
CS-05-001 MUST Core modules SHALL NOT import from application layer modules Derives from: ARCH-01-001
CS-05-003 MUST Shared types and errors SHALL be placed in neutral modules

Code Reuse

ID Priority Requirement Related
CS-09-001 SHOULD NOT New code SHOULD NOT duplicate logic already present elsewhere; prefer extracting shared logic into reusable helpers, base classes, or type aliases

Duplication inflates maintenance cost and allows bug fixes to be applied inconsistently

Port and Adapter Data Exchange

ID Priority Requirement Related
CS-10-001 MUST Data passed across port/adapter boundaries MUST use named, domain-typed objects rather than type-erased containers (plain maps, untyped base classes, or equivalent). Port interface signatures MUST declare parameters and return values as specific domain types, not as generic base types or untyped collections.

Ports that accept or return type-erased containers (dicts, bare base models) serve as machine-readable contracts in name only — the type system cannot enforce what passes through them. Naming the specific domain type at the boundary makes the contract explicit, enables static verification, and surfaces layer-coupling violations at compile/analysis time rather than runtime.
Refines: ARCH-08-001

Datetime and Timezone Conventions

ID Priority Requirement Related
CS-13-001 MUST All datetime values produced by the application MUST be timezone-aware and MUST use UTC

Naive datetimes are ambiguous in a distributed system where actors may run in different timezones; UTC eliminates this ambiguity
CS-13-003 SHOULD All datetime values SHOULD be stored at second precision (microseconds set to zero)

Configuration Management

Requirements for unified, structured application configuration in Vultron. Configuration uses YAML files backed by Pydantic-based models loaded via pydantic-settings with environment variable override support.

File ID: CFG

Config File Format and Loading

ID Priority Requirement Related
CFG-02-005 MUST The application MUST start successfully with no config file and no relevant environment variables set; all fields have sensible defaults.

DataLayer Port

Behavioural contract that all DataLayer adapter implementations MUST satisfy. Covers auto-rehydration on read, type-safe write operations, and port isolation.

File ID: DL

Per-Actor Store Isolation

ID Priority Requirement Related
DL-07-009 MUST When execution is scoped to an actor, every store reference reachable from that scope MUST be reconciled to the same actor — not the store alone. A component injected with a store of its own MUST either offer a named way to be rebound to another actor's store or hold no store at all; writes belonging to one logical operation MUST NOT land in two actors' stores.

Reconciling the store alone fixes half of the problem and leaves the other half looking correct. BTBridge re-scoped the blackboard's datalayer to the executing actor, but the driven adapters on that same blackboard kept the DataLayer they were constructed with — and the adapter is what persists the outbound activity. A delegated emit (a case owner's invite-actor trigger emits as the CaseActor, PCR-08-007) therefore created the Invite in the requesting actor's store while the node appended its id to the executing actor's outbox. Nothing raised: delivery found the queue entry, could not read the activity, logged "not found in DataLayer", and skipped — so the invitee was never told it had been invited and the case stalled with no error anywhere (#2548). Rebinding is opt-in so that a stateless collaborator is not swapped out behind its caller's back, and the opt-in must be discoverable on the type rather than the instance, because a test double answers any attribute.
Refines: DL-07-005
Implements: PCR-08-007

Storage Fidelity for Inline-Required References

Persistence dehydrates AS2 reference fields to their id to avoid storing a nested object twice. Some references must not be dehydrated, because the protocol requires the object to be carried rather than referenced and nothing else ever gives it a record to be read back from. Added by ISSUE-2482, where the round-trip silently emptied a Create(CaseProposal) of its report.

ID Priority Requirement Related
DL-08-001 MUST NOT Persistence MUST NOT dehydrate a reference field to its id when the declaring model requires that field to carry the object inline. A model MUST be able to declare such fields, and the storage layer MUST consult that declaration rather than enumerating the affected types itself.

Dehydration is only reversible when the nested object has a record of its own, and inbound activities are given records only one level deep — so a second-level inline object collapses to an id that no read can expand. CP-01-004 requires as_CaseProposal to carry its as_VulnerabilityReport inline for exactly the reason that makes the collapse unrecoverable: the receiver may not hold the report and this codebase does not yet dereference (AKM-03-003). The declaration belongs on the model because that is where the invariant is, and because a lookup table in the storage layer is action at a distance from the field it governs.
Refines: CP-01-004
Refines: AKM-03-001
DL-08-002 MUST A type annotation MUST NOT be relied on as the record of an inline-only requirement when that annotation also admits a URI string.

ActivityStreamRequiredRef[T] expands to T | as_Link | str, so a bare URI type-checks and model_validate accepts a dehydrated object without complaint. CP-01-004 was therefore recorded only in prose, and the storage layer had nothing to consult; the resulting collapse surfaced as three unrelated "best-effort" skips several layers away, none of which named the cause.
Refines: DL-08-001
DL-08-003 MUST An activity whose reference field names another actor MUST declare that field inline-required.

Under ADR-0073 an actor's record lives in its own store, so a peer named by an outbound activity has no record in the sending actor's store and never will — actor discovery is not implemented (ADR-0024). Dehydration is only reversible against such a record (DL-08-001), so collapsing a peer actor to its id is unrecoverable by construction rather than by accident of write order. Four activities name a peer: _RmInviteToCaseActivity and _RecommendActorActivity in object_, and _OfferCaseParticipantRoleActivity and _OfferCaseOwnershipTransferActivity in target — the role offer also declares its object_, whose CaseParticipantRole the factory mints inline and no path persists. Each factory wraps a peer named by a bare id as an as_Actor, so the peer travels inline. Every other required typed reference in the vocabulary is either an activity, kept inline already, or a case, report, status, participant or embargo, which the sender does hold. A received activity is stored as the parser's base class, so storage also honours the declaration of the semantic class the activity matches: the CASE_MANAGER holds no record of an ownership transferee, and its ledger snapshot must carry the transferee inline (CLP-07-006). Left undeclared, the stored Invite came back carrying a bare string, outbox delivery refused it for AKM-03-003 and gave up after its retries, and the failure surfaced one hop away as the invitee answering 404 to reject-case-invite for an invitation it had never been told about. Carrying the object is necessary but not sufficient: read-back must also re-type it, or semantic matching classifies the activity UNKNOWN and the receiver has nothing to dispatch on — which is why the re-typing set (_inline_typed_fields in db_record.py) is wider than the dehydration set and includes context.
Refines: DL-08-001
Refines: AKM-03-001

Embargo Management Behavioral Requirements

Event-Condition-Action behavioral requirements for Embargo Management (EM). Each group is triggered by either a received EM message or a local EM state transition, and specifies typed preconditions, ordered steps, and postconditions that implementations must satisfy.

File ID: EMB

EM Write-Boundary Consolidation

Requirements ensuring that all EM state transitions are executed through the EmbargoLifecycle service layer rather than written directly to case.current_status.em in BT nodes or helpers. Identified by audit in CONCERN-2412: three BT nodes (EmActivateEmbargo, ClearEmbargo, SetDefaultActiveEmbargoNode) bypass EmbargoLifecycle with direct EmDimension assignment. Complements SM-04-001 (general write-boundary guard requirement) with an EM-specific routing rule.

ID Priority Requirement Related
EMB-18-001 MUST Every change to a case's embargo register, and so to the EM state derived from it (ADR-0122), MUST be made through EmbargoLifecycle (its public operations such as propose_embargo(), activate_embargo(), and reject_embargo_proposal(), each of which applies one register step), or, for an act converted to the copy model, by applying the act's ledger entry through the one apply function, apply_embargo_change() (ADR-0124, CM-23-016). BT nodes and helper functions MUST NOT otherwise apply a register step to the case themselves, or assign case.current_status.em, and then call dl.save(case) as a substitute for routing through one of those two.

EmbargoLifecycle and the one apply function are the only embargo writers; the copy model moves each act to the second as it comes to be committed before it is applied (#4294). Bypassing both skips the fail-closed reads of EMB-18-003, the PEC cascade and the shared starting-state and EM checks, and a status EM assigned directly is overwritten by the register's derived value. Source: CONCERN-2412.
Refines: SM-04-001
EMB-18-002 MUST NOT A BT node or helper MUST NOT check an embargo register step as a non-blocking warning-only guard and then proceed to write regardless of the result. A refused register step MUST either raise or cause the node to return Status.FAILURE without writing to the DataLayer.

Warning-only guards create the illusion of validation while allowing invalid states to be persisted. The correct pattern is to route through EmbargoLifecycle (EMB-18-001), which enforces fail-closed behavior in STRICT mode. Source: CONCERN-2412.
Refines: EMB-18-001
EMB-18-003 MUST A path that activates an embargo register entry MUST already hold that EmbargoEvent's record in the same store. The EmbargoLifecycle activation writers (activate_embargo(), initialize_creation_embargo()) MUST read the record of the embargo being activated, as well as the one it replaces (EP-05-001), and MUST fail closed on an unreadable one before changing the register. An unreadable replaced embargo is a broken invariant, reported as a refusal, not deferred.

The EP-05-001 comparison reads the replaced embargo, so it holds only if no case names an embargo its store cannot read. The ledger applies an embargo's proposal before any activation that replaces it, but any writer that accepts a bare id (an activation writer, or seeding a case whose embargo arrived as a bare reference) lets a case name an unreadable record. A deferral would wait for a record no path delivers. Source: CONCERN-4004.
Refines: EMB-18-001
Constrains: EP-05-001
Depends on: AKM-05-003
EMB-18-006 MUST The EM transitions of EMB-18-005 MUST be held as one declarative table, and applying an embargo register step MUST refuse, with no write, a step whose EM move is not in it. The CASE_MANAGER and every replica MUST apply the same check, as part of the one apply function (ADR-0124).

A check only in tests protects only the register states a test reaches. Refusing at the step makes a register defect fail when it happens, on every node, and gives tools that draw or export the EM machine one table to read (ADR-0130).
Refines: EMB-18-005

Embargo Policy

Requirements for a standardized embargo policy record associated with an Actor profile. Allows actors to declare their default embargo preferences so that coordinators can evaluate compatibility before proposing an embargo or inviting an actor to a case.

File ID: EP

Embargo Negotiation Relays Through the CASE_MANAGER

How an embargo proposal — the first one for a case or a revision of the active embargo — reaches every participant and every replica in the CASE_MANAGER topology. The behavioural specs (EMB-01 through EMB-06) are written in the voice of the formal protocol, in which every Participant is a peer and every Participant "receives EP" or "receives EV". Here a participant addresses its proposal to the CASE_MANAGER alone (PCR-08-001), so the Participant receiving EP or EV is the CASE_MANAGER on receipt, and every other participant learns the resulting case state from the ledger. The protocol interactions the behavioural specs define still happen: the CASE_MANAGER relays the proposal to the participants as an Invite, they answer the CASE_MANAGER, and the owner's answer decides. Each step is recorded in the ledger. Ledger entries carry case state to replicas and set no parse-and-respond expectation; they augment the negotiation as the record of the case, they do not replace it. Decided by ADR-0113 from Concerns #3892, #3836 and #3863 for revisions; generalised to every proposal, with the acknowledgement, RSVP-deadline, trigger-side write and invitee rules below, by its 2026-09-30 amendment from Concern #3918.

ID Priority Requirement Related
EP-09-007 MUST Every ledger semantic type the revision relay commits (the proposal, each Invite emission, each participant's consent answer, each consent row the CASE_MANAGER changes, and the owner's decision) MUST have a replica apply node reachable from create_announce_log_entry_tree (RSH-08-004) that applies what the entry carries by copying it through the one apply function (ADR-0124, CM-23-016): the proposal's entry is its PROPOSE register step, an answer's entry carries the row the answer states (ADR-0122), and every other consent row the proposal or a relayed Invite changes is the CASE_MANAGER's own Create or Update(EmbargoConsent) entry that follows it. The replay node for a transition MUST land before the received-side effect that wrote it directly is gated (RSH-08-003).

Replicas take state from the ledger, so a semantic type with no replay node is a state change no replica can see (ADR-0108). Before this rule the only embargo event with a replica effect was teardown; proposal, revision and activation entries were stored and ignored, which is why a replica's EM stayed ACTIVE through a whole revision cycle (#3892). Covering every semantic type of the relay is what makes the participant-side embargo trees gateable with no further work.
Refines: RSH-08-004
Depends on: EMB-18-001

Event-Driven Control Flow

Defines the event-driven processing model for Vultron actors: how primary events enter the system, how cascading consequences are produced automatically, and what constraints apply to demo scripts and integration tests.

File ID: EDF

Definitions

ID Priority Requirement Related
EDF-01-001 MUST A primary event is an externally supplied stimulus or an actor-initiated decision: a message delivered to an actor's inbox, or a trigger invocation representing an actor choosing to begin an action.
EDF-01-002 MUST A cascade is an automatic, BT-driven consequence that occurs within a single actor's execution context as a result of processing a primary event or an earlier cascade step.
EDF-01-003 MUST A cascade chain is the full sequence of causally connected steps that flows from one primary event: within-actor cascade → outbox emission → delivery to peer inbox → peer's own cascade → and so on.

Event-Driven Processing

ID Priority Requirement Related
EDF-02-001 MUST Every protocol-significant action in Vultron MUST be triggered by an event, not by direct procedural calls from upstream code.
EDF-02-002 MUST The system MUST follow an "on event, do" model: business logic is invoked in response to events; events are not invoked by business logic.
EDF-02-003 MUST NOT The inbox MUST be the sole entry point for inbound activity processing; handlers MUST NOT be invoked directly by callers that bypass the inbox dispatch pipeline.
EDF-02-004 MUST NOT The outbox MUST be the sole exit point for outbound activity delivery; handlers MUST NOT deliver activities directly to peer actors.

Cascade Implementation

ID Priority Requirement Related
EDF-03-005 MUST NOT Cascade logic MUST NOT be duplicated between actors; each actor's BT handles its own local cascade, triggered by receipt of the peer's outgoing activity.

External Decision Nodes

ID Priority Requirement Related
EDF-04-001 MUST When a cascade reaches a point where the actor cannot proceed autonomously, the stopping point MUST be represented as an explicit external decision node in the BT.
EDF-04-003 MUST The arrival of the required external input (a trigger invocation, an inbox message, or a UI response) MUST cause a fresh evaluation that determines where the exchange stands and proceeds accordingly.

Amended by ADR-0080. The previous form required the awaiting node to re-evaluate "on the next BT tick", which presumes the node survived to be ticked again; it does not. The arriving input drives a new evaluation whose first act is to determine the state of the exchange (ASK-02-001).

Causal Gating in Demo Scenarios

ID Priority Requirement Related
EDF-06-001 MUST A harness step that depends on an asynchronous effect MUST be gated on positive evidence that the effect has been committed by the actor that produces it. Elapsed time, the step's position in the script, and the return of an HTTP 202 acknowledgement MUST NOT be treated as such evidence.

Sequencing a scenario temporally ("A and then B") rather than causally ("A therefore B") makes every step a race against asynchronous delivery. This was the shared root cause of a recurring family of demo failures under Epic

Handler Protocol

Handler use-case classes process VultronEvent domain objects and implement protocol business logic. All handlers follow a common contract defined by the UseCase Protocol.

File ID: HP

Handler Signature

ID Priority Requirement Related
HP-01-007 MUST Every received use case MUST declare its sender entitlement exactly once, as either a sender rule (HP-01-008) or an exemption carrying a reason, and the receive-tree factory MUST compose the matching guard from the single sender-entitlement module into the tree's guard stage. A handler MUST NOT implement its own sender predicate, and the module MUST NOT have a twin.

Sender checks already existed in seven near-duplicate forms across behaviors/ and use_cases/ before this rule, and the handlers that lacked one were found only by a manual audit. One declaration read by the factory and by a ratchet makes a missing check a test failure and makes the remaining gaps a visible exemption list. See ADR-0115.
Refines: HP-01-006
HP-01-008 MUST A declared sender entitlement MUST state the full rule the receiver applies: it MUST be composed of entitlement kinds, MAY require any one of several kinds, and MUST state the standing required for each addressee the activity is sent to: the CASE_MANAGER, a participant, or, for an activity sent before a case exists, an actor outside any case. The receive-tree factory MUST compose the guard from the declaration alone, and a tree MUST NOT add a sender check narrower than its declaration.

When this was written, Remove(Note) declared "an active participant" while CM-30-001 entitles only the note's author or the Case Owner, and the embargo Invite declared one kind while its check is two-sided; the full rules lived inside the trees. A declaration that is only a floor is a second sender predicate in disguise, and anything derived from it, such as the occasion table's "Sent by" (MSM-08-005), understates who may send. See ADR-0129.
Refines: HP-01-007
HP-01-009 MUST The entitlement kinds MUST be the closed vocabulary of sender standings, covering both roles a participant holds and positions held within one exchange (such as the invitee, the addressee of a request and the author of an object). A position MUST NOT be added to CVDRole, and a position a new rule needs MUST be added as an entitlement kind. The addressee of a request MUST be one kind whatever the request, except that the invitee of a recorded Invite is its own kind.

A CVDRole value is a durable fact stored on a participant record, while a position is held relative to one exchange; the same coordinator receives one report and sends the next, and a report precedes any case. Keeping positions as kinds means a position named in the docs is always one a receiver enforces. One addressee kind keeps one guard per check mechanism; the request it names is supplied when it is worded (MSM-08-010). The invitee is checked against the recorded Invite itself rather than against an addressee the receiver recorded, and is the protocol's established term (EP-09-010), so it stays a kind of its own. See ADR-0129.
Refines: HP-01-007
HP-01-012 MUST The receive-tree factory MUST place the HP-01-005 addressee check in every received tree it builds, after intake and the post-close boundary and before the sender guard, and every check of whether an activity is addressed to the receiver MUST read one shared definition: the receiver sent it, is named in to, or is an active participant (CM-10-004) of a case whose participants collection to names. An Offer(Report)'s case-creation check reads its to part alone (HP-09-001). A tree MAY leave the check out only by naming a registered exemption that states why, and MUST NOT write its own copy of the check.

HP-01-005 binds every handler, but the check was opt-in per tree: only the embargo trees asked for it, the others did not check at all, and one use case checked in Python ahead of its tree. A check written once in the factory cannot be forgotten, and a closed, reasoned exemption list makes each tree that differs a visible decision (CS-22-001).
Implements: HP-01-005
Refines: CLP-10-010

Semantic Verification

ID Priority Requirement Related
HP-02-001 MUST All handlers MUST have semantic type verification before execution Depends on: DR-01-003
HP-02-002 MUST The verification mechanism MUST check that the activity's semantic type matches the handler's expected type

Prevents routing errors where wrong handler processes an activity
Refines: DR-01-003

Handler Registration

ID Priority Requirement Related
HP-03-001 MUST All handlers MUST be discoverable via a handler registry mechanism
HP-03-002 MUST Registry keys MUST match handler semantic verification types Refines: DR-02-002

Error Handling

ID Priority Requirement Related
HP-05-001 MUST Handlers MUST raise exceptions for unrecoverable errors
HP-05-002 MUST Handlers MUST log expected errors without raising exceptions

Idempotency

ID Priority Requirement Related
HP-07-001 SHOULD Handlers SHOULD be idempotent to support retries Depends on: ID-04-001
HP-07-002 MUST Handlers MUST complete execution within 30 seconds

Prevents indefinite blocking of background task queue

Idempotency

Requirements for handling duplicate activity submissions gracefully, ensuring repeated identical requests produce the same outcome without unintended side effects.

File ID: ID

Activity ID Uniqueness

ID Priority Requirement Related
ID-01-002 MUST Activity IDs MUST remain stable (same activity = same ID)

Duplicate Detection

ID Priority Requirement Related
ID-02-001 MUST System MUST detect duplicate activity submissions by ID

Detection lives at ingress (IE-10-001): the receiver already holds the activity under a key it derives from the id, so the redelivered body is not held again and the copy the receiver holds is routed in its place. It does not live at the HTTP route. A route-level check keyed on the actor record's inbox collection was a production no-op, because the core actor's inbox is a URI string (ARCH-12-006), and its tests passed only by supplying a wire actor shape the route never receives (#3638).
ID-02-002 SHOULD System SHOULD track processed activity IDs in DataLayer
ID-02-003 MUST Detection MUST occur before handler business logic execution

Detection precedes dispatch, but does not replace it: the activity is still routed to the handler, so the handler's own idempotency (ID-04-004, ID-05-003) is what keeps a redelivery from producing a second side effect. Detection is the evidence; handler idempotency is the guarantee.

Idempotent Response

ID Priority Requirement Related
ID-03-001 MUST Duplicate submissions MUST return same HTTP status as original
ID-03-002 MUST NOT Duplicate detection MUST NOT delay response beyond 100ms
ID-03-003 SHOULD System SHOULD log duplicate submissions at INFO level

A redelivery is ordinary ActivityPub behaviour — a peer retry after a lost acknowledgement — not a fault the operator must act on, so it is a lifecycle event, not a WARNING (SL-03-001). The ingress adapter already logs the redelivery it detects at INFO with the activity and actor ids (IE-10-001, #3638). A different body under a held id is not a redelivery: it is refused and logged at WARNING (IE-10-002).
Depends on: SL-03-001

Handler Idempotency

ID Priority Requirement Related
ID-04-001 SHOULD Handlers SHOULD be idempotent (same input → same output)
ID-04-002 SHOULD Handlers SHOULD check existing state before state transitions Implements: VP-03-013
ID-04-003 SHOULD Handlers SHOULD log attempted re-execution at INFO level
ID-04-004 MUST State-changing handlers MUST be idempotent

Duplicate processing of state-change messages causes data corruption; SHOULD-level idempotency is insufficient for state machines
ID-04-005 MUST A record that a handler later reads as evidence that a transition already happened — an idempotency latch or guard record — MUST NOT be written until every other half of that transition has succeeded. A handler MUST NOT write the latch first and then attempt the remaining work.

A latch written ahead of the transition it attests to is indistinguishable from a completed transition, so the guard that reads it suppresses every retry. The transition can then never finish: the failure is silent, permanent, and self-concealing. Issue #2548 is the worked example — TransitionRMtoValid wrote the report-phase RM.VALID ParticipantStatus before advancing the case-scoped CaseParticipant. When the case was absent the second half was skipped, but CheckRMStateValid read the latch and short-circuited every subsequent validate-report to SUCCESS, freezing the participant at RM.RECEIVED for the life of the case. Ordering the writes so the latch is last makes an incomplete transition retryable instead of terminal.
Refines: ID-04-004
Refines: ARCH-15-001

Implementation Strategy

ID Priority Requirement Related
ID-05-001 SHOULD Validation layer SHOULD reject exact duplicates (same ID, same content)

"Reject" applies to the redelivered body, not the delivery: ingress finds the id already held under the receiver's key and does not hold the body again (IE-10-001), while the activity is still routed to the handler and the response is the same 202 as the original (ID-03-001). Rejecting the body is therefore not what prevents a second side effect; the handler's own idempotency is (ID-02-003 rationale, ID-04-004, ID-05-003).
Depends on: IE-10-001
ID-05-002 MAY HTTP layer MAY implement early duplicate filtering for performance
ID-05-003 SHOULD Use-case handlers SHOULD verify that an incoming activity has not already been applied by inspecting the current domain state before executing any transitions, serving as the final idempotency guard when earlier filtering layers have not already rejected the duplicate.

Inbox Endpoint

The inbox endpoint is the primary entry point for actor-to-actor communication in Vultron. It receives ActivityStreams activities via HTTP POST, validates them, and queues them for asynchronous processing.

File ID: IE

Endpoint Configuration

ID Priority Requirement Related
IE-02-004 MUST NOT The inbox path MUST NOT expose an actor's received activities, as a collection or as a list of ids. What an actor has received is an operator question, answered by the datalayer router (/actors/{id}/datalayer/...) over the actor's own store; a sender learns the disposition of what it posted from the reply activities delivered to its own inbox, never by reading the recipient's.

Ingress already stores the received activity under its type, so refusing a read loses nothing. The DataLayer inbox queue holds only deferred or re-queued deliveries (bootstrap replay, retries) and is drained as they are processed: a processing queue, not a record of receipt. The core actor's inbox is a URI string (ARCH-12-006), so no receipt bookkeeping may hang off the actor record. A stub read over the drained queue reported an empty inbox after each successful delivery (#3141).
Depends on: IE-02-003
Depends on: ARCH-12-006
Depends on: DL-07-001

Message Validation

Requirements for validating ActivityStreams 2.0 activities at the inbox handler boundary, covering ActivityStreams structure and semantic validation. Invalid activities are rejected with appropriate error responses before dispatch.

File ID: MV

Inbound Unknown-Key Disposition

ID Priority Requirement Related
MV-11-004 MUST Setting inbound unknown keys aside at the parse edge MUST NOT loosen extra="forbid" on any core type (ARCH-12-003). The parse edge is the only door inbound data enters by, so a core class never sees an inbound unknown key; an unknown key on in-process or stored data remains a refusal. The retired-name list MV-11-002 consults MUST live in the wire layer and be consulted only at the parse edge; core types MUST NOT carry a per-class retired-key reject-guard (SDO-03-005).

The two rules answer different questions. extra="forbid" asks "did this process build or store an object with a key it does not know?" — always a bug. The parse edge asks "did a peer send a property we do not read?" — usually an extension. Keeping them separate is what lets the receive side be liberal without reopening the #2232 hole on the stored side. ParticipantStatus._reject_retired_vfd_keys predated this rule and was the one guard SDO-03-005 already forbade; #3921 deleted it when the wire-side list arrived.
Derives from: ARCH-12-003
Refines: SDO-03-005
MV-11-005 MUST NOT After parse_activity returns, the receive path MUST NOT re-validate an inline object from the raw request body. The parsed object graph is the authority for what arrived in each slot; the raw body is received evidence, kept for audit, not a second source to parse from.

A re-parse from the raw body sees the keys the parse edge set aside and, on a core class, fails on them. inbox_storage._reparse_as_specific_type did exactly that and fell back to a base class on failure with a DEBUG line, so once keys were set aside it would have stored the wrong class silently; #3922 deleted it. Its purpose — recovering the specific class when the parser had validated an inline object as the base type — ended when the parser began resolving inline objects to their specific class itself (MV-04-003).
Depends on: MV-11-003
Depends on: MV-04-003

Observability

Requirements for health check endpoints and high-level observability architecture, enabling operators to understand system behavior, diagnose issues, and monitor health.

File ID: OB

Health Checks

ID Priority Requirement Related
OB-05-001 MUST The system MUST provide liveness endpoint at /health/live
OB-05-002 MUST The system MUST provide readiness endpoint at /health/ready

Outbox

Requirements for actor outboxes, which hold response activities generated by handlers. Covers outbox structure, population, delivery, local and federated dispatch, idempotency, outbound reference dehydration, and outbox addressing rules.

File ID: OX

Undelivered-Activity Correlation

Requirements linking a permanently undelivered activity back to the canonical case ledger entry that recorded the event the activity carries, so that divergence between the canonical log and a participant's replica is observable rather than silent. Derived from CONCERN-2657 and ADR-0080.

ID Priority Requirement Related
OX-14-001 MUST A dead-lettered activity MUST be correlatable to the canonical CaseLedgerEntry whose event it carries, where such an entry exists.

CONCERN-2657 observed that a permanently undelivered activity leaves the canonical log asserting an event a participant never received. The defect is not the write ordering (see OX-14-002) but that nothing connects the dead-letter store (OX-13-008) to the entry claiming the event happened, so replica divergence is undetectable.
OX-14-003 MUST Exhaustion of an activity's delivery budget MUST produce an observable that names the recipients that never received it and the canonical entry the activity was replicating, where one exists.

A participant whose replica is missing an entry cannot ask for it if it does not know the entry exists; naming the unreached recipients is what makes targeted replay (SBT-03-002) possible instead of a full resynchronisation.

Protocol Asks and Outstanding-Ask Registers

Requirements for the protocol-ask primitive: how an actor requests something it lacks authority to do, how outstanding asks are recorded and enumerated in both directions, how asks expire, and how a receiver reports that it could not process an inbound message. Derived from ADR-0080 and the G01 planning session (CONCERN-2829).

File ID: ASK

Ask and Terminate

An ask is an ordinary protocol message that completes the asking behavior. Nothing suspends awaiting a reply.

ID Priority Requirement Related
ASK-01-002 MUST A behavior that emits an ask MUST terminate with SUCCESS on the strength of having emitted it; SUCCESS at an ask point means the ask was emitted and MUST NOT be read as authorization having been granted.


*The behavior is divided at the question rather than paused there, which is what makes an asynchronous exchange hostable in a single-shot executor.
*
ASK-01-003 MUST NOT A BT node MUST NOT return RUNNING to represent waiting for an external reply.


*BTBridge.execute_tree treats root RUNNING as "continue ticking", exhausts its iteration budget, logs ERROR and returns FAILURE, then discards the tree; no tree survives an invocation, so no node can be resumed.
*

Conversation-State Routing

A gated interaction is one tree that determines where the conversation stands before taking any action.

ID Priority Requirement Related
ASK-02-001 MUST A tree implementing a gated interaction MUST determine the state of the ask/reply exchange before executing any branch that produces a protocol effect.


*State-first routing is what makes repeated entry safe without relying on per-node duplicate detection.
*
ASK-02-002 MUST The branches of a gated interaction tree MUST be mutually exclusive, such that at most one branch is applicable for any state of the exchange.


*Disjoint branches mean a later entry does not re-attempt work an earlier entry completed, so no idempotency guard is required to abort it.
*

Outstanding-Ask Register

A durable, actor-scoped record of asks outstanding in both directions.

ID Priority Requirement Related
ASK-04-001 MUST An actor MUST record an entry in its outstanding-ask register when it emits an ask, and MUST record an entry when it receives an ask addressed to it.


*The asker needs the entry to avoid re-asking and to notice expiry; the answerer's entries are the queue of decisions it owes, which is the surface a human, UI, or agent acts through.
*
ASK-04-002 MUST An outstanding-ask register entry MUST be removed when a reply that closes its ask is recorded.


*Presence in the register means outstanding, which keeps the working set small and the invariant checkable; history lives in the case ledger.
*
ASK-04-003 MUST The outstanding-ask register MUST be durable across process restarts.


*An in-memory register would orphan every outstanding ask on restart, producing a permanent stall worse than the one the register prevents.
*
ASK-04-004 MUST The outstanding-ask register MUST be actor-scoped rather than case-scoped, so that asks exchanged before a case exists are recorded.


*Pre-case handshakes such as Create(CaseProposal) have no case ledger to derive state from; VultronOfferRecord already demonstrates the actor-scoped form.
*
ASK-04-005 MUST The outstanding-ask register MUST support enumeration of an actor's outstanding asks in each direction, without the caller supplying the identity of a specific ask.


*Reaping deadlines and presenting a decision queue both require listing entries; a keyed query cannot answer "what am I waiting on".
*
ASK-04-006 MUST The outstanding-ask register MUST be reconstructible from the case ledger for case-scoped asks, and the ledger MUST prevail where the two disagree.


*A register that cannot be rebuilt makes lost entries unrecoverable; the same index-plus-authority division governs actor_participant_index.
*
ASK-04-007 MUST The create, close-on-named-event, and time-out lifecycle shared by outstanding-ask tracking and pending-assertion suppression MUST be implemented once as a reusable register, instantiated separately for each use.


*Both uses already share ProtocolPair as their key type and both need reaping and enumeration; two implementations of one lifecycle drift.
* Refines: CLP-11-002
ASK-04-008 MUST Registration of an emitted ask MUST occur on the single shared emit path, so that no call site can emit an ask without recording it.


*Registration that each call site must remember to perform is registration that will be forgotten; the wire factory cannot do it because the wire layer has no DataLayer access.
*
ASK-04-009 MUST An actor MUST expose its outstanding asks as a dereferenceable ActivityStreams collection in each direction.


*A collection is the ActivityStreams idiom for the set of things belonging to an actor, so no new activity verb is needed to answer "what decisions do I owe"; the answerer already holds the underlying data in its ledger replica.
*

Expiry and Reaping

How an expired ask is noticed, given that elapsed time is not an event.

ID Priority Requirement Related
ASK-05-001 MUST A gated interaction tree MUST treat an outstanding ask past its deadline as expired at the next entry into the tree, without requiring a prior reaping pass.


*Time passing delivers no message, so opportunistic evaluation is the only mechanism guaranteed to run; a peer's own retry is frequently the clock.
*
ASK-05-003 MUST NOT Reaping expired asks MUST NOT be implemented as a periodic loop inside core.


*A timer-driven reaper inside core makes expiry behaviour observable only by waiting, which EDF-06-001 forbids treating as evidence, and misplaces a call-in concern.
*

State Machine

Requirements governing how the RM (Report Management), EM (Embargo Management), and CS (CVD Case State: VFD and PXA axes) protocol state machines are defined, represented, and executed at runtime. Each participant maintains its own RM state; the CASE_MANAGER maintains shared EM and Case State. Within CS, the PXA axis is shared across participants while the VFD axis is per-participant.

File ID: SM

Runtime State Changes

ID Priority Requirement Related
SM-04-001 MUST Runtime state changes MUST be guarded by an explicit precondition check before the state value is written

The precondition check MUST be contextually appropriate — it verifies that the intended transition is valid for the specific domain situation, not merely that it passes a strict adjacency test. For example, when the CASE_MANAGER sets a reporter participant to RM.ACCEPTED at bootstrap time, the guard does not check strict RM adjacency (the reporter's intermediate states were never observed and MUST NOT be inserted into the ledger); it checks instead that the transition is a legitimate bootstrap-forward move — which is valid because a report submitter can only be at RM.ACCEPTED at the time of submission (see CBT-01-008). Treating is_valid_rm_transition as the only acceptable precondition form would block valid bootstrap paths while blocking invalid state bypasses is the actual goal.
SM-04-002 MUST State-changing handlers MUST be idempotent with respect to repeated application of the same transition Implements: HP-07-001 — handler-protocol.md

State History

ID Priority Requirement Related
SM-05-001 MUST Persisted state for both case-level and participant-level axes MUST use an append-only history list
SM-05-004 SHOULD The case state log SHOULD store references (IDs) to entries in the main case history log rather than duplicating full record content

Case Event Log Synchronization

Requirements for the append-only canonical recorded case ledger, replication transport, conflict handling, per-peer state tracking, and retry semantics for distributed case state synchronization across Participant Actors.

File ID: SYNC

Ledger Sync Phase Concepts

ID Priority Requirement Related
SYNC-00-001 MUST The AppendOnlyLedger phase MUST establish the local append-only case ledger with hash-chain indexing.
SYNC-00-005 MUST Each entry in the AppendOnlyLedger MUST be immutable once committed.

Immutability is the foundation of hash-chain integrity; a mutable entry would invalidate all successor entries and undermine replay determinism.
SYNC-00-006 MUST Each entry in the AppendOnlyLedger MUST be cryptographically linked to its predecessor via a content hash, providing the integrity foundation for all subsequent replication phases.

The hash link makes tampering detectable; any modification to an entry breaks the chain from that point forward, allowing receivers to detect corruption during replication.
SYNC-00-002 MUST The LedgerFanout phase MUST implement one-way replication from the actor holding the CASE_MANAGER role to all Participant Actors via Announce(CaseLedgerEntry) messages. A participant replica MUST be treated as synchronized when and only when its log tail hash matches the CASE_MANAGER's authoritative tail hash.
SYNC-00-003 MUST The LedgerReconciliation phase MUST extend LedgerFanout with a full synchronisation loop, retry, and backoff.
SYNC-00-007 MUST When a participant's replica falls behind, the LedgerReconciliation mechanism MUST detect the gap.

Gap detection is the precondition for triggered retry; without it, a lagging replica would remain silently diverged.
SYNC-00-008 MUST When a participant's replica falls behind, the LedgerReconciliation mechanism MUST retry entry delivery until the participant's tail hash converges to the CASE_MANAGER's tail hash.

Convergence to the authoritative tail hash is the definition of a synchronised replica; retrying until convergence ensures eventual consistency.
SYNC-00-004 SHOULD The PeerLedgerSync phase SHOULD enable multi-peer synchronisation between actors with equal standing. Where LedgerFanout assumes one actor holds CASE_MANAGER authority, PeerLedgerSync SHOULD allow two or more actors to reconcile their ledgers with each other to support federated and ownership-transfer scenarios.

Vultron Protocol

Requirements extracted from the Vultron Protocol documentation, covering participant state tracking, Report Management (RM), Embargo Management (EM), CVD Case State (CS) messaging, model interactions, and implementation guidance. This is the normative source for protocol behavior requirements that implementations must satisfy.

File ID: VP

Implementation Guidance

ID Priority Requirement Related
VP-15-001 SHOULD Vultron Protocol messages SHOULD use well-defined format specifications (e.g., JSON Schema, protobuf, XSD)
VP-15-002 SHOULD Vultron Protocol implementations SHOULD use common API patterns (e.g., REST, WebSockets) for inter-participant communication
VP-15-003 SHOULD Vultron Protocol implementations SHOULD use transport-layer encryption to protect sensitive data in transit
VP-15-005 MAY Vultron Protocol implementations MAY use encryption to protect sensitive data at rest