Skip to content

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

PARENT_ONLY = 'parent-only'

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_BRIDGED = 'parent-bridged'

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

PUBLIC_A2A = 'public_a2a'

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

NO_EXTERNAL = 'no-external'

Never opens an outbound A2A call. Used by self-grown children that purely answer inbound work.

PARENT_PERMITTED class-attribute instance-attribute

PARENT_PERMITTED = 'parent-permitted'

Outbound calls allowed only to peers attested by the parent (in the parent's allowlist). Canonical for broker children.

UNRESTRICTED class-attribute instance-attribute

UNRESTRICTED = 'unrestricted'

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

schema_version: Literal['v1', 'v2'] = 'v2'

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

signing_dict(*, version: str | None = None) -> dict[str, Any]

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
def signing_dict(self, *, version: str | None = None) -> dict[str, Any]:
    """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.
    """
    effective = version or self.schema_version
    base: dict[str, Any] = {
        "parent_id": self.parent_id.hex(),
        "child_id": self.child_id.hex(),
        "birth_path": self.birth_path,
        "closed_by_default": self.closed_by_default,
        "tool_whitelist_inherited": sorted(self.tool_whitelist_inherited),
        "tool_whitelist_added": sorted(self.tool_whitelist_added),
        "tool_whitelist_removed": sorted(self.tool_whitelist_removed),
    }
    if effective == "v1":
        return base
    # v2 (default for fresh manifests).
    base["endpoint_reachability"] = str(self.endpoint_reachability)
    base["outbound_policy"] = str(self.outbound_policy)
    return base

signing_payload

signing_payload() -> bytes

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
def signing_payload(self) -> bytes:
    """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."""
    return self._signing_payload_for(self.schema_version)

verify

verify() -> bool

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
def verify(self) -> bool:
    """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.
    """
    return self._verify_against(self.schema_version)

ensure_birth_path

ensure_birth_path(value: str) -> Literal['D1', 'D2', 'D3', 'D4', 'D5']

Type-narrowing helper for callers building manifests from string inputs (e.g. logbook payloads).

Source code in src/vacant/composite/manifest.py
def ensure_birth_path(value: str) -> Literal["D1", "D2", "D3", "D4", "D5"]:
    """Type-narrowing helper for callers building manifests from string
    inputs (e.g. logbook payloads)."""
    if value not in BIRTH_PATHS:
        raise ManifestError(f"invalid birth_path {value!r}; expected one of {BIRTH_PATHS}")
    return value  # type: ignore[return-value]

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
def siblings_of(child_id: VacantId, manifests: Iterable[ChildManifest]) -> set[VacantId]:
    """Return the set of *other* children sharing the same parent.

    Excludes the child itself."""
    by_parent: dict[VacantId, set[VacantId]] = {}
    for m in manifests:
        by_parent.setdefault(m.parent_id, set()).add(m.child_id)
    parent: VacantId | None = None
    for m in manifests:
        if m.child_id == child_id:
            parent = m.parent_id
            break
    if parent is None:
        return set()
    return by_parent.get(parent, set()) - {child_id}

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
def 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.
    """
    if not caller_manifest.closed_by_default:
        return True
    if callee_id == caller_manifest.parent_id:
        return True
    if siblings is not None and callee_id in set(siblings):
        return True
    return False

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
def 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."""
    if not is_call_allowed(
        caller_manifest=caller_manifest,
        callee_id=callee_id,
        siblings=siblings,
    ):
        raise TreeOnlyViolationError(
            f"closed child {caller_manifest.child_id.short()} cannot call "
            f"{callee_id.short()} (not parent / not sibling)"
        )

graduation

Graduation -- flip a closed child's registry_visibility from NONE to PUBLIC (P5 §3.7, dispatch §4).

Three preconditions, all must hold:

  1. Parent consent -- a GraduationRequest signed by both parent and child (mirrors ChildManifest's dual-signature design).
  2. Rate limit -- per-parent 24h sliding window, defaults to GRADUATION_RATE_LIMIT_PER_PARENT_24H (D012 §A).
  3. Collusion check -- max signal strength on (parent, child) below GRADUATION_COLLUSION_THRESHOLD (D012 §B). Uses the CollusionDetector injected at construction time; a default CompositeStubDetector returning 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

GRADUATED_KIND = 'COMPOSITE_GRADUATED'

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
def __init__(
    self,
    *,
    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.time,
) -> None:
    if rate_limit_per_24h < 1:
        raise ValueError(f"rate_limit_per_24h must be >= 1; got {rate_limit_per_24h}")
    if not (0.0 <= collusion_threshold <= 1.0):
        raise ValueError(f"collusion_threshold must be in [0, 1]; got {collusion_threshold}")
    self._rate_limit = rate_limit_per_24h
    self._collusion_threshold = collusion_threshold
    self._detector: CollusionDetector = detector or default_detector()
    self._clock = clock
    self._window_per_parent: dict[VacantId, deque[float]] = {}

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
async def graduate(
    self,
    *,
    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."""
    record = runtime.get_child(request.child_id)
    old_manifest = record.manifest
    if old_manifest.parent_id != request.parent_id:
        raise GraduationConsentError(
            f"graduation parent_id {request.parent_id.short()} does not match "
            f"child's manifest parent {old_manifest.parent_id.short()}"
        )
    if not request.verify():
        raise GraduationConsentError("graduation request signatures did not verify")

    when = ts if ts is not None else self._clock()
    self._enforce_rate_limit(request.parent_id, when)

    signals = self._detector.signals_for(request.parent_id, request.child_id)
    if max_signal_strength(signals) >= self._collusion_threshold:
        raise GraduationCollusionError(
            f"collusion signals too high for graduation: "
            f"controller={signals.same_controller:.2f}, "
            f"substrate={signals.same_substrate:.2f}, "
            f"stylo={signals.same_stylo:.2f} "
            f"(threshold {self._collusion_threshold})"
        )

    # All checks passed: rebuild manifest with closed_by_default=False,
    # mint capability card, append GRADUATED to both logbooks.
    new_manifest = self._rebuild_manifest(old_manifest)
    new_manifest = new_manifest.signed_by_parent(_parent_signing_key(runtime))
    new_manifest = new_manifest.signed_by_child(record.child_signing_key)

    spec = substrate_spec or record.child_form.substrate_spec
    child_card = CapabilityCard(
        vacant_id=request.child_id,
        capability_text=request.capability_text,
        substrate_spec=spec,
    ).signed(record.child_signing_key)

    runtime.mark_graduated(request.child_id, new_manifest)
    self._record_graduation(request.parent_id, when)
    _append_graduated_entries(
        runtime=runtime,
        request=request,
        child_form=record.child_form,
        child_signing_key=record.child_signing_key,
    )

    return GraduationOutcome(
        new_manifest=new_manifest,
        child_card=child_card,
        collusion_signals=signals,
    )

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
def 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`."""
    payload = _signing_payload(parent_id, child_id, capability_text)
    return GraduationRequest(
        parent_id=parent_id,
        child_id=child_id,
        capability_text=capability_text,
        parent_signature=sign(parent_signing_key, payload),
        child_signature=sign(child_signing_key, payload),
    )

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

CollusionSignals(same_controller: float, same_substrate: float, same_stylo: float)

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.

Source code in src/vacant/composite/collusion.py
def max_signal_strength(signals: CollusionSignals) -> float:
    """Conservative composition: the *highest* signal is the trip line."""
    return max(signals.same_controller, signals.same_substrate, signals.same_stylo)

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.

Source code in src/vacant/composite/collusion.py
def default_detector() -> CollusionDetector:
    """Return a no-signal detector. Used when P3 is not wired (D012 §B);
    graduation then defers to parent consent + rate limit."""
    return CompositeStubDetector()

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

ChildHandler = Callable[[Any], Coroutine[Any, Any, Any]]

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
def __init__(
    self,
    *,
    parent_form: ResidentForm,
    parent_signing_key: SigningKey,
) -> None:
    self._parent_form = parent_form
    self._parent_signing_key = parent_signing_key
    self._children: dict[VacantId, ChildRecord] = {}
    self._lock = asyncio.Lock()

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
def register_child(self, 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`."""
    if record.manifest.parent_id != self._parent_form.identity:
        raise ManifestError(
            f"manifest parent_id {record.manifest.parent_id.short()} does not "
            f"match this composite {self._parent_form.identity.short()}"
        )
    if record.manifest.child_id != record.child_form.identity:
        raise ManifestError(
            f"manifest child_id {record.manifest.child_id.short()} does not "
            f"match the registered child form's identity {record.child_form.identity.short()}"
        )
    record.manifest.verify_or_raise()
    if record.child_form.identity in self._children:
        raise CompositeError(f"child {record.child_form.identity.short()} already registered")
    self._children[record.child_form.identity] = record

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
async def delegate(
    self,
    *,
    child_id: VacantId,
    subtask: Any,
) -> DelegationResult[Any]:
    """Send `subtask` to the named child and write to both logbooks."""
    record = self.get_child(child_id)
    async with self._lock:
        self._parent_form.logbook.append(
            DELEGATE_KIND,
            {
                "child_id": child_id.hex(),
                "subtask_kind": _subtask_kind(subtask),
            },
            self._parent_signing_key,
        )
    record.child_form.logbook.append(
        EXECUTE_KIND,
        {
            "parent_id": self._parent_form.identity.hex(),
            "subtask_kind": _subtask_kind(subtask),
        },
        record.child_signing_key,
    )
    response = await record.handler(subtask)
    return DelegationResult(child_id=child_id, subtask=subtask, response=response)

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
async def delegate_many(
    self,
    plan: Iterable[tuple[VacantId, Any]],
) -> list[DelegationResult[Any]]:
    """Sequential delegation of a list of (child_id, subtask) pairs."""
    results: list[DelegationResult[Any]] = []
    for child_id, subtask in plan:
        results.append(await self.delegate(child_id=child_id, subtask=subtask))
    return results

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
def aggregate(
    self,
    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."""
    materialised = list(results)
    responses = [r.response for r in materialised]
    combined = combiner(responses)
    self._parent_form.logbook.append(
        AGGREGATE_KIND,
        {
            "n_results": len(materialised),
            "child_ids": [r.child_id.hex() for r in materialised],
        },
        self._parent_signing_key,
    )
    return combined

outbound_call

outbound_call(*, caller_child_id: VacantId, callee_id: VacantId) -> None

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
def outbound_call(
    self,
    *,
    caller_child_id: VacantId,
    callee_id: VacantId,
) -> None:
    """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."""
    manifest = self.manifest_for(caller_child_id)
    sibling_set = siblings_of(caller_child_id, self.manifests)
    tree_only_filter(
        caller_manifest=manifest,
        callee_id=callee_id,
        siblings=sibling_set,
    )

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
def mark_graduated(self, 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.
    """
    record = self.get_child(child_id)
    if new_manifest.parent_id != record.manifest.parent_id:
        raise ManifestError("graduated manifest parent_id mismatch")
    if new_manifest.child_id != record.manifest.child_id:
        raise ManifestError("graduated manifest child_id mismatch")
    if new_manifest.closed_by_default:
        raise ManifestError("graduated manifest must have closed_by_default=False")
    new_manifest.verify_or_raise()
    self._children[child_id] = ChildRecord(
        manifest=new_manifest,
        child_form=record.child_form,
        child_signing_key=record.child_signing_key,
        handler=record.handler,
    )

errors

P5 composite error hierarchy.

CompositeError

Bases: CoreError

Base class for vacant.composite errors.

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).