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).
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.
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.
Source code in src/vacant/composite/manifest.py
verify
¶
True iff both signatures verify under their respective keys.
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).