vacant.registry¶
P4 registry — SQLite-backed halo store, RPC endpoints, halo
aggregation (per-vacant, not routed-through), anti-tamper, and the
Visibility axis that decides whether a halo is in the public index.
halo
¶
Halo emission — per-vacant CapabilityCard publication + revocation.
Per THEORY_V5 §7.1 (Registry ontology), each vacant carries its own capability card; the registry stores a signed copy plus index entries so direct vacant-to-vacant calls can bypass it. LOCAL-state vacants are not stored centrally (visibility=NONE).
HaloRecord
dataclass
¶
HaloRecord(vacant_id: str, visibility: Visibility, event_seq: int, capability_card_hash: bytes)
Result of a successful publish_halo.
RevocationRecord
dataclass
¶
Result of a successful revoke_halo.
RegisterEventDraftInputs
dataclass
¶
RegisterEventDraftInputs(vacant_id: str, capability_card_hash: bytes, halo_version: int, visibility: Visibility, ts_ms: int, actor_seq: int, idempotency_key: str)
Bag of inputs that name a single register event draft.
Both the client (CLI publishing over HTTP) and the server (HTTP handler verifying the request) construct the same canonical bytes from these fields, so the signature is bit-stable between sides.
publish_halo
async
¶
publish_halo(*, store: RegistryStore, card: CapabilityCard, runtime_state: VacantState, signing_key: SigningKey, base_model: str | None = None, base_model_family: str | None = None, owner_org: str | None = None, declared_capabilities: list[str] | None = None, parent_id: str | None = None, version: str | None = None, visibility: Visibility = PUBLIC) -> HaloRecord
Insert / update a vacant's halo + emit a register event.
Visibility rules (D006 §G + dispatch §"Visibility"):
- LOCAL state forces Visibility.NONE regardless of visibility.
- LOCAL halos are stored (so owner/parent direct lookup works) but
effective_visibility returns NONE → discovery filters them out.
Republish kwargs policy (Pfix3 F2): base_model,
base_model_family, owner_org, version are
None-default. On new publish, they fall back to defaults
("unknown" / "0.0.1" / None). On republish, a
None argument means "leave the existing column untouched";
only non-None arguments overwrite the row. This avoids
accidentally clobbering version="0.5.0" with the default
"0.0.1" when a caller republishes only to flip visibility.
Card-derived columns (capability_card_*,
declared_capabilities_json, visibility) always overwrite
on republish — they are intrinsic to the new card.
Source code in src/vacant/registry/halo.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | |
register_event_payload
¶
register_event_payload(inp: RegisterEventDraftInputs) -> dict[str, object]
Canonical payload dict the register event carries.
Mirrors the in-process publish_halo payload (lines above) so
HTTP-published rows produce the same audit footprint as direct
calls.
Source code in src/vacant/registry/halo.py
register_event_canonical_bytes
¶
register_event_canonical_bytes(inp: RegisterEventDraftInputs, *, signed_by_pubkey: bytes) -> bytes
Canonical Ed25519-signing payload for a register event.
The CLI publishes via HTTP by computing this byte-string, signing
it under the vacant's own key, and POSTing card_blob + the
signature to /v1/halo. The server reconstructs the same bytes
and verifies before letting the row land in the audit chain.
Source code in src/vacant/registry/halo.py
publish_halo_signed
async
¶
publish_halo_signed(*, store: RegistryStore, card: CapabilityCard, runtime_state: VacantState, visibility: Visibility = PUBLIC, base_model: str | None = None, base_model_family: str | None = None, owner_org: str | None = None, declared_capabilities: list[str] | None = None, parent_id: str | None = None, version: str | None = None, event_ts_ms: int, event_actor_seq: int, event_idempotency_key: str, event_signature: bytes) -> HaloRecord
HTTP-friendly variant of publish_halo: the caller pre-signs the
register event so the registry never needs the vacant's private key.
Server-side flow:
- Verify the capability card's own signature.
- Insert the vacant row if missing (so
submit_event's actor lookup can succeed). - Submit the pre-signed register event via
store.submit_event, which re-runs L1 signature verification + L2 sequence check.
Republish kwargs policy mirrors publish_halo (Pfix3 F2):
None arguments leave the existing column untouched on
republish; only non-None values overwrite. New publishes fall
back to "unknown" / "0.0.1" defaults.
Source code in src/vacant/registry/halo.py
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 | |
revoke_halo
async
¶
revoke_halo(*, store: RegistryStore, vacant_id: str, reason: str, signing_key: SigningKey, pubkey_bytes: bytes) -> RevocationRecord
Mark a vacant as revoked — emit a signed revoke event and flip
status to revoked. Append-only: the historical capability card
stays in the table.
Source code in src/vacant/registry/halo.py
aggregation
¶
Aggregation/index layer over the central store.
search_capability does substring + filter matching on the index columns;
rank_by_reputation defers to a ReputationOracle Protocol that P3
plugs in. For now the default oracle returns 0.0 for everyone, so
ordering falls back to insertion order — but the API is stable.
lineage_query walks the lineage edge in either direction.
Result objects always include the halo signature so consumers can verify the card independently of the registry's say-so (THEORY_V5 §7.1 trust-anchor-not-trust-origin).
HaloMatch
dataclass
¶
HaloMatch(vacant_id: str, capability_card_hash: bytes, capability_card_sig: bytes, declared_capabilities_json: str, base_model_family: str, visibility: Visibility, score: float = 0.0, capability_card: CapabilityCard | None = None)
A single search/rank result.
Carries the halo signature and the full signed CapabilityCard
(D015 §C) so the caller can verify the card and dispatch directly to
card.endpoint in a single round-trip — no registry rehydration.
capability_card is optional only for legacy rows written before
the blob column existed; new rows always carry one.
ReputationOracle
¶
Bases: Protocol
P3 plugs in here; for P4 we ship a stub that returns 0.0.
search_capability
async
¶
search_capability(*, store: RegistryStore, query: str | None = None, family: str | None = None, limit: int = 20, include_local: bool = False) -> list[HaloMatch]
Search the registry index. NONE-visibility halos are excluded by
default — include_local=True is for owner/parent direct paths and
callers must additionally enforce that the requester is the owner.
Source code in src/vacant/registry/aggregation.py
rank_by_reputation
async
¶
rank_by_reputation(matches: Sequence[HaloMatch], *, dimensions: Sequence[str] = ('factual', 'logical', 'relevance'), oracle: ReputationOracle = DEFAULT_REPUTATION_ORACLE) -> list[HaloMatch]
Re-score and sort matches using oracle. Stable for ties.
Source code in src/vacant/registry/aggregation.py
lineage_query
async
¶
lineage_query(*, store: RegistryStore, vacant_id: str, direction: Literal['descendants', 'ancestors'] = 'descendants', depth: int = 8) -> list[str]
Walk the lineage edge and return a list of vacant_ids.
Source code in src/vacant/registry/aggregation.py
store
¶
Central-MVP registry store (SQLAlchemy AsyncEngine + aiosqlite).
Implements RegistryBackend for SQLite. Anti-tamper layers L1-L3 are
checked here at write time; L4 (Merkle snapshots) is exposed via
seal_epoch(); L5 (anomaly counters) is wired into submit_event as a
post-write signal; L6 (append-only) is enforced by exposing no delete_*
methods + raising AppendOnlyViolation if a caller tries SQL-direct DELETE.
Async only. Construction takes a SQLAlchemy AsyncEngine (DI seam — tests
use sqlite+aiosqlite:///:memory:; production uses a file path).
SignedEventDraft
dataclass
¶
SignedEventDraft(event_type: str, actor_vacant_id: str, subject_vacant_id: str | None, payload: dict[str, object], idempotency_key: str, signed_by_pubkey: bytes, signature: bytes, actor_seq: int, ts: int)
A pre-signed event draft handed to the store. The store re-derives the hash chain + verifies the signature before insert.
RegistryStore
¶
SQLite-backed RegistryBackend impl with anti-tamper hooks.
Source code in src/vacant/registry/store.py
init_schema
async
¶
submit_event
async
¶
submit_event(draft: SignedEventDraft) -> Event
The hot path: idempotency → sig verify → seq monotone → chain
→ insert. Returns the persisted Event with seq and event_hash.
Race protection (F-B): the in-process asyncio.Lock is a
fast-path mutex within a single worker; the load-bearing defense
is the (actor_vacant_id, actor_seq) UNIQUE on the event
table, which turns concurrent inserts of the same actor_seq
into IntegrityError and back into SequenceMonotonicityError
for the loser of the race.
Source code in src/vacant/registry/store.py
submit_register_event_atomic
async
¶
submit_register_event_atomic(*, vacant_to_insert: Vacant | None, vacant_id_to_update: str | None, new_visibility: str | None, draft: SignedEventDraft, vacant_field_updates: dict[str, object] | None = None) -> Event
F-A defense: insert/update vacant + submit register event in
ONE transaction. If submit_event fails (signature rejected,
idempotency conflict, sequence race), the vacant row insert /
visibility update is rolled back, so the audit chain and the
publicly-visible state can never diverge.
Exactly one of vacant_to_insert or vacant_id_to_update should
be non-None per call site. If both are None, only the event is
submitted (used by tests).
vacant_field_updates (Pfix3 B5): when vacant_id_to_update
is set, callers can pass a dict of column-name → new-value to
apply onto the existing row before the register event lands.
Used by publish_halo republish so the row's
capability_card_* columns track the new card instead of
going stale while the audit chain advances. new_visibility
is the legacy single-field path; if vacant_field_updates
contains a visibility key it takes precedence.
Source code in src/vacant/registry/store.py
verify_event_chain
async
¶
Recompute every stored event's payload_hash, signature, and
event_hash from payload_json + canonical bytes. Returns False on
any mismatch — i.e. detects in-place tampering that bypassed the
signed write path (UPDATE instead of INSERT). The append-only
guard (L6) catches DELETE; this catches UPDATE.
Source code in src/vacant/registry/store.py
verify_vacant_index_consistent
async
¶
True iff the indexed vacant.visibility column matches the
visibility recorded on the most recent register event for that
vacant. Catches direct SQL UPDATE of the visibility column —
Padv-P4 §2.
Returns True if the vacant has no register events on file (nothing to compare against; rejected at higher levels).
Source code in src/vacant/registry/store.py
seal_epoch
async
¶
seal_epoch(*, signing_key: SigningKey) -> MerkleEpoch
Build a Merkle root over all unsealed events, store it, and
attach epoch_id back to each leaf event. Operator-signed.
Source code in src/vacant/registry/store.py
lookup_halo_for_caller
async
¶
lookup_halo_for_caller(target_vacant_id: str, *, caller_pubkey_hex: str | None = None) -> Vacant
Visibility-aware halo lookup. Returns the halo iff:
target.visibility == PUBLIC, ORcaller == target(owner-direct), ORcaller == target.parent(parent-direct).
Raises VisibilityViolation for stranger lookups against NONE
halos. Raises NotFoundError if the target doesn't exist.
Source code in src/vacant/registry/store.py
now_ms
¶
canonical_json
¶
JSON canonicalisation for payload_json storage. D006 §F: same
sort_keys + tight separators form used by P0 logbooks; JCS-strict
is future work.
Source code in src/vacant/registry/store.py
backend
¶
RegistryBackend Protocol — the seam for swapping central → federated/DHT.
Acceptance criterion (dispatch §"Acceptance"): "Architected so the swap
from central → federated/DHT is local to one module (a RegistryBackend
Protocol)". This module declares the contract; central.py (mid-PR
class wired into store.py) implements it for SQLite. Federated and DHT
backends are post-MVP.
RegistryBackend
¶
Bases: Protocol
Storage contract every backend implementation honours.
Methods are async; return types use SQLModel rows directly so the aggregation layer can index them without a translation step. A federated backend would implement the same interface using cross-shard reads + witness-verified writes (post-MVP).
models
¶
SQLModel tables for the central-MVP registry backend.
13 tables per architecture/components/P4_registry.md §3.1, mapped onto
SQLModel for typed CRUD + Alembic-generated migrations. Field names that
collide with Python keywords are suffixed _.
Schema decisions reconciled in D006:
- Hashes are 32-byte BLAKE2b digests (HASH_DIGEST_BYTES), not BLAKE3.
- Timestamps are stored as int milliseconds since epoch (matches
spec §3.1 "all timestamps millis since epoch").
- vacant_id stored as the lowercase hex pubkey (matches VacantId.hex()).
Vacant
¶
Bases: SQLModel
Per-vacant capability-card snapshot. P4 §3.1 table 1.
capability_card_blob
class-attribute
instance-attribute
¶
Canonical-JSON serialized signed CapabilityCard (D015 §C).
Carried verbatim through HaloMatch so dispatch can call card.endpoint
without rehydrating from individual columns.
Attestation
¶
Bases: SQLModel
Identity attestation issued by a developer / org / peer / oracle.
Event
¶
Bases: SQLModel
Append-only signed event log. P4 §3.1 table 3.
The (actor_vacant_id, actor_seq) UniqueConstraint is the
load-bearing race defense against codex F-B: an in-process
asyncio.Lock only guards concurrent submits inside a single
worker, so two workers reading the same latest_event_for_actor
could both pass check_sequence_monotonic and both try to insert
the same actor_seq. The DB-level UNIQUE turns that race into an
IntegrityError at insert time, which the store layer catches and
re-raises as SequenceMonotonicityError.
actor_seq
class-attribute
instance-attribute
¶
Per-actor monotonic sequence (anti-tamper L2).
EventFinalization
¶
Bases: SQLModel
N-of-M attestation finalization for an event. P4 §3.1 table 4.
MerkleEpoch
¶
Bases: SQLModel
Periodic Merkle root over the event log. P4 §3.1 table 5.
EpochWitness
¶
Bases: SQLModel
L6 federated witness cosignature on an epoch root.
ReputationSnapshot
¶
Bases: SQLModel
Per-vacant + per-epoch five-dimensional reputation snapshot.
CompositionLink
¶
Bases: SQLModel
Bilateral composition agreement between two vacants.
SinkRecord
¶
Bases: SQLModel
Sunk vacant terminal record. P4 §3.1 table 9.
Freeze
¶
Bases: SQLModel
Temporary freeze (anomaly / governance / self).
Revocation
¶
Bases: SQLModel
Public-key revocation record.
ReadAudit
¶
Bases: SQLModel
Optional read-side audit log (P4 §3.1 table 12; off by default).
AnomalyWindow
¶
Bases: SQLModel
Rolling-window anomaly counter (rule-based MVP, P4 §3.2 table).
rpc
¶
FastAPI RPC surface — 25 endpoints documented in OpenAPI.
Per dispatch §"Acceptance": "13 tables present, 25 RPC endpoints
documented in OpenAPI". This module wires every endpoint listed in
architecture/components/P4_registry.md §3.2 to a Pydantic v2
request/response model and a thin handler that delegates to
RegistryStore / aggregation.py / halo.py.
Endpoints whose backing logic belongs to other components (P3
reputation snapshots, P5 composition links, P6 envelope dispatch) carry
a not_implemented_in_p4 flag in the response so callers can plan
around the stubs without the endpoint disappearing later.
HaloPublishRequest
¶
Bases: _Base
Body for POST /v1/halo.
The caller serialises a signed CapabilityCard (via
vacant.protocol.capability_card.serialize) and pre-signs the
audit-chain register event under their own Ed25519 key. The
server reconstructs the canonical event bytes from these fields,
re-verifies the signature, and submits the event to the store.
capability_card_blob_hex
instance-attribute
¶
Hex-encoded serialize(card) bytes — full signed CapabilityCard.
HaloMatchResponse
¶
Bases: _Base
capability_card_blob_hex
class-attribute
instance-attribute
¶
Hex of the canonical-JSON serialized signed card. Empty for legacy
rows written before the blob column existed; clients should treat
empty as an indication to fall back to capability_card_sig_hex
+ the index columns.
StubResponse
¶
Bases: _Base
Returned by endpoints whose backing logic belongs to a later component.
build_app
¶
build_app(store: RegistryStore, *, reputation_oracle: ReputationOracle | None = None) -> FastAPI
Build the FastAPI app with all 25 endpoints wired to store.
reputation_oracle is consulted by /v1/query_capability to
rank halo matches by 5-D Beta means (P3). When omitted the
DEFAULT_REPUTATION_ORACLE (zero-score stub) is used — that
falls back to insertion order, which is fine for unit tests but
not for the demo dashboard / production. The MVP demo wires a
real vacant.reputation.aggregator.Aggregator here (F6).
Source code in src/vacant/registry/rpc.py
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 | |
antitamper
¶
Six anti-tamper layers (dispatch §5):
- Signature verify — every write checks the actor's Ed25519 signature against the canonical event bytes before insert.
- Sequence-number monotonicity — per-vacant
actor_seqstrictly increases; out-of-order writes are rejected. - Freshness window — attestations carry a validity window; stale
attestations fail at
submit_attestationtime and again at consume time. - Merkle-root snapshots —
seal_epoch()builds a balanced Merkle tree over all unsealed event hashes and stores the root + the registry operator's signature on it. - Anomaly counters — rule-based windows over rep-jump, review
bursts, spawn rates; surfaced as a
triggeredflag, not a hard block. - Append-only audit log — every signed write also lands in the
event log; DELETE on
eventis rejected at the store layer.
These are pure functions (no I/O); the store wires them in before commit.
MerkleProof
dataclass
¶
Inclusion proof: sibling hashes from leaf up to (but excluding) root.
The position of each sibling (left vs right) is reconstructed by
walking the bits of leaf_index rather than tagging each sibling
explicitly — saves a byte per level and matches RFC 6962.
Attributes:
| Name | Type | Description |
|---|---|---|
leaf_index |
int
|
Position of the leaf in the original sequence. |
leaf |
bytes
|
The hashed leaf ( |
siblings |
tuple[bytes, ...]
|
Hashes of the sibling at each level, leaf side
up. Length equals |
AnomalyAssessment
dataclass
¶
Outcome of evaluating one anomaly counter against its threshold.
Attributes:
| Name | Type | Description |
|---|---|---|
metric |
str
|
Counter name (e.g. |
value |
float
|
Measured value. |
threshold |
float
|
Threshold from the operator's configuration. |
triggered |
bool
|
|
canonical_event_bytes
¶
canonical_event_bytes(*, event_type: str, actor_vacant_id: str, subject_vacant_id: str | None, payload_hash: bytes, idempotency_key: str, signed_by_pubkey: bytes, ts: int, actor_seq: int) -> bytes
Build the canonical byte form of a registry event.
The same byte string is fed to both sign() (when the actor
creates the event) and verify() (when the registry accepts it).
It is also the pre-image of event_hash. Matches P4 §3.1 hash-
chain canonical rules (modulo BLAKE2b vs BLAKE3 — see D006 §A).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event_type
|
str
|
Event kind (e.g. |
required |
actor_vacant_id
|
str
|
Hex of the vacant submitting the event. |
required |
subject_vacant_id
|
str | None
|
Optional hex of the vacant the event is about (e.g. the target of a review). Empty string when absent. |
required |
payload_hash
|
bytes
|
BLAKE2b of the event-specific payload. |
required |
idempotency_key
|
str
|
Caller-supplied identifier for de-dup. |
required |
signed_by_pubkey
|
bytes
|
Raw 32-byte Ed25519 pubkey expected to have signed the event. |
required |
ts
|
int
|
Unix timestamp in seconds (or epoch-resolution of choice). |
required |
actor_seq
|
int
|
Strictly-increasing per-actor sequence number. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
Bytes with the eight fields joined by the |
bytes
|
suitable for signing or verification. |
Source code in src/vacant/registry/antitamper.py
verify_event_signature
¶
Verify an event signature, raising on any failure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pubkey_bytes
|
bytes
|
Raw 32-byte Ed25519 pubkey to verify under. |
required |
canonical_bytes
|
bytes
|
Output of |
required |
signature
|
bytes
|
Ed25519 signature claimed by the actor. |
required |
Raises:
| Type | Description |
|---|---|
SignatureRejected
|
If |
Source code in src/vacant/registry/antitamper.py
compute_event_hash
¶
Compute the hash that links one event to the next in the chain.
The signature is mixed in so two events with identical canonical bytes but distinct actors (one impersonating the other) cannot collide — defensive against an adversary who somehow forged a canonical-byte collision.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prev_event_hash
|
bytes
|
Hash of the previous event in the chain. |
required |
canonical_bytes
|
bytes
|
Output of |
required |
signature
|
bytes
|
Actor's Ed25519 signature over |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
|
bytes
|
row and used as |
Source code in src/vacant/registry/antitamper.py
check_sequence_monotonic
¶
Enforce strict-by-one monotonicity for a per-actor sequence number.
CONSTANTS.md pins "Sequence-number monotonicity tolerance: 0
(strict)" — the candidate must be exactly last_seq + 1, not
just > last_seq. The strict form catches both reordering attacks
(where a stale event is replayed) and gap-introduction attacks
(where a malicious actor bumps the sequence to skip auditable
history).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
last_seq
|
int
|
Highest |
required |
candidate_seq
|
int
|
The |
required |
Raises:
| Type | Description |
|---|---|
SequenceMonotonicityError
|
If |
Source code in src/vacant/registry/antitamper.py
check_attestation_freshness
¶
Reject attestations that are outside their validity window.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
valid_from_ms
|
int
|
Earliest moment the attestation should be accepted, in milliseconds since epoch. |
required |
valid_until_ms
|
int | None
|
Latest moment, or |
required |
now_ms
|
int
|
Wall-clock timestamp the registry will compare against. |
required |
Raises:
| Type | Description |
|---|---|
FreshnessError
|
If |
Source code in src/vacant/registry/antitamper.py
build_merkle_tree
¶
Build the full Merkle tree as a list of levels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
leaves
|
Sequence[bytes]
|
Pre-image bytes for each leaf. Order is significant — inclusion proofs index into this order. |
required |
Returns:
| Type | Description |
|---|---|
list[list[bytes]]
|
A list of levels, leaves first, root last (so the root is at |
list[list[bytes]]
|
|
list[list[bytes]]
|
|
list[list[bytes]]
|
root shape. |
Source code in src/vacant/registry/antitamper.py
build_merkle_root
¶
Build only the root (convenience wrapper).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
leaves
|
Sequence[bytes]
|
Pre-image bytes for each leaf, in deterministic order. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
The 32-byte root hash. For empty input, a stable empty-epoch |
bytes
|
root. |
Source code in src/vacant/registry/antitamper.py
merkle_inclusion_proof
¶
merkle_inclusion_proof(leaves: Sequence[bytes], leaf_index: int) -> MerkleProof
Build an inclusion proof for the leaf at leaf_index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
leaves
|
Sequence[bytes]
|
The full leaf sequence the tree was built over. |
required |
leaf_index
|
int
|
Index into |
required |
Returns:
| Type | Description |
|---|---|
MerkleProof
|
A |
MerkleProof
|
succeed against the root of the same tree. |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Source code in src/vacant/registry/antitamper.py
verify_inclusion_proof
¶
verify_inclusion_proof(proof: MerkleProof, root: bytes) -> bool
Verify that proof.leaf is included in a tree with root.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
proof
|
MerkleProof
|
The proof returned by |
required |
root
|
bytes
|
The expected Merkle root. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
(using |
bool
|
yields |
Source code in src/vacant/registry/antitamper.py
sign_epoch_root
¶
Operator-key signature over an epoch root.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
bytes
|
The epoch's Merkle root. |
required |
signing_key
|
SigningKey
|
The registry operator's private key. |
required |
Returns:
| Type | Description |
|---|---|
bytes
|
Ed25519 signature over |
bytes
|
The domain-separation prefix prevents the signature from being |
bytes
|
replayed against a non-epoch payload that happens to start |
bytes
|
with these bytes. |
Source code in src/vacant/registry/antitamper.py
verify_epoch_signature
¶
Verify a previously-signed epoch root.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
bytes
|
The epoch's Merkle root. |
required |
signature
|
bytes
|
Output of |
required |
operator_pubkey
|
VerifyKey
|
The expected operator verify-key. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
epoch payload. |
Source code in src/vacant/registry/antitamper.py
assess_anomaly
¶
assess_anomaly(*, metric: str, value: float, threshold: float) -> AnomalyAssessment
Compare value to threshold and package as an AnomalyAssessment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metric
|
str
|
Counter name. |
required |
value
|
float
|
Measured value. |
required |
threshold
|
float
|
Threshold to compare against. |
required |
Returns:
| Type | Description |
|---|---|
AnomalyAssessment
|
An |
AnomalyAssessment
|
|
Source code in src/vacant/registry/antitamper.py
sha256_hex
¶
SHA-256 hex digest helper for test-fixture ergonomics.
BLAKE2b is the canonical hash everywhere in the registry; this helper exists only because some fixtures predate the BLAKE2b canonicalisation and retain SHA-256 inputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes
|
Bytes to digest. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Lowercase 64-character hex digest. |
Source code in src/vacant/registry/antitamper.py
visibility
¶
Halo visibility — the registry_visibility axis of THEORY_V5 §1.1's
three-axis ontology (registry_visibility x endpoint_reachability x
outbound_policy).
Visibility.NONE matches VacantState.LOCAL per CLAUDE.md §LOCAL: a
LOCAL vacant runs and signs but is not published to the public index.
The two concepts overlap at the discovery layer: effective_visibility
collapses the runtime state into the externally-observable visibility.
Visibility
¶
Bases: StrEnum
Discovery visibility for a halo record.
NONE
class-attribute
instance-attribute
¶
Not in any public index. Reachable only via owner/parent direct path.
RESTRICTED
class-attribute
instance-attribute
¶
Indexed but only revealed to authenticated callers (P5/P6 future).
PUBLIC
class-attribute
instance-attribute
¶
Default for ACTIVE-state vacants — fully discoverable.
effective_visibility
¶
effective_visibility(state: VacantState, registry_visibility: Visibility) -> Visibility
Compute the discovery-layer visibility from runtime state + setting.
LOCAL state forces NONE regardless of registry_visibility —
CLAUDE.md §LOCAL is load-bearing: a LOCAL vacant must not appear in
the public index even if its capability card was previously published.
Sunk / Archived states keep their existing visibility (the halo is
historically retained per THEORY_V5 §4.1) — but is_runnable(state)
is False, so callers see the record but cannot make new calls.
Source code in src/vacant/registry/visibility.py
errors
¶
Error hierarchy for vacant.registry.
RegistryWriteError
¶
Bases: RegistryError
A write violated an anti-tamper invariant before commit.
SignatureRejected
¶
Bases: RegistryWriteError
A submitted envelope's signature did not verify (anti-tamper L1).
SequenceMonotonicityError
¶
Bases: RegistryWriteError
A submitted event's per-vacant sequence is not strictly greater than the last one (anti-tamper L2).
FreshnessError
¶
Bases: RegistryWriteError
An attestation is outside its freshness window (anti-tamper L3).
IdempotencyConflict
¶
Bases: RegistryWriteError
The same idempotency_key was used with a different canonical
payload hash (P4 §2.6 double-spend protection).
VisibilityViolation
¶
Bases: RegistryError
A read attempt crossed a visibility boundary (e.g. stranger requesting a LOCAL vacant's halo).
NotFoundError
¶
Bases: RegistryError
The requested record does not exist.
AppendOnlyViolation
¶
Bases: RegistryWriteError
A DELETE was attempted against an append-only table (anti-tamper L6).