Runtime composition contract (proposal)
Status: proposed; the capability-taking entry points and the supplied-only tool registry are implemented. This page is the composition-API proposal that #10993 asks for before callers move. The contract lives in crates/zeroclaw-runtime/src/composition.rs. The runtime entry points listed under step 2 take it through *_with_capabilities forms, and their old signatures remain adapters over an explicitly config-backed set. Application wiring (step 3 onward) and the rest of #7432 R1 are separate work.
The goal is the one RFC #5574 set for Phase 2 D1: an agent runtime that an embedder can run with capabilities it supplies, where the runtime “has no knowledge of Telegram, Discord, Anthropic, or any specific tool implementation”, and where the binary becomes a thin wiring layer.
Where the runtime stands today
Measured on master c44909b1ed with cargo metadata and a production-only grep that excludes #[cfg(test)] modules and test files.
Crate edges
| Crate | Depends on (workspace crates) |
|---|---|
zeroclaw-runtime | api, commands, config, infra, log, macros, memory, providers, relay-proto, sop-graph, spawn, tls, tool-call-parser, tools; plugins (optional) |
zeroclaw-channels | api, config, infra, log, memory, providers, runtime, spawn, tool-call-parser; tools (optional) |
zeroclaw-gateway | api, channels, config, hardware, infra, log, memory, providers, runtime, spawn, tls, tools; plugins (optional) |
The runtime links every concrete provider, memory backend, and optional tool unconditionally. The channels crate sits above the runtime, which is the inversion #6864 describes.
Where concrete capabilities are constructed
Inside the runtime, all capability construction funnels through a small number of entry functions:
| Function | Builds |
|---|---|
agent::loop_::run | provider, memory, observer, tool registry |
agent::loop_::process_message_shared | provider, memory, observer, tool registry |
Agent::from_config_with_session_cwd_and_mcp_approval_mode | provider, memory, observer |
Agent::try_apply_model_switch, build_session_model_provider | provider |
agent::turn::assemble_owned_execution | memory, tool registry |
tools::delegate (build_target_provider, memory_for_target_agent, independent_agentic_tools_for_target) | provider, memory, tool registry for a delegate target |
daemon::run, daemon::run_heartbeat_worker | memory, observer |
cron::scheduler::run_agent_job | memory |
tools::all_tools_with_runtime and default_tools | the tool registry itself |
| Capability | Production construction sites | Files |
|---|---|---|
| Provider | 13 | 6 |
| Memory | 14 | 8 |
| Observer | 8 | 4 |
| Tool registry assembly | 10 | 6 |
zeroclaw_tools:: uses | 98 (78 in tools/mod.rs) | 10 |
Construction also happens outside the runtime, in parallel with it. That is a second source of truth, not only a dependency problem:
- Gateway (
zeroclaw-gateway/src/lib.rs) builds its own resilient provider, memory backend, observer, and two tool registries, then callsagent::process_message. - Channels orchestrator keeps a per-sender provider cache (
get_or_create_provider) built withcreate_resilient_model_provider_from_ref, and callsall_tools_with_runtimedirectly. - ACP server builds agents through
Agent::from_config_with_session_cwd_and_mcp_backchannel*. src/main.rsconstructs providers for several commands and memory for the SOP engine, then callsagent::runanddaemon::run.
What DaemonRegistry already does
daemon::DaemonRegistry (landed in #7430) registers subsystem starters: gateway, channels orchestrator, local socket, WSS, relay, enrollment, and MQTT. It also carries the shared SOP engine and audit logger. It does not register providers, memory, tools, or observers. It solves process composition, not capability composition.
Proposal
Two registries, because there are two lifetimes
RFC #5574 sketched one Registry with register_channel, register_tool, set_provider, set_memory, and set_observer. The current code has two things with different lifetimes, and folding them into one type would hide that:
| Capabilities | Subsystems | |
|---|---|---|
| What | Providers, memory, tools, outbound channels, observer | Gateway, channels orchestrator, socket, WSS, relay, enrollment, MQTT |
| Type | RuntimeCapabilities (new) | DaemonRegistry (existing) |
| Lifetime | One config generation | One daemon run or reload iteration |
| Consumed by | Every agent turn | daemon::run supervision |
The runtime holds both. Subsystem starters receive the generation’s capabilities instead of constructing their own, which is what removes the parallel construction in the channels orchestrator. The gateway is the exception: its starter is deleted at the v0.9.0 gateway split, so it is never migrated onto this contract (step 4).
Sources, not instances
The RFC sketch passes finished instances (set_provider(Arc<dyn Provider>)). That cannot serve the current runtime:
- Providers and memory resolve per agent under the multi-agent model (ADR-011).
- A session can switch model mid-conversation (
try_apply_model_switch), and a delegate resolves a different agent’s provider and memory. - A live config apply produces a new generation (ADR-012), and turns already running must keep the capabilities they started with.
So the runtime asks sources for what it needs, naming the agent and the generation. A source decides how to build and cache. The runtime decides when to ask, for which agent, and under which resolved policy.
Public types
These are in the skeleton, zeroclaw_runtime::composition:
#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct RuntimeCapabilities {
pub providers: Arc<dyn ProviderSource>,
pub memory: Arc<dyn MemorySource>,
pub tools: Arc<dyn ToolSource>,
pub channels: Arc<dyn ChannelSource>,
pub observer: Arc<dyn Observer>,
}
pub trait ProviderSource: Send + Sync {
fn model_provider(&self, request: &ProviderRequest<'_>)
-> anyhow::Result<Arc<dyn ModelProvider>>;
// Defaults to `model_provider`.
fn switched_model_provider(&self, request: &ProviderRequest<'_>)
-> anyhow::Result<Arc<dyn ModelProvider>>;
}
#[async_trait]
pub trait MemorySource: Send + Sync {
async fn memory(&self, request: &MemoryRequest<'_>) -> anyhow::Result<Arc<dyn Memory>>;
}
pub trait ToolSource: Send + Sync {
// Default Ok(None): the supplied vector is the whole registry.
// A source may instead explicitly select a borrowed native recipe.
fn native_recipe<'s>(&'s self, request: &ToolRequest<'_>)
-> anyhow::Result<Option<&'s NativeRegistryRecipe>> { Ok(None) }
fn tools(&self, request: &ToolRequest<'_>) -> anyhow::Result<Vec<Box<dyn Tool>>>;
}
pub trait ChannelSource: Send + Sync {
fn channel(&self, alias: &str) -> Option<Arc<dyn Channel>>;
}
}
ProviderRequest carries the config, the agent alias, an optional explicit provider reference, the model the runtime resolved, and the requesting principal when the entry point knows one (Option<&PrincipalId>). The principal is in the request from the start because adding a field to a trait request that embedders implement against is a breaking change later; a source may use it to choose credentials or quotas, and it grants nothing. ProviderSource::switched_model_provider serves an agent’s mid-session model switch and defaults to model_provider; a source overrides it only when a switch resolves credentials or options differently from a session start, as the runtime’s config-backed source does to keep the agent’s existing switch behavior. ProviderSource::vision_model_provider serves the configured [multimodal] vision route for an image-bearing turn whose provider lacks vision, and also defaults to model_provider; a refusal ends vision routing rather than falling back to config, and the config-backed source overrides it to keep the existing alias-aware vision construction. MemorySource::memory is asynchronous because opening a store can touch disk or the network. MemoryRequest carries the config and agent alias. ToolRequest carries the config, agent alias, resolved SecurityPolicy, selected RuntimeAdapter, and the agent’s memory handle.
On the Agent path, which RPC sessions also use, a selected model route is not resolved through the source. Agent asks the source once, for its default model’s provider. When effort routing, query classification or auto-classify then selects a [[model_routes]] entry, it sends that provider the route as a hint:<name> selector in place of a model name. The config-backed source’s provider decodes these selectors, so configured agents are unchanged. A supplied provider that serves an Agent with model routes must resolve hint:<name> itself, or the agent must have no routes. The CLI loop and process_message instead ask the source again with the route’s provider and model. #12092 tracks doing the same on the Agent path.
The runtime entry point these feed is proposed as follows. It is not in the skeleton, because it cannot exist without an implementation:
#![allow(unused)]
fn main() {
pub struct Runtime { /* config, Arc<RuntimeCapabilities>, DaemonRegistry */ }
impl Runtime {
pub fn new(config: Config, capabilities: RuntimeCapabilities) -> Self;
pub fn with_daemon(self, subsystems: DaemonRegistry) -> Self;
/// One agent turn with no daemon: the independent-consumer path.
pub async fn run_turn(&self, agent_alias: &str, message: &str) -> anyhow::Result<String>;
}
/// Daemon mode: supervise the registered subsystems.
pub async fn run(runtime: Runtime) -> anyhow::Result<DaemonExit>;
}
run_turn is deliberately minimal. The full inbound command path, with session serialization, admission, cancellation, and approval, belongs to the RuntimeIngress contract tracked in #11012. run_turn exists so that the independent-consumer acceptance criterion has a stable surface before that contract lands, and it should become a thin client of RuntimeIngress once it does.
Ownership and lifetime rules
- The runtime owns resolution; sources own construction. The runtime decides which agent, which provider reference, and which generation a request is for. A source never reads ambient state to decide that.
- Capabilities are immutable per generation. A config apply that changes wiring builds a new
RuntimeCapabilities. A turn keeps theArcit started with until it finishes, so a reload never swaps a provider out from under an in-flight turn. - Returned handles are borrowed for a scope. A provider or memory handle belongs to the session or turn that asked for it. Sources may cache and share instances; callers must not stash them past their session.
- One store per agent per generation. Two memory requests for the same agent in one generation must reach the same underlying store. Today’s code guarantees this implicitly by constructing from the same config; the contract makes it an obligation of the source.
- The observer is process-level per generation. It is an instance, not a source, because it is not resolved per agent.
- Channels belong to the supplied source.
ChannelSourcecovers replies by alias, scheduled delivery, approval prompts, and theclisession channel. SuppliedNoneis authoritative; it does not fall back to the process-global CLI factory. The config-backeddefaults::NoOutboundChannelsadapter explicitly mapsclito that registered application factory. The CLI loop retains its existing stdin reader; this adds no inbound turn transport.
Retained sources can receive newer config snapshots during a live model refresh or newly admitted child work. Config-backed sources resolve each request against the supplied snapshot. Hosts must keep retained sources compatible with those snapshots, or rebuild affected sessions and registries when construction wiring changes. A full daemon reload drains old work before replacing its capability set.
Which principal each caller passes
ProviderRequest.principal is the principal the entry point resolved for the turn, and None when it has none. It never widens what a turn may do; a source may use it only to choose credentials or quotas. As callers move onto the contract, each passes:
| Caller | Principal |
|---|---|
CLI agent (interactive and one-shot) | None, as today |
RPC sessions (session/new, rehydration, model changes) | The connection’s resolved principal |
| Cron agent jobs | None: a job records no owning principal today. When job ownership lands, the job’s owner |
| SOP drivers and the heartbeat worker | None: they run as the daemon, not for a caller |
| Channels orchestrator | None until inbound principal resolution supplies one through RuntimeIngress |
With config-backed sources, a None principal constructs and switches exactly as the compatibility adapters do, which is what makes each move a no-op for existing configurations.
Security policy stays with the runtime
The issue requires resolved security policy to remain explicit across the boundary. The rule is:
- The runtime resolves each agent’s
SecurityPolicyandRuntimeAdapterand passes them intoToolRequest. Native sources are trusted implementations required to build tools against those supplied values. Passing them into a Rust trait does not mechanically confine in-process code: a source can return arbitrary native tools, so runtime admission, policy filtering, and per-call gating and approval remain independent requirements. - Building a tool against a policy authorizes nothing. The runtime still gates and approves every call after construction, exactly as today.
- Approval keys one exemption on the tool’s name, not its origin. On a non-interactive turn with no approval back-channel, a tool named
shellskips the outer approval step unlessalways_asklists it, because the native shell enforces its own command policy and approval. A supplied-only registry can hold a source tool with that name, and it gets the same exemption, so a sourceshellmust enforce its own command policy. The other name-keyed checks, for re-entrant agent tools and repeated prompt signatures, only make a call stricter. - Supplied-only construction does not add core tools. Both construction modes remove tools the policy or caller/context gates exclude. A source cannot widen execution authority by returning a tool.
Complete supplied registries and explicit native compatibility
Each eager build asks the tool source once, through ToolSource::native_recipe, which construction it wants. It is asked after memory and the security policy are resolved, and before the model provider is built.
- Supplied-only (the default).
native_recipereturnsOk(None), and the vector fromtools()is the whole registry, even when it is empty. The runtime does not call the native registry factory, load skills, connect MCP servers or peripherals, create the pipeline ortool_searchhelpers, build an unused SOP engine, inject hardware datasheet context, or run post-turn skill creation and skill review. SOP handles the caller already holds stay with the caller. The policy, principal, caller and memory/delivery context filters still apply to the supplied tools. - Native compatibility. A source that wants the previous behavior returns
Ok(Some(&defaults::native_compat_registry));defaults::NoSuppliedToolsdoes this. The recipe builds the native registry with the caller’s own handles, and the source’s tools are added through the same filters. Native names win. A source tool is dropped from the registry and from every child view when it reuses the name of a tool the native recipe built, a peripheral tool, a skill tool (<skill>__<tool>), an MCP tool (<server>__<tool>, including deferred tools that activate later) or an MCP capability tool (mcp_resourcesandmcp_promptsare reserved even when MCP is off), when it reusesexecute_pipelinewhile[pipeline] enabledis set, and when it reusestool_searchand the runtime builds that tool (MCP or built-in schemas are deferred). Source tools never become skill-elevation targets. WASM plugin tools count as native here, because the native recipe loads them: a source tool that reuses a plugin tool’s name is dropped. A plugin tool in turn gives way to the built-in, reserved, peripheral and skill names above.
In both modes a source vector that repeats a name keeps its first tool. Every source tool the runtime drops, for a repeated name or for a native name, is logged as a warning that names the tool and what kept the name.
This changes the contract #11090 documented, where native tools were always built and source tools added on top. A custom source that relied on that must now select the native recipe.
NativeRegistryRecipe is a dyn Fn(NativeRecipeInputs<'_>). NativeRecipeInputs is a #[non_exhaustive] struct with named fields: the ToolRequest, workspace directory, canvas store, subagent flag, forwarded environment, SOP engine and audit log, live config, execution capability and ACP session view. The runtime builds it, so new inputs can be added without breaking recipes. The recipe is Send + Sync + 'static, so a source can own a closure that keeps per-instance state, for example one that wraps defaults::native_compat_registry. native_recipe<'s>(&'s self, &ToolRequest) -> Result<Option<&'s NativeRegistryRecipe>> borrows only the source. A selection or recipe error ends the build. A recipe may let the tools it returns hold the handles they need, but must not keep caller handles anywhere else. When SOPs are on, the entry point builds the SOP engine (reusing the turn’s memory, without a second memory request), or passes the one it was given, and hands it to the recipe; independent delegation passes none.
Selection must not do I/O and must not use request.memory beyond comparing identity. Bounded delegation asks only for the selection: request.memory is then the parent registry’s memory handle, not an opened target store, and neither the recipe nor tools() is called. A source that must refuse bounded inheritance does it by returning Err from selection. A delegate with no bound capabilities uses native compatibility without asking any source; a bound delegate that is missing its runtime or memory refuses.
Bounded delegation recognizes native memory tools by a typed witness stamped when the native factory built them, together with the tool’s Arc identity. A name, role or type that looks like memory does not count. Only recognized native memory tools make the delegate open the target agent’s memory and replace them. In supplied-only mode, a recognized native memory tool in the parent handle makes the delegation refuse before it opens target memory; other source tools keep their own objects and stores. In native compatibility mode, a memory failure refuses without falling back to the parent’s memory. The child delegate carries the target’s tools and the same weak witnesses, so a second hop opens its own target memory. Background and parallel workers carry the same pair and keep the caller’s policy chain and tool-name filter.
Routing a session’s memory to a principal binds the build’s native memory tools to the principal’s memory once, before any memory operation. The tool objects do not change, so pipeline, skill and parent views see the same routed memory. Routing does not reach source tools: a source builds its tools before routing, against ToolRequest.memory, so a source memory tool keeps the handle it was given. In a supplied-only registry that means routing changes the agent’s own memory and none of its tools. Routing after a native memory tool has run is refused and leaves the agent unchanged. An owned session needs a registry built by the native factory (defaults::native_compat_registry, directly or wrapped) or a supplied-only registry.
AllToolsResult now carries private provenance data. Code outside the crate that builds it with a struct literal must use AllToolsResult::from_prebuilt_tools(tools) and then set the public fields it owns. Tools added that way have unknown provenance, so they are never treated as native memory tools.
Parity tests in the first implementation slice must show that the effective tool set and approval behavior are unchanged for every agent in a representative config.
How DaemonRegistry fits
DaemonRegistry is incorporated, not replaced:
- It keeps its role and its current registration methods.
- The channels, cron, SOP, and
RpcContextstarters gain access to the generation’sRuntimeCapabilities, so they stop building providers, memory, and tools of their own. That signature change is its own slice (step 4 below). The gateway starter receives no capabilities: it is deleted at the v0.9.0 cut, and its own construction disappears as its routes move to RPC. - The shared SOP engine and audit logger it carries today are capability-shaped state. They stay where they are in this proposal and are listed as a follow-up to move behind the capability generation.
Relationship to other work
- #6864 was narrowed on 2026-08-23 to mechanical dependency gating. The orchestrator move it originally proposed now belongs to the runtime-owned session work. This proposal does not duplicate either. It supplies the capability contract the moved orchestrator will consume.
- #11012 and its RFC #9487 own the inbound command path (
RuntimeIngress/InboundTurn). This proposal owns what a turn is built from. The two meet at theRuntimehandle, which ingress handlers hold. - #10998 (core tools only in the default runtime) consumes
ToolSource: the optional tools move behind the application’sToolSource, and the runtime keeps only its core tools.
Where the contract lives
The contract needs Config and SecurityPolicy, which live in zeroclaw-config. That rules out zeroclaw-api, which has no workspace dependencies by design. The remaining homes are:
| Option | For | Against |
|---|---|---|
A. zeroclaw-runtime (skeleton’s current placement) | It is the runtime’s public surface; no new crate; callers already depend on it | The crate is the transitional holding crate. Placing new public API there needs a Core Team exception under ADR-016, which this proposal cannot grant itself |
| B. A new contract crate depending on api and config | Clean boundary; application wiring can depend on it without the runtime | A new crate ahead of the planned kernel extraction; the ADR-016 record already notes the risk of establishing boundaries early |
C. The planned zeroclaw-kernel | The destination both active runtime exceptions already name | Does not exist; extracting it first would block this work on the agent-loop extraction |
Decision: option A. The contract lives in zeroclaw-runtime, under the bounded holding-crate exception recorded separately in #11092, which received Core Team approval and merged on 2026-09-28. The scope covers src/composition.rs, the *_with_capabilities entry-point adapters that consume it in src/agent/{loop_.rs,agent.rs,turn/mod.rs}, and the capability-carrying DaemonRegistry starter signatures in src/daemon/{registry.rs,mod.rs}, limited to the #10993 composition API and this page’s migration steps. It does not cover moving concrete provider, memory, or tool construction into this crate.
The intended destination is the composition owner in the planned zeroclaw-kernel extraction. The exception comes up for review at the agent-loop extraction design review, or before the contract adds a capability kind beyond providers, memory, tools, outbound channels, and the observer. The placement prerequisite is satisfied within that recorded scope; it does not approve broader runtime additions or establish implementation acceptance.
Migration order for callers
Every step keeps existing configuration and effective permissions. Old entry points stay as compatibility adapters until their last caller moves, as the issue’s risk section requires.
The table records the staged migration plan. Its tier-1/tier-2 rows describe native compatibility; a supplied-only source skips the native recipe as described above. The measurements and the proposed Runtime::run_turn below are historical, not current results.
| Step | Change | Evidence required |
|---|---|---|
| 0 | This proposal and the skeleton. No callers. | Review of the API boundary |
| 1 | DefaultCapabilities::from_config: sources that wrap today’s create_* functions unchanged | Parity tests for provider, memory, and tool-set resolution per agent |
| 2 | Runtime entry points gain *_with_capabilities forms: agent::loop_::run, process_message_shared, Agent::from_config*, assemble_owned_execution, the delegate tool’s target construction, and cron agent jobs. Old signatures delegate through a config-backed set that reproduces today’s construction | Existing agent tests pass unchanged; policy-propagation regression tests; a stub ProviderSource receives the principal |
| 3 | src/main.rs builds DefaultCapabilities once and calls the new forms for agent and daemon | Startup, cancellation, and shutdown regression coverage for CLI and daemon |
| 4 | The channels starter, cron scheduler, SOP drivers, and RpcContext receive the generation’s RuntimeCapabilities. The gateway starter is deleted at the v0.9.0 cut and receives no capabilities; its own construction disappears as its routes move to RPC | Parity for each migrated starter; no create_* calls left in the migrated starters. The gateway is not migrated in place: the check that it cannot re-couple is the dependency ratchet in step 7, not a rule |
| 5 | Channels orchestrator’s per-sender provider cache becomes a ProviderSource implementation; ACP moves to the new forms. Sequenced with #11012 | No create_* calls left in zeroclaw-channels |
| 6 | Internal callers: delegate, cron, heartbeat, subagents, SOP tool specs | No create_* calls left in the runtime outside DefaultCapabilities |
| 7 | DefaultCapabilities and tier-3 (optional) tool assembly move to the application layer behind its ToolSource. Tier-1 (core) and tier-2 (host-coupled) tools stay runtime-constructed, and the runtime keeps zeroclaw-tools as an explained dependency through v0.9.0 (see below); dropping it is v1.0.0 work. Tools are grouped under three coarse Cargo features, tools-core (always on), tools-host, and tools-extra (both default on), plus per-integration features for heavy dependencies (with #10998) | A dependency ratchet test over cargo metadata with three rules: the runtime (its zeroclaw-tools edge explained), the gateway (no runtime, channels, tools, providers, memory, plugins, config, hardware, or sop-graph), and rpc-proto (api, config, and sop-graph only, no tokio I/O). An independent consumer running a real agent turn through Runtime::run_turn with a ToolSource that returns an empty tier 3 (the existing tests/fixtures/runtime-composition-consumer crate already runs supplied-only and native-recipe turns through run_with_capabilities). Size evidence comparing --no-default-features --features agent-runtime,tools-core against dist per target (#10998 AC5) |
| 8 | Remove the compatibility adapters | Removal is mechanical once no caller remains |
Dependencies that remain, and why
The issue asks for any retained dependency to be explained rather than hidden behind an unused feature. The expected end state is:
zeroclaw-tools: retained through v0.9.0, as an explained edge. Tier-1 core tools are split across the two crates today (shellandfile_readin the runtime;file_write,file_edit,glob_search,content_search, the memory tools,git_operations, andweb_fetchinzeroclaw-tools), and tier-2 tools need runtime internals (the scheduler, canvas store, ask-user handle, SOP engine, session backend, and live config) thatToolRequestdoes not carry. Moving either behind an applicationToolSourcewould leak those internals into a public contract. So step 7 moves only tier 3, and the ratchet names this edge. Dropping the dependency is v1.0.0 work.zeroclaw-providersandzeroclaw-memory: expected to remain for now, for reasons outside turn composition:doctor,quickstartandmigrationconstruct providers or memory directly and are still hosted in this crate, and the config-backed sources build through these crates. Vision routing is inside turn composition: an image-bearing turn asks its provider source for the configured vision route (ProviderSource::vision_model_provider, with the turn’s principal), and only the config-backed source builds that route from config. Each is a separate extraction candidate under the holding-crate plan. Step 7’s ratchet should allow exactly these edges and name them, so a new use fails the check.
Acceptance mapping
| #10993 acceptance item | Delivered by |
|---|---|
Document the public composition API, ownership and lifetime rules, DaemonRegistry, relevant part of #6864 | This page (step 0) |
| Independent consumer runs a real agent turn with supplied capabilities, without concrete provider/channel/tool construction inside the runtime | Partly delivered now by tests/fixtures/runtime-composition-consumer: a crate that depends only on zeroclaw-runtime (without default features), zeroclaw-api and zeroclaw-config runs real supplied-only and native-recipe turns, with traps that catch native construction. The rest is the proposed step 7: Runtime::run_turn would exercise supplied capabilities, but tier-1 core and tier-2 host-coupled tools would remain runtime-constructed. This staged retained-tool design does not fully satisfy the criterion; the remaining construction boundary must be implemented and proved, or the criterion explicitly reconciled in #10993, before acceptance is complete |
| Dependency checks demonstrate the boundary; retained dependencies explained | Partly delivered by the consumer’s dependency ratchet (the runtime’s retained zeroclaw-* edges, each with an owner and a reason, and no concrete constructors in the capability entry points); the step 7 ratchet and the section above cover the rest |
| CLI and daemon use the shared contract, with policy, startup, cancellation, and shutdown coverage | Steps 2 and 3 |
| Implementation PRs and evidence linked to #7432 R1 | Each step’s PR |
Settled decisions
These were open when this page was first proposed. The v0.9.0 architecture review settled both.
run_turnversus waiting forRuntimeIngress.run_turnships and stays minimal, as a client of the same session actor thatsession/promptuses. It does not wait for #11012.- Whether
ProviderSourcereceives the requesting principal. Yes, from the first implementation slice:ProviderRequestgainsprincipal: Option<&PrincipalId>when the capability-taking constructors land. Adding a field to an embedder-implemented request type later would be a breaking change. The capability-taking constructors carry it.