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
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
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
Driven Adapter Injection
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
Fail-Fast Domain Objects
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
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
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
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
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
Event-Driven Processing
Cascade Implementation
External Decision Nodes
Causal Gating in Demo Scenarios
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
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.
Protocol Asks and Outstanding-Ask Registers
Requirements for the protocol-ask primitive: how an actor requests something it lacks authority to do, how outstanding asks are recorded and enumerated in both directions, how asks expire, and how a receiver reports that it could not process an inbound message. Derived from ADR-0080 and the G01 planning session (CONCERN-2829).
File ID: ASK
Ask and Terminate
An ask is an ordinary protocol message that completes the asking behavior. Nothing suspends awaiting a reply.
Conversation-State Routing
A gated interaction is one tree that determines where the conversation stands before taking any action.
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.
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
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
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