vacant.composite¶
P5 composite — composite parents, child manifests, Tree-Only protocol for sealed children, graduation (visibility flag flip — same keypair, same logbook), and the 3-layer collusion gate that graduation must pass.
manifest
¶
ChildManifest -- the dual-signed link between a composite parent
and one of its children (P5 §3.1, dispatch §1).
The manifest is the only in-tree authorisation a child needs:
- the parent vouches that this child is its child (signature_parent),
- the child vouches that it accepts this parent (signature_child).
Both signatures cover the canonical-json of the same payload (D012 §C).
A manifest with one signature missing or with either signature invalid
is rejected by verify_or_raise.
The manifest is held by the composite parent's runtime (CompositeRuntime);
it is not serialised to the public registry, so externally a composite
vacant exposes only its own halo (P5 §3.3 black-box principle).
Three-axis ontology (THEORY_V5 §5.1):
| Axis | Values |
|---|---|
registry_visibility |
NONE / UNLISTED / PUBLIC (in registry.visibility) |
endpoint_reachability |
PARENT_ONLY / PARENT_BRIDGED / PUBLIC_A2A |
outbound_policy |
NO_EXTERNAL / PARENT_PERMITTED / UNRESTRICTED |
closed_by_default predates the 3-axis split and remains as a coarse
visibility shorthand. The two new enums let callers express the three
canonical configurations cleanly:
- Self-grown: visibility=NONE, reachability=PARENT_ONLY, outbound=NO_EXTERNAL
- Broker: visibility=UNLISTED, reachability=PARENT_BRIDGED, outbound=PARENT_PERMITTED
- Public resident (graduated): visibility=PUBLIC, reachability=PUBLIC_A2A,
outbound=any-of-the-three (least-privilege per V5 §5.2)
Reachability
¶
Bases: StrEnum
The 2nd axis of THEORY_V5 §5.1's composite ontology — how a peer can route an A2A envelope to this vacant.
PARENT_ONLY
class-attribute
instance-attribute
¶
Reachable only via the parent runtime (no own HTTP endpoint exposed). Canonical for self-grown children. The parent forwards relevant calls inwardly via the orchestrator.
PARENT_BRIDGED
class-attribute
instance-attribute
¶
Parent exposes a bridged route on its own endpoint. The child's halo lists the parent's endpoint with a routing hint (e.g. a path suffix) so a caller can reach the child through the parent. Canonical for broker children.
PUBLIC_A2A
class-attribute
instance-attribute
¶
Child runs its own HTTP A2A endpoint, addressable by anyone holding its capability_card. Canonical for graduated vacants.
OutboundPolicy
¶
Bases: StrEnum
The 3rd axis of THEORY_V5 §5.1's composite ontology — what this vacant is allowed to do toward the outside world.
Independent of reachability per V5 §5.2 (a graduated, listed
vacant can still choose NO_EXTERNAL as a least-privilege
posture).
NO_EXTERNAL
class-attribute
instance-attribute
¶
Never opens an outbound A2A call. Used by self-grown children that purely answer inbound work.
PARENT_PERMITTED
class-attribute
instance-attribute
¶
Outbound calls allowed only to peers attested by the parent (in the parent's allowlist). Canonical for broker children.
UNRESTRICTED
class-attribute
instance-attribute
¶
Outbound calls to any peer that the registry resolves. Required for top-level public residents that act as callers.
ChildManifest
¶
Bases: BaseModel
Dual-signed parent <-> child link.
closed_by_default is True for every D2 subagent-bud spawn (the path
designed for composite children). For D1/D3/D5 spawns the manifest
can still be issued, but the orchestrator typically reserves them
for follow-on reasoning, not the canonical "closed sub-vacant" role.
schema_version
class-attribute
instance-attribute
¶
Signing-payload schema version. New manifests sign with v2 (axis-bearing). Verification tries v2 first and falls back to v1 so manifests persisted by pre-2026-05-15 code remain valid without a forced re-sign migration.
signing_dict
¶
Canonical dict over which both parent and child sign.
Excludes the two signature fields. Tool-whitelist lists are
sorted so ["a","b"] and ["b","a"] produce the same payload.
Output shape depends on version (defaults to
self.schema_version):
- v1: original 7 fields only (matches pre-2026-05-15 verifiers).
- v2: v1 fields + endpoint_reachability + outbound_policy.
endpoint_reachability and outbound_policy are serialised as
their string values so older verifiers (that don't know the
enum classes) can still re-derive the canonical bytes from
the stored manifest JSON.
Source code in src/vacant/composite/manifest.py
signing_payload
¶
Canonical bytes used at sign time (always the manifest's own
schema_version). Kept for callers that want to re-derive
the exact payload the sigs cover.
Source code in src/vacant/composite/manifest.py
verify
¶
True iff both signatures verify under the manifest's own
schema_version.
We deliberately do NOT fall back across versions. The earlier v2→v1 fallback was a downgrade-attack surface: an attacker who observed a legitimately-signed v1 manifest could append attacker-chosen axis fields and present it as v2; v2 verification would fail (axes weren't in the original sig), the fallback to v1 would strip the axes and re-verify against the original payload, and the consumer would then read the attacker's chosen axis values as if they were authenticated.
Callers loading pre-upgrade persisted manifests must set
schema_version="v1" explicitly so the signing payload
matches what was originally signed.
Source code in src/vacant/composite/manifest.py
ensure_birth_path
¶
Type-narrowing helper for callers building manifests from string inputs (e.g. logbook payloads).
Source code in src/vacant/composite/manifest.py
tree_only
¶
Tree-Only outbound filter (P5 §2 D2 / dispatch §3).
A closed child can only call: - its parent (the composite root), or - a sibling within the same composite tree (mediated through the parent).
Cross-tree calls and direct external calls are rejected. The filter is the structural enforcement of D1 (Closed children is a hard principle): ACLs were rejected as too easy to bypass.
The orchestrator (CompositeRuntime) holds a list of ChildManifests.
The tree itself is parent_id -> {child_id, ...}. is_call_allowed
takes the caller's manifest and a callee identity and returns True iff
the callee is the parent or a sibling registered under the same parent.
In production, every outbound socket from a closed-child runtime is
gated through tree_only_filter middleware -- given the (caller_id,
callee_id) pair, it raises TreeOnlyViolationError for non-tree calls.
siblings_of
¶
siblings_of(child_id: VacantId, manifests: Iterable[ChildManifest]) -> set[VacantId]
Return the set of other children sharing the same parent.
Excludes the child itself.
Source code in src/vacant/composite/tree_only.py
is_call_allowed
¶
is_call_allowed(*, caller_manifest: ChildManifest, callee_id: VacantId, siblings: Iterable[VacantId] | None = None) -> bool
True iff a closed child may call callee_id.
Permitted callees:
- caller_manifest.parent_id (the composite root).
- any id in siblings (the orchestrator passes the precomputed
sibling set; pass None to disable sibling calls entirely).
A child whose closed_by_default=False is treated as graduated
-- it can reach any callee, so this filter no-ops and returns True.
Source code in src/vacant/composite/tree_only.py
tree_only_filter
¶
tree_only_filter(*, caller_manifest: ChildManifest, callee_id: VacantId, siblings: Iterable[VacantId] | None = None) -> None
Raise TreeOnlyViolationError if the call would breach Tree-Only.
The orchestrator's outbound dispatch wires this in front of every
network call from a closed-child runtime. A graduated child
(closed_by_default=False) is unaffected.
Source code in src/vacant/composite/tree_only.py
graduation
¶
Graduation -- flip a closed child's registry_visibility from
NONE to PUBLIC (P5 §3.7, dispatch §4).
Three preconditions, all must hold:
- Parent consent -- a
GraduationRequestsigned by both parent and child (mirrorsChildManifest's dual-signature design). - Rate limit -- per-parent 24h sliding window, defaults to
GRADUATION_RATE_LIMIT_PER_PARENT_24H(D012 §A). - Collusion check -- max signal strength on (parent, child) below
GRADUATION_COLLUSION_THRESHOLD(D012 §B). Uses theCollusionDetectorinjected at construction time; a defaultCompositeStubDetectorreturning zeroes is used when P3 is not wired.
Identity preservation is load-bearing (CLAUDE.md §Closed children +
graduation): the same keypair, the same logbook, just a visibility
flag flip. The graduated child gets a new dual-signed manifest with
closed_by_default=False and fresh GRADUATED log entries appended
to both logbooks; nothing else changes.
A successful graduate() call returns a GraduationOutcome carrying
the new manifest plus the CapabilityCard ready for publish_halo
in P4. The caller is responsible for the actual halo publish (P5 does
not import P4 directly to keep the layering clean).
GRADUATED_KIND
module-attribute
¶
Logbook entry kind written to both parent and child on success.
GraduationRequest
dataclass
¶
GraduationRequest(parent_id: VacantId, child_id: VacantId, capability_text: str, parent_signature: bytes, child_signature: bytes)
Dual-signed authorisation for graduation.
parent_signature and child_signature are detached Ed25519
signatures over _signing_payload(parent_id, child_id,
capability_text). Both must verify under their respective ids.
GraduationOutcome
dataclass
¶
GraduationOutcome(new_manifest: ChildManifest, child_card: CapabilityCard, collusion_signals: CollusionSignals)
Result of a successful graduate().
new_manifest replaces the child's old (closed) manifest in the
composite runtime. child_card is the freshly-signed capability
card for publish_halo (P4); the caller actually publishes.
GraduationService
¶
GraduationService(*, rate_limit_per_24h: int = GRADUATION_RATE_LIMIT_PER_PARENT_24H, collusion_threshold: float = GRADUATION_COLLUSION_THRESHOLD, detector: CollusionDetector | None = None, clock: Callable[[], float] = time)
Stateful graduation gate held by the composite runtime.
Owns the per-parent rate-limit sliding-window deque + the injected collusion detector. The same instance is reused across graduation calls so the window persists.
The service does NOT publish to P4. It returns the
CapabilityCard and the new dual-signed manifest; the caller wires
up publish_halo (P4 §publish_halo) so P5 is not coupled to the
registry implementation.
Source code in src/vacant/composite/graduation.py
graduate
async
¶
graduate(*, runtime: CompositeRuntime, request: GraduationRequest, substrate_spec: SubstrateSpec | None = None, ts: float | None = None) -> GraduationOutcome
Run all three checks and, on success, flip the child's
manifest in runtime and return the new outcome.
Raises GraduationConsentError, GraduationRateLimitError, or
GraduationCollusionError on the first failing check. The
runtime is not mutated until all checks pass.
Source code in src/vacant/composite/graduation.py
make_graduation_request
¶
make_graduation_request(*, parent_id: VacantId, parent_signing_key: SigningKey, child_id: VacantId, child_signing_key: SigningKey, capability_text: str) -> GraduationRequest
Builder helper: produce a fully signed GraduationRequest.
Source code in src/vacant/composite/graduation.py
collusion
¶
Collusion-detection adapter for the graduation flow (P5 §6, dispatch §5).
Before a closed child's halo is published, the graduation flow must confirm that the parent <-> child pair does NOT exhibit the canonical collusion signals (same-controller, same-substrate, same-stylo). Above-threshold signals indicate the parent is trying to graduate a sock-puppet rather than a genuinely independent capability.
This module wraps P3's same_detect for the graduation path. P3 is not
a hard dependency: callers may pass any CollusionDetector Protocol
implementation. A default_detector() is provided that returns 0.0 on
every signal -- safe default so graduation works in P3-less builds and
defers to other gates (parent consent + rate limit). Tests inject a
detector that returns specific signal strengths.
Per the stated theory invariant (CLAUDE.md): same- detection raises cost, not prevents. The graduation gate's role is the same: a high collusion signal blocks this* graduation; the attacker can re-attempt after burning more identity capital.
CollusionSignals
dataclass
¶
Strengths of the three collusion signals (THEORY_V5 §6 framing).
Each strength is in [0.0, 1.0]. same_controller -- both vacants
appear to be operated by the same human/org. same_substrate --
both run on the same base model family. same_stylo -- the
children's behavioural fingerprints (STYLO Vec16) are within the
drift threshold of each other or of the parent.
CollusionDetector
¶
Bases: Protocol
Plug-point for the graduation flow.
Concrete impls call into P3's same_detect (substrate fingerprint
comparison, controller graph, STYLO embedding distance). Tests pass
a stub returning fixed signals.
CompositeStubDetector
dataclass
¶
CompositeStubDetector(same_controller: float = 0.0, same_substrate: float = 0.0, same_stylo: float = 0.0)
Constant-returning detector. Useful in tests + the P3-less default.
max_signal_strength
¶
max_signal_strength(signals: CollusionSignals) -> float
Conservative composition: the highest signal is the trip line.
default_detector
¶
default_detector() -> CollusionDetector
Return a no-signal detector. Used when P3 is not wired (D012 §B); graduation then defers to parent consent + rate limit.
orchestrator
¶
Composite orchestrator (P5 §3, dispatch §2).
CompositeRuntime holds a composite parent's ResidentForm plus the
ChildManifest list for every direct child, and exposes:
delegate(subtask, child_id)-- dispatch a subtask to a sub-vacant through the Tree-Only protocol.aggregate(child_responses)-- combine sub-results into the composite's response.outbound_call(caller_child_id, callee_id)-- the gate every outgoing socket from a closed-child runtime passes through.
Every dispatch writes to both logbooks: the parent's logbook records the delegation, the child's logbook records the inbound task. That dual-write is the audit trail used by graduation + by the internal mini_rep tracker (P5 §3.6).
ChildHandler
module-attribute
¶
Async callable invoked when the orchestrator delegates to a child. The orchestrator hands the subtask in; the child returns its result. Real implementations would route via P6 envelopes; tests pass a lambda.
ChildRecord
dataclass
¶
ChildRecord(manifest: ChildManifest, child_form: ResidentForm, child_signing_key: SigningKey, handler: ChildHandler)
A child registered with the composite parent.
Combines the dual-signed manifest with the runtime hooks the orchestrator needs to dispatch to it.
DelegationResult
dataclass
¶
DelegationResult(child_id: VacantId, subtask: Any, response: T)
Output of CompositeRuntime.delegate.
CompositeRuntime
¶
CompositeRuntime(*, parent_form: ResidentForm, parent_signing_key: SigningKey)
In-process composite parent. Holds the parent form + child registry.
Construction does not perform any I/O. Use register_child to add
a fully-spawned child + its dual-signed manifest. delegate
dispatches a single subtask; aggregate combines a list of
DelegationResults into one output.
Concurrency: delegation acquires self._lock per dispatch so that
parent-logbook writes stay strictly ordered (BLAKE2b chain
integrity). Children are dispatched sequentially in delegate_many
by default; callers who want parallelism inject their own
asyncio.gather through delegate.
Source code in src/vacant/composite/orchestrator.py
register_child
¶
register_child(record: ChildRecord) -> None
Validate the manifest's dual signatures and add the child.
Raises ManifestError if the manifest is unsigned or the parent
id does not match this composite's identity. Re-registering the
same child id raises CompositeError.
Source code in src/vacant/composite/orchestrator.py
delegate
async
¶
delegate(*, child_id: VacantId, subtask: Any) -> DelegationResult[Any]
Send subtask to the named child and write to both logbooks.
Source code in src/vacant/composite/orchestrator.py
delegate_many
async
¶
delegate_many(plan: Iterable[tuple[VacantId, Any]]) -> list[DelegationResult[Any]]
Sequential delegation of a list of (child_id, subtask) pairs.
Source code in src/vacant/composite/orchestrator.py
aggregate
¶
aggregate(results: Iterable[DelegationResult[Any]], *, combiner: Callable[[list[Any]], Any] = list) -> Any
Combine sub-results. Default combiner returns the list of responses; callers pass a custom combiner for domain shaping.
Source code in src/vacant/composite/orchestrator.py
outbound_call
¶
Gate every outbound network call from a closed child.
Raises TreeOnlyViolationError for cross-tree / external calls.
A graduated child (closed_by_default=False) bypasses the gate.
Source code in src/vacant/composite/orchestrator.py
mark_graduated
¶
mark_graduated(child_id: VacantId, new_manifest: ChildManifest) -> None
Replace a child's manifest after graduation.
The new manifest must (a) be dual-signed, (b) have the same
parent_id and child_id, and (c) have closed_by_default=False.
Raises ManifestError otherwise.
Source code in src/vacant/composite/orchestrator.py
errors
¶
P5 composite error hierarchy.
ManifestError
¶
Bases: CompositeError
A ChildManifest is malformed or its dual signature is invalid.
TreeOnlyViolationError
¶
Bases: CompositeError
A closed child attempted an outbound call to a non-tree target (P5 §2 D2 / CLAUDE.md §Closed children).
GraduationError
¶
Bases: CompositeError
Graduation precondition failed: missing parent consent, rate limit exceeded, or collusion signal too high.
GraduationRateLimitError
¶
Bases: GraduationError
Graduation rejected because the per-parent 24h rate limit was hit (D012 §A).
GraduationConsentError
¶
Bases: GraduationError
Graduation rejected because the parent's consent was missing, malformed, or did not verify.
GraduationCollusionError
¶
Bases: GraduationError
Graduation rejected because a same_* signal between parent
and child exceeded GRADUATION_COLLUSION_THRESHOLD (D012 §B).