Capsules, Manifests, and the Event Bus
Astrid’s composition problem is specific: how can a runtime assemble software from replaceable parts without turning every part into a copy of the host’s authority? This chapter focuses on the capsule—the unit Astrid uses to package behavior, declare interfaces, request effects, and participate in a running system. By the end, the reader should be able to look at a capsule declaration as a contract for composition, follow how the daemon turns it into a live object, and see exactly where that contract stops short of permission.
A capsule is a declaration before it is a process
In a conventional plugin system, the package is often just code plus an informal expectation about what the host will provide. The plugin may inherit the host process’s filesystem, network identity, environment, or credentials. Even when the host exposes an API, the boundary can be difficult to inspect: some assumptions live in code, some in deployment scripts, and some in whatever the parent process happens to have open.
Astrid moves the first description of a component into a manifest. A Capsule.toml gives the runtime a structured account of what the capsule is, what it contains, what it needs from other capsules, what it offers, which events it may publish or receive, and which external effects it requests. The manifest can name WebAssembly components, commands, tools, context, environment fields, MCP integrations, uplinks, and capability allow-lists. Those declarations do not make the capsule trustworthy. They make its intended shape visible enough for the loader and operator to reason about before code runs.
That distinction is the first practical consequence of Astrid’s authority model. A manifest is closer to a component contract and an installation-review surface than to a permission slip. It answers questions such as “which interface does this capsule provide?” or “which topic does it intend to publish on?” It does not answer, by itself, “may this invocation read this principal’s home?” or “may this request connect to that host?” Those decisions belong to the effect-specific gates examined later.
The manifest also keeps several kinds of relationship separate. Package identity says which capsule is being loaded. A component entry identifies executable material. An import says that the capsule expects another component to provide a versioned interface; an export says what it offers. Publish and subscribe declarations describe event relationships. Capability fields describe requested access to resources. Environment declarations describe values that may be supplied during installation or resolved through principal-scoped state. Treating these as different fields prevents a dependency from being mistaken for an authority grant.
The interface declarations are versioned. Astrid accepts both nested and flat forms for its namespaced import and export tables, but normalizes them into the same internal representation. An import can carry a version requirement and can be optional. An export carries an exact version. This is not merely syntactic tidiness. If two components are to be replaced independently, the runtime needs a stable answer to whether they speak the same contract. A name alone is too weak: two components can use the same label while disagreeing about the shape or meaning of the exchange.
The same principle applies to IPC topics. A publish or subscribe pattern is not only documentation; it is part of the route authorization surface. An empty publish or subscribe table means that the capsule may not use that side of the bus. Topic names are validated, and matching rules are constrained rather than treated as unrestricted string search. A capsule therefore declares the paths by which it participates in the system before it starts emitting traffic.
The loader composes different engines under one lifecycle
Astrid does not force every capsule to choose one execution technology. Its loader translates the manifest into a composite capsule. A composite can contain a WebAssembly engine, a native stdio MCP engine when the manifest explicitly requests that host-process integration, and a static engine for context or commands that need no virtual machine. The point is not to pretend that these engines have identical trust properties. The point is to give the runtime one lifecycle and one place to apply the surrounding policy while preserving their different internal mechanisms.
The WebAssembly engine is the ordinary sandboxed path. It runs component code and exposes Astrid’s typed host interfaces. The MCP engine is an airlock for a native subprocess, which is a materially different boundary: native code does not receive the same isolation merely because it appears inside a capsule package. The static engine is different again. It can load declarative context or commands into memory without starting a VM or process. These distinctions matter because a composite lifecycle is not a claim that all three engines are equally safe; it is a claim that their admission, health, activation, and retirement can be coordinated.
The composite owns its engines and fans lifecycle operations across them. Loading moves through the engines; if a later engine fails, already-loaded engines are cleaned up. Unloading proceeds in reverse order and records failures without simply abandoning the remaining engines. Autonomous work can be activated only after the runtime has prepared the capsule, and externally visible routes can be published or retired as separate lifecycle actions. This gives the daemon a way to prepare a generation before exposing it, rather than making “the code exists on disk” equivalent to “the code is accepting traffic.”
That arrangement is useful for real software. A provider may have a WebAssembly component, an MCP bridge, and static configuration that need to appear as one installed unit. A tool-oriented capsule may be replaced without changing the daemon’s generic routing logic. A future frontend can be added as another bus participant instead of creating a privileged callback path in the kernel.
It also creates a hidden coupling that operators must notice. The word “capsule” can conceal several execution boundaries. A package that includes native MCP execution has a host-process escape path that must be checked separately from the WebAssembly sandbox. A static context engine has no VM fault to contain, but its data can still affect what another component sees. A shared lifecycle simplifies management while making the composite’s total authority the relevant review unit. Astrid’s design is strongest when the package description makes those differences legible rather than allowing the common name to flatten them.
Dependencies describe order, not ownership
Before capsules can exchange events, the runtime needs an order in which to prepare them. Astrid derives dependency edges from declared interfaces and capabilities rather than from package names. A capsule that imports a versioned interface can be placed after a compatible provider. A cycle in the dependency graph is an error because no topological order can satisfy it.
This is a useful separation from directory-based startup. Package names are labels chosen for distribution; an interface relationship describes an actual compatibility need. The loader can therefore reason about a provider being replaced or about several providers exposing related capabilities without hard-coding a product-specific sequence.
The boundary is important: the hosted loader currently logs unsatisfied requirements as warnings and treats them as met, while cycles fail. That means the topological order is not a universal proof that every declared provider exists or that every runtime interaction will succeed. A capsule may still load into a system where an optional or unresolved relationship is unavailable. The resulting behavior may be a later health failure, a missing response, or a deliberate fallback in the capsule. Readers should therefore treat “dependency resolved” and “capsule loaded” as different facts.
This is an example of why Astrid’s declarations cannot be read as guarantees without checking the enforcement path. The manifest makes a relationship visible. The loader imposes an order where it can. The capsule and its host interfaces still determine what happens when the relationship is absent. A design that exposes this limitation is more useful than one that turns a warning into a false promise of strict composition.
The event bus is the shared meeting place
Once components are loaded, Astrid connects them through an event bus. Frontends such as the CLI, HTTP gateway, Discord integration, and MCP bridge are uplinks: protocol clients that publish and receive events through the same general mechanism. There is no special Frontend concept that grants them a privileged route into the application. The daemon owns the bus and its management surfaces, but product behavior remains in capsules.
An event has a topic, payload, source identity, and—when relevant—principal attribution. The topic says which kind of exchange is taking place; the payload carries the data; the source identifies the publishing capsule; the principal tells the receiver whose invocation the message represents. These are separate fields because a capsule identity is not necessarily the identity of the human or agent on whose behalf the capsule is acting.
Astrid’s current bus has two delivery shapes. General observers can receive broadcast events in order, while routed guest subscriptions are demultiplexed by capsule, topic, and principal on the publish side. Each route has bounded capacity and per-principal queues. Deficit-round-robin draining gives active principals a fair chance to make progress instead of letting one burst consume all of a shared route. Lag is surfaced as a backpressure condition rather than silently treated as a complete history.
That routing choice is more than a performance optimization. If a shared capsule consumes a global queue and filters principal identity only after receiving a batch, one principal’s traffic can arrive mixed with another’s. The consumer then needs complicated requeue logic, and a slow or busy principal can distort what another invocation observes. Publish-side separation puts the principal boundary into the delivery structure itself. For an ordinary principal route, the guest receives a queue scoped to the identity it is allowed to see, rather than a firehose followed by a best-effort filter; deliberately privileged services can still receive broader routes.
The IPC contract also distinguishes a verified principal from a claimed principal. A normal publish inherits principal attribution from the invocation context. An uplink may use publish-as to relay on behalf of a named principal, but the contract marks that identity as claimed rather than kernel-verified. Sensitive downstream operations must not treat a claim as authenticated context without an additional check. The distinction protects the exact point where a protocol bridge could otherwise turn a user-supplied name into authority.
A subscription is similarly not just a listening convenience. It consumes a bounded resource, is limited by the capsule’s ACL, and has a lifetime tied to the guest resource. An invocation-local response waiter is scoped to the effective principal so that a shared runtime cannot let one caller consume another caller’s response. An audit-covering subscription is scoped to the owner unless the principal holds the separate authority to receive the broader feed. These rules show how an event route becomes part of the security model without making the event bus itself the entire security model.
The visual boundary is intentional. The manifest and loader decide how a capsule is assembled. The bus decides how messages are routed. The kernel provides the generic machinery around those decisions. Product-specific tool meaning, model behavior, and orchestration logic stay above the routing boundary. A tool is an IPC convention: a capsule can intercept an agreed topic and implement the behavior, while the kernel need not know the tool’s schema.
What the composition layer can and cannot promise
Declarative composition solves a real problem: it gives replaceable software a visible identity, explicit interfaces, a known lifecycle, and bounded communication paths. It also makes several kinds of change easier to reason about. A provider can publish a new interface version. A capsule can add a static context engine without changing the daemon. An uplink can connect through events instead of acquiring a bespoke frontend privilege. A route can be scoped to a principal before traffic reaches the guest.
But the same layer has boundaries that matter in practice. A manifest request is not a signed capability token. A topological order is not proof that every dependency is available. A composite lifecycle is not proof that a native MCP subprocess has WebAssembly-level isolation. A bounded queue is not durable storage, and a verified source principal is not created merely because a payload contains a principal-looking string. The system becomes trustworthy only when the later enforcement points preserve these distinctions.
This is why Astrid’s composition model is deliberately incomplete as a security answer. It tells the runtime what software is trying to be and how components should connect. It does not allow the software to decide unilaterally whether it may read a path, open a connection, spawn a process, or alter another principal’s state. Those questions begin at the next boundary, where sandboxing, manifest-backed host checks, IPC ACLs, signed capabilities, human approval, budgets, and audit take over.
The capsule is therefore neither a miniature operating system nor a bag of code. It is a declared participant in a larger authority system. Its value comes from making the participant’s interfaces and routes inspectable while refusing to confuse inspectability with permission. Once that distinction is clear, the next question follows naturally: when capsule code actually asks to cross an effect boundary, which mechanism decides whether the request is allowed?