Skip to content

WIT Interfaces

WASM artifacts — capsules, tools, drivers, and hooks — talk to the runtime through WIT interfaces. This page lists each interface, who uses it, and what its behaviour means for you. The type definitions themselves live in the repository under crates/capsule-runtime/wit/; each row below links to its source, and Package versioning lists the version each one carries.

Interfaces

Interface Used by Purpose
murmur:tool/run Exported by tool and driver components The entrypoint the runtime calls to run a tool or an inference driver
murmur:tool-registry/invoke Imported by capsule components Call an allowlisted tool by name
murmur:capsule/run Exported by capsule components The capsule entrypoint
murmur:artifact-manager/manage Provided by the runtime to capsule components List, describe, and pull artifacts during a session
murmur:shell/execute Implemented natively by the runtime Run an allowlisted shell binary
murmur:message/send Provided by the runtime to capsule components Send an A2A task to a peer capsule
murmur:task/task Imported by tool components Pause the agent loop and wait for external input
murmur:text/chunks Imported by tool and driver components Emit response and thinking chunks to the session's SSE stream
murmur:hook/lifecycle Exported by hook artifacts The lifecycle handlers the runtime calls
murmur:runtime/inference Provided by the runtime to hook components Run one LLM completion through the capsule's configured driver
murmur:runtime/tokens Provided by the runtime to hook components Count the tokens in a string the way the runtime counts them
murmur:task-io/read Provided by the runtime to hook components Read the task's input text and the agent's result text
murmur:conversation/read Provided by the runtime to hook components Read the capsule's durable conversation record

Worlds

A world is what your component's source compiles against with wit_bindgen::generate!.

World Imports Exports Defined in
capsule tool-registry/invoke capsule/run guest/worlds.wit
tool task/task, text/chunks tool/run guest/worlds.wit
driver text/chunks tool/run guest/worlds.wit
hook runtime/inference, runtime/tokens, task-io/read, conversation/read hook/lifecycle hook/worlds.wit
runtime-host artifact-manager/manage, shell/execute, tool-registry/invoke, message/send host/host.wit

Agent capsules compile against no world — the agent loop runs inside the runtime, and the capsule is defined by its manifest alone.


murmur:tool/run

A tool-result carries a metadata list — a free-form key/value channel back to the runtime. These keys are reserved:

Key Value Effect
continuation_id Opaque id string Tells the runtime the driver is holding conversation state provider-side. See Stateful driver continuation.
state_effect "read" or "mutate" How this call affected the resource it addressed. Recorded on the call's tool_call trace event.
resource_id Opaque, tool-defined string Which resource this call addressed. Recorded verbatim on the tool_call trace event.

state_effect and resource_id together let mur trace spot redundant calls without knowing anything about your tool. Omit either and the call counts as "unknown": it is never credited as a redundant read.

For resource_id, scope the value by addressing scheme (for example "sym:Foo::bar") so it cannot collide with an unrelated resource of the same name. Two calls address the same resource when their resource_id values are byte-equal. Without one, the redundancy detector guesses from path-like input fields (path, file, file_path, filepath, filename) — so declare the key if your resource is a symbol, URI, or query.

Returning a large result

A tool with more output than it wants to carry in data writes the output to a file and returns the filename in data-path. Name that file relative to the session workdir, the directory the tool's own . resolves to. When the runtime reads the file on the tool's behalf, it resolves the name beneath that directory and refuses anything that leaves it:

data-path names Outcome
A regular file under the session workdir Its contents become the result
An absolute path, or one climbing out with .. The call fails
A path passing through a symlink, wherever the symlink points The call fails
A file that does not exist The call fails
A file over 10 MiB The call fails
A path holding % escapes Decoded before it is resolved, so %2e%2e fails like ..

A result carrying data uses it verbatim and never opens data-path.

Stateful driver continuation

By default the runtime resends the full conversation history on every turn. A driver holding conversation state provider-side can opt out by returning a non-empty continuation_id.

Situation What the runtime does
Driver returns a non-empty continuation_id Holds the id for the rest of the session loop
Next turn, same context_id Sends only the messages appended since the driver last acknowledged state, plus the held id
Next turn, different context_id Full resend — a continuation is never reused across unrelated tasks
Driver omits the key or returns an empty string Drops the held id; the next turn is a full resend
A hook commits replace-context (for example compaction) Drops the held id; the next turn resends the post-compaction history

Token accounting for the compaction threshold is always computed from the full conversation, not the smaller payload actually sent, so continuation never changes when compaction fires.

Prompt cache routing hint

Every request the runtime hands a driver carries a top-level prompt_cache_key string. Its value is fixed for every turn of a task, and it does not change when history is compacted or when a held continuation_id is dropped:

Task prompt_cache_key
Has a context id, and the value fits in 64 characters <capsule-name>:<version>:<context-id>
Has no context id, and the value fits in 64 characters <capsule-name>:<version>
The value would run past 64 characters The leading characters of the same value, then : and a 16-character hex suffix

A provider that routes on the value keeps a task's turns on one machine, so each turn reaches the cache entry the previous turn warmed. Two runs of the same capsule get different values, because a context id is minted per task — including when the value was shortened.

Shortening keeps the value inside the 64 characters the OpenAI Chat and Responses APIs accept; a capsule name of ordinary length already runs past that once a context id is appended. A shortened value still opens with the capsule's own name, so a key seen in a payload dump names the capsule that produced it.

A driver reads the field only if it declares it, so a driver that ignores it runs unchanged. Forward it only where the provider defines a field of its own for it: the OpenAI Chat and Responses APIs accept prompt_cache_key in the request body, and the Anthropic Messages API rejects a body carrying any field it does not define.

The [Capsule] block the runtime prepends to every system prompt is a pure function of the capsule's name and version — it names no host-specific path, so it is byte-identical across every launch of the same capsule and version. Prefix caching depends on that stability: a provider matches its cache against the bytes it has seen before, and the first block of every prompt would otherwise never repeat.

Reported token usage

A driver response may carry a top-level usage object holding the provider's own token counts for that call:

{
  "stop_reason": "end_turn",
  "content": [{"type": "text", "text": "..."}],
  "usage": {
    "input_tokens": 12043,
    "output_tokens": 218,
    "cached_tokens": 11780,
    "cache_write_tokens": 0
  }
}
Member Value
input_tokens Tokens the provider billed for the request
output_tokens Tokens the provider billed for the completion
cached_tokens Request tokens served from the provider's prompt cache
cache_write_tokens Request tokens written into the provider's prompt cache

Every member is optional and every member is a non-negative integer. A driver reports whichever members its provider returned; a provider with no prompt cache reports no cache members. Omit a member the provider did not report rather than sending 0 — the runtime keeps the two apart, and a 0 reads as a genuine cache miss on the trace.

Where the two provider shapes carry each number:

Member Anthropic Messages API OpenAI Chat and Responses APIs
input_tokens usage.input_tokens usage.prompt_tokens
output_tokens usage.output_tokens usage.completion_tokens
cached_tokens usage.cache_read_input_tokens usage.prompt_tokens_details.cached_tokens
cache_write_tokens usage.cache_creation_input_tokens Not reported

The runtime records the reported numbers on the call's inference trace event and its capsule.inference span, and acts on none of them: the compaction threshold and every budget decision keep running on the runtime's own pre-flight estimate. See Observability schemas for the field names.

A malformed report degrades to no report rather than failing the call:

What the driver returns What the runtime records
No usage Nothing
A usage that is not an object Nothing
A member that is not a non-negative integer Nothing for that member; its well-formed siblings are still recorded
A member the runtime does not define Nothing for that member

An omitted number is absent from the trace event, never written as 0.


murmur:artifact-manager/manage

Lets a capsule component inspect and install artifacts mid-session: list and describe report what is installed, diagnostics returns the session id alongside that list, and pull installs a new artifact. search and remove are unimplemented: calling either returns an error.

pull resolves an artifact from the session's registry, verifies its bytes against the registry hash and any pinned murmur.lock entry, then installs it under <workdir>/tools/<name>/ and updates murmur.lock. A hash mismatch, or a version or hash that conflicts with an existing murmur.lock pin, returns an error before anything is written. A pulled artifact is immediately visible to list() and describe() and, for WASM tools, callable via invoke().


murmur:shell/execute

The contract for running an allowlisted shell binary. The runtime implements it natively.

Behaviour Detail
Non-zero exit code Data, not an error — the call only errs on spawn/IO failure
truncated: true stdout or stderr exceeded 16 KiB and was cut; full-output-path points at the untruncated log in the workdir
Allowlist binary must appear in capabilities.shell.allow; anything else is rejected before the process is spawned

murmur:task/task

request-input pauses the agent loop at a decision boundary and waits for a reply delivered by message/send. From the component's perspective the call is synchronous: pass a prompt string, receive the reply string.

Call it when the agent has reached a decision it cannot make on its own — ambiguous requirements, or a consequential action that needs approval — and a human or supervisor capsule is expected to reply.

It is available in the tool world only, and only while the capsule is running an A2A task. Called anywhere else, it returns an error immediately.

Timeout: lifecycle.input_timeout_secs in the manifest (see lifecycle.input_timeout_secs). When it fires, the component is aborted and the task transitions to failed.


murmur:message/send

Sends a task to a peer capsule. send takes the peer URL and a message — a message-id, an optional context-id, and the text — and returns the peer's task-id, context-id, and state. The runtime handles the JSON-RPC 2.0 wire format, so the capsule never sees it.

Behaviour Detail
Allowlist The peer URL must appear in the sender's capabilities.network.allow. Otherwise the call returns Err("network policy: '...' not in capabilities.network.allow") and no connection is made.
Tracing With OTel configured, the runtime injects a W3C traceparent header so the peer's session span nests under the sender's.
Origin The runtime stamps x-murmur-task-origin: peer and the sending task's own x-murmur-task-trust on every request, so the receiving capsule inherits the sender's trust class. The message record has no field for either, so a capsule cannot set them. See Task origin and trust class.
Result state task-result.state is the peer's response to the send: submitted, working, input-required, completed, failed, rejected, or canceled. Poll the peer's tasks/get endpoint for the final state.

murmur:hook/lifecycle

Hook artifacts (runtime: hook) export this interface. The runtime calls each handler synchronously. Returning an error logs it to logs/hook-<name>.log in the workdir and the session continues — except at on-compaction, where the error ends the session, because the runtime has no other way back under the token budget:

on-compaction error Session exit_status
Follows a run-inference call a spend ceiling refused spend_ceiling_reached
Any other failed
Handler Fires
on-stage Once at capsule staging, before any session starts
on-session-start Once per capsule launch, before the first task's work begins
on-task-start Once per task, before that task's first inference turn
on-inference After each inference driver response is parsed
on-tool-call Twice per model-requested tool invocation: once before it is dispatched, once after it returns or errors
on-shell Twice per allowed shell command: once before it is dispatched, once after it returns
on-compaction When the session token threshold is reached, before any history is replaced
on-task-end Once per task, immediately after that task's agent loop returns
on-session-end Once per capsule launch, after the task loop exits

With task_acceptance: none or single, on-task-start coincides with on-session-start; with queue it fires once per queued task. session-id is the session identifier — ses_ followed by 32 hex characters — and is the same value in every hook event of a run.

on-task-start and on-task-end are optional exports: a component that omits them loads and is simply never dispatched for them. The six session handlers are required — a component missing one fails to load with an error naming it. A missing on-stage is logged rather than fatal, and staging continues.

The two dispatches of a gated event

on-tool-call and on-shell are each dispatched twice, and outcome is what tells the two apart:

outcome Dispatch Meaning
none Decision point, before the call The call has not run. A returned deny refuses it.
some(...) Observation, after the call The call has run. Nothing returned can change that.

The decision point runs only for a hook whose own murmur.yaml declares commit_policy: deny on binding: on-shell or binding: on-tool-call. Every other hook sees the observation dispatch alone.

What each handler can commit

A handler may return any hook-output arm, but the runtime commits only one arm per event:

Handler Committed arm
on-stage write-manifests
on-inference artifact
on-compaction replace-context
on-task-end reopen-task
on-task-start seed-context
on-shell, on-tool-call deny, at the decision point only
All others none — only none is silent

This table is also enforced ahead of time. A hook artifact declares in its own murmur.yaml which handler it binds to (binding:) and what it expects committed (commit_policy:), and the runtime checks the pair against this table when the capsule is staged. A commit_policy the binding's handler cannot commit (say commit_policy: reopen-task on binding: on-stage) fails staging with an error naming the binding, the declared policy, and the one this binding honors, rather than becoming a mid-session dispatch fault. A hook with no binding: receives every event, so any commit_policy is valid for it. See Hook contract fields.

Returning none is always silent, and is the normal case for an observational hook. Returning any other non-none arm is a loud but non-fatal fault:

  • A line naming the hook, the handler, and the discarded arm is appended to logs/hook-<name>.log.
  • A hook_dispatch_error event is written to trace.jsonl — see Session trace schema. The one exception is on-stage, which runs before the trace file exists. An async hook's fault reaches the trace by the same path; the trace and the log are the only places it appears, since the agent loop never sees an async hook's return value.

reopen-task is a control arm: returning it from on-task-end re-runs the task's agent loop with the arm's string injected as feedback, subject to lifecycle.max_task_reopens and inference.max_turns. See Task reopening.

seed-context is honored at on-task-start alone: the first bound blocking hook to return it wins, and its messages are placed at the head of the task's context, ahead of any loaded history and ahead of the task message. The list is chronological, oldest first. The host measures it against task-start-event.budget-tokenscontext.max_tokens × context.seed_budget — and commits it whole, drops its oldest messages, summarizes the overflowing front through the compaction hook, or refuses it; every outcome is recorded as a context_seed event, and a refusal never fails the task. Returning it from any other handler is the same loud-but-non-fatal fault as any other unhonored arm. Only binding: on-task-start accepts it as a commit_policy.

deny is a control arm honored at the decision-point dispatch of on-shell and on-tool-call alone: the call is not made, the agent is handed a result naming the hook and the reason, and a call_denied event is written. It only narrows — there is no arm that permits a call, and the manifest's own capability check still runs first, so a hook can never make a capsule able to do something its manifest did not allow. Returning it from any other handler, or from the observation dispatch of these two, is the same loud-but-non-fatal fault as any other unhonored arm. See Policy hooks for the fail-closed rule that governs a policy hook's failures.

Event field notes

  • compaction-event.model and compaction-event.system-prompt carry inference.compaction.model and inference.compaction.system_prompt verbatim, so a compaction hook knows which model and which prompt to use for its own summarization call. Setting inference.compaction.system_prompt_file instead delivers that file's contents. Either field is absent when the manifest leaves it unset, and the hook resolves its own default.
  • message.id and message.source-id are runtime bookkeeping carried alongside a message's role and content. id is msg_ followed by a uuid — an identity, so two byte-identical messages still carry different ids. source-id is opaque: whatever produced the content sets it, and the runtime records it verbatim without parsing it. Both are stripped before the driver payload is built, so neither reaches the provider — a uuid at the head of a cached prefix would break prompt-prefix caching on every request. The runtime mints an id for every message it builds out of a hook-returned message list, so an id a hook sets on a message it returns is replaced rather than kept; a source-id a hook sets is carried verbatim, and the field is absent when the hook set none.
  • task-start-event.context-window is the capsule's context.max_tokens, or 0 when the manifest declares no context: block. It is precomputed so a hook sizing its work against the window never has to know which model the session runs.
  • task-start-event.budget-tokens is the ceiling a seed-context returned from this event is enforced against: context.max_tokens × context.seed_budget, rounded down. task-start-event.prior-tokens is the token count of the conversation history the task will load under lifecycle.conversation: threaded, and 0 under every other conversation mode. Read 0 as "the runtime has not computed this" and decline, rather than as an unbounded budget.
  • shell-event.binary is the program the shell tool invoked — a canonicalized absolute path (for example /usr/bin/pytest) when the runtime resolved the name against PATH, and the bare invoked name when nothing resolved.
  • shell-event.command carries the argument list alone (for a shell interpreter, the script text passed via -c), truncated to 200 characters. It is a display string. shell-event.argv is the exact argument list the runtime passes to the executable and shell-event.script is the -c body (absent for every other form), both untruncated — a policy must decide on those two and on binary, never on command.
  • shell-event.recipe is the body of the recipe a build-tool call names, read by the runtime out of the capsule's workdir. A hook holds no grant on that directory and reads nothing itself.

    Invocation Body
    make <target> the target's recipe lines, from GNUmakefile, makefile or Makefile
    just <recipe> the recipe's indented body, from justfile, .justfile, Justfile or JUSTFILE
    npm run <script> the string value of scripts.<script> in package.json

    The body is the file's own text: variables, interpolations, conditionals and recipe arguments are carried as written. It is absent when the call names no recipe, and when the runtime could not resolve one:

    • no recipe file in the workdir, or one that is unreadable, oversized or not UTF-8
    • a target, recipe or script that is not defined, or one reached through a make pattern rule or a just alias
    • a form that could resolve to more than one body: two make rules for one target, a duplicate just recipe, a make recipe written on the rule line as target: ; cmd, or an npm script with a pre/post sibling
    • an invocation carrying flags beyond the recipe file and working-directory selectors, or a make variable override such as make build VERSION=2

    Read an absent body as "the runtime has no body to show", which is a statement about the resolution and not about the call. - tool-event.input is the exact tool input JSON the tool will receive, untruncated, and is what a policy decides on. - shell-event.outcome and tool-event.outcome carry what the call produced, and are absent at the decision point — see The two dispatches of a gated event.


murmur:runtime/inference

A runtime-provided import available to any hook component that declares it. Nothing needs to be exported and nothing in the manifest changes.

run-inference runs exactly one LLM completion through the capsule's configured driver (inference.driver.artifact).

Case Behaviour
No model given Uses the manifest's inference.model
model given Sends that model. If the driver or provider rejects it the call returns an error; the runtime never silently falls back. To retry on the primary model, call again without a model.
No system-prompt given No system prompt is sent
No driver configured Returns an error naming inference.driver.artifact. The import still links, so the hook itself runs.
A spend ceiling refuses the call Returns an error starting spend ceiling reached: that names inference.max_session_tokens or spend.machine_tokens_per_day. Nothing is sent, and retrying will not succeed.

model-used is the model string the runtime actually sent. input-tokens and output-tokens are runtime-side tiktoken counts of the request payload and the raw driver response.

Every call that reaches the driver, success or failure, writes one inference record to trace.jsonl and one capsule.inference OTel span carrying origin: "hook:<hook name>" and model.


murmur:runtime/tokens

A runtime-provided import available to any hook component that declares it. It needs no capability grant and no manifest entry: counting text reaches no resource.

count returns the runtime's own cl100k_base count of the string — the same number behind the compaction trigger and the context-occupancy calculation. A hook measuring a payload against a budget and the runtime enforcing that budget therefore agree on what the payload costs, which a hook's own tokenizer would not guarantee.


murmur:task-io/read

A runtime-provided import available to any hook component that declares it. It hands a hook the text of the task its capsule was given and the result text the agent produced, so an output gate or an archiver needs no filesystem grant to see either.

Reading is granted per hook with capabilities.task_io.read: true on that hook's entry in the capsule manifest. A hook without the key still links and still runs — every function returns not-granted.

Function Returns
input-len(form) Byte length of the task input
read-input(form, offset, max-bytes) A window of the task input
output-len() Byte length of the agent's result text
read-output(offset, max-bytes) A window of the result text

Ask for a length first, then read the window you can afford: the runtime imposes no truncation cap of its own. A read returns the longest prefix of the value from offset that fits in max-bytes and ends on a character boundary, so a multi-byte character is never split. Advance offset by the byte length of what you got back. An empty return with offset still below the length means max-bytes was too small for the next character.

form picks which rendering of the input to read:

Form Text
as-given Exactly what this attempt's agent loop was handed, including any feedback a previous reopen appended. What an output gate judges against.
original The task before any reopen feedback was appended. What an archiver or cost-attribution hook wants.

On a task that has never been reopened the two are identical.

When a task is readable

A task is in scope from the moment one of its attempts enters the agent loop until the runtime finishes with that task, reopens included.

Lifecycle event input-* output-*
on-stage no-task no-task
on-session-start no-task no-task
on-task-start no-task no-task
on-inference, on-tool-call, on-shell, on-compaction This attempt's input no-output until the loop finishes
on-task-end This attempt's input This attempt's result text, or no-output if it ended without one
on-session-end no-task no-task

on-task-start fires before the task enters the loop and on-session-end after the last task has left it, so an archiver binds to on-task-end. On the second attempt of a reopened task the output is cleared at attempt start: a hook judging attempt 2 never sees attempt 1's result.

An execution_mode: async hook reads whatever is in scope when its worker reaches the call, which may be after the task has left scope. Bind a hook that reads the task as blocking.

Error Meaning
not-granted The hook's entry does not declare capabilities.task_io.read: true
no-task No task is in scope at this lifecycle event
no-output A task is in scope but the loop produced no result text
out-of-range offset is past the end of the value, or inside a multi-byte character

murmur:conversation/read

A runtime-provided import available to any hook component that declares it. It hands a hook the capsule's durable conversation record — every message the runtime put in front of the model, newest first, paged — without a filesystem grant. No artifact gets a filesystem path into ~/.murmur/conversations/; this interface is the only way in. The runtime is the only writer, and there is no write function.

Reading is granted per hook with capabilities.conversation.read: true on that hook's entry in the capsule manifest. A hook without the key still links and still runs — read-messages returns not-granted.

Function Returns
read-messages(cursor, limit) One message-page: messages, next-cursor, total

messages[0] is the most recently appended message, and each carries the id its record line holds.

Field Meaning
cursor An opaque token. none starts at the newest message; pass back the next-cursor of the page you just read, unmodified.
next-cursor none once a page has reached the oldest message in the record, which is what ends a paging loop.
limit Clamped to 1..=100, so 0 reads one message and a loop always makes progress.
total Parseable messages in the whole record at the moment the page was served, not in this page.

A cursor stays valid while the runtime appends to the record underneath a paging hook: it names a position counted from the oldest message.

total is a snapshot, not a loop invariant. The runtime appends while a hook pages, so it grows from one page to the next and the page lengths of a completed walk can sum to less than the final total. Every page is a consistent snapshot anchored at the oldest message, so nothing repeats and nothing is skipped. Never size a paging loop from total; stop on next-cursor: none.

A message whose role is "tool" reaches a reader with its content replaced by a JSON object, because a tool result carries more than the one string content has room for.

Key Value
__murmur_tool_msg__ Always true, and what marks the envelope
tool_call_id The call this result answers, or null
is_error Whether that call failed, or null
body The tool result as the runtime holds it

This is the same encoding a compaction hook receives on murmur:hook/lifecycle's message — one encoding, in both places.

Error Meaning
not-granted The hook's entry does not declare capabilities.conversation.read: true
invalid-cursor A cursor naming a position outside the record
unavailable: <reason> The record exists and could not be read

A record that does not exist is not an error: the read succeeds with no messages and total: 0. That is what a hook sees before the first task of a launch, under context.record: off, and in a capsule running inference.transport: process — whose CLI owns its own conversation, so the runtime puts no message list in front of a model and writes no record.

The record in scope is the one belonging to the task the hook is being dispatched for, which is the context-id on task-start-event. A hook bound to on-task-start therefore reads the conversation its task is about to continue.


Package versioning

Every murmur:* package declares an explicit @x.y.z version, so the contract a compiled .wasm artifact was built against is readable from the component binary itself.

Package Version
murmur:tool 0.1.0
murmur:tool-registry 0.1.0
murmur:capsule 0.1.0
murmur:artifact-manager 0.1.0
murmur:shell 0.1.0
murmur:message 0.1.0
murmur:task 0.1.0
murmur:task-io 0.1.0
murmur:conversation 0.1.0
murmur:text 0.1.0
murmur:hook 0.8.0
murmur:runtime 0.3.0
murmur:host 0.1.0
murmur:runtime-guest 0.1.0
Tier When
Patch Doc or comment edits, no ABI change
Minor A wholly new function or interface the runtime treats as optional to export
Breaking Any signature change, any change to an existing record or variant, or removing or renaming a function or interface

Every murmur:* package is pre-1.0, so the minor field is the breaking axis: a breaking change to 0.4.0 ships as 0.5.0, not 1.0.0. Only a patch moves the patch field.

Adding a field to an existing record or a case to an existing variant is always breaking, never minor: every field and case is positional in the binary encoding, so an addition changes the shape of every call that carries the type.

A wholly new interface goes in a new package at 0.1.0 when the package that would otherwise host it already has published consumers. The package version is part of every instance name in that package, so a minor bump renames interfaces the new one has nothing to do with, and every artifact importing one of them stops loading until rebuilt. The exception is a change set that already forces that rebuild for another reason, in which case the new interface joins the existing package instead: murmur:runtime/tokens sits in murmur:runtime because the same bump that introduced it took murmur:hook to 0.6.0 and rebuilt every hook regardless.

One accepted version per interface. The runtime resolves each interface by its versioned name and nothing else — there is no compatibility fallback for an earlier version or for an unversioned name. An artifact built against a different version fails to load, with an error naming the version the runtime expects and a rebuild hint (mur install for a default artifact, or a source rebuild otherwise). When a package is bumped, every artifact exporting it must be rebuilt and republished.

An installed artifact carries the versioned interface names its own component declares in the wit_contracts key of its <name>-<version>.meta.json; see Local artifact cache. mur list --contract <PREFIX> lists the installed artifacts touching a package.

Full policy in wit/VERSIONING.md.