How to let a coding capsule pull the tools it needs
A coding agent does not always know at launch which tools a task will need. Declaring every tool up front gives the model a long tool list it mostly ignores, and a task that needs one more tool fails. With capabilities.install, the operator names the tools a capsule may add, and the model adds them itself: it lists what it may pull, pulls the one it needs, calls it on its next turn, and removes it when it is done. Every step is pinned in murmur.lock and written to the session trace.
What is a pulled artifact?
A pulled artifact is a skill or tool installed into a running session by the capsule itself, rather than declared in murmur.yaml by the operator. Its murmur.lock pin carries origin: runtime, and the model is told it was not vetted by the operator.
The relevant manifest options are:
| Option | Controls |
|---|---|
| capabilities.shell.allow | Which binaries the agent may invoke as shell tools |
| capabilities.install.tool | Which tool artifacts the capsule may pull while it runs |
| capabilities.install.from | Whether a pull may fetch from the scope's GitHub source |
Step 1 — create the manifest
Create a murmur.yaml file for a coding capsule that runs git and cargo, and may pull any Murmur tool:
name: coding-agent
version: "0.1.0"
artifacts:
- name: murmur/driver-anthropic
version: "0.19.0"
runtime: driver
gateway:
endpoint: https://api.anthropic.com
api_key: ${ANTHROPIC_API_KEY}
capabilities:
shell:
allow:
- git
- cargo
install:
tool: ["murmur/tool-*"]
from: [stores, sources]
inference:
transport: http
model: claude-sonnet-5
driver:
artifact: murmur/driver-anthropic
name: coding-agent
version: "0.1.0"
artifacts:
- name: murmur/driver-openai
version: "0.19.0"
runtime: driver
gateway:
endpoint: https://api.openai.com/v1
api_key: ${OPENAI_API_KEY}
capabilities:
shell:
allow:
- git
- cargo
install:
tool: ["murmur/tool-*"]
from: [stores, sources]
inference:
transport: http
model: o3-mini-high
driver:
artifact: murmur/driver-openai
The install block gives the model two tools, pull-artifact and remove-artifact. Without it neither tool exists. tool: ["murmur/tool-*"] admits every tool in the murmur scope whose name starts with tool-, and no skill, hook or driver. from: [stores, sources] lets a pull that neither the project store nor the global store can answer be fetched from the scope's GitHub source by the runtime itself; leave from out and a pull is answered by the stores alone.
capabilities.install works only with transport: http. Under transport: process the harness owns the tool list, and the manifest is refused at load.
Step 2 — install dependencies
mur install
Different ways to install artifacts
mur install fetches an artifact from the source of its scope. murmur/ artifacts come from
the built-in source, murmur-nexus/default-artifacts, with no configuration.
Option A — name the source of a scope in ~/.murmur/config.yaml:
registry:
sources:
- repo: <owner>/<repo>
token: "${GITHUB_TOKEN}"
Then install by full name, with or without a version:
mur install <owner>/<artifact-name>@<version>
Option B — pass a full GitHub reference and skip configuration entirely:
mur install github:<owner>/<repo>@<tag>
See Installing artifacts to learn more.
murmur/tool-edit is not declared, so mur install does not install it. The capsule pulls it.
Step 3 — write the task
Create a task.md file:
The test `parse_empty_input` in this repository fails. Find the cause, fix it with a file-editing
tool you pull for the purpose, run `cargo test`, and remove the tool when you are done.
Step 4 — run the capsule
mur run --task task.md -v
murmur: url localhost:52222
session: ses_019ed2af76b57c42912b612f07dd4d51
workdir: /home/you/coding-agent/workdir/ses_019ed2af76b57c42912b612f07dd4d51
manifest: coding-agent v0.1.0
driver: murmur-driver-anthropic (claude-sonnet-5)
status: ok
Step 5 — follow what the model did
The model's first request offers pull-artifact and remove-artifact beside the shell tools. Their descriptions name what this capsule may pull and from where.
The model lists what it may pull by passing a pattern ending in *:
{"artifact": "murmur/tool-*"}
The result names each tool, its versions newest first, the version a pull would choose, and where it is held. A pattern pulls nothing:
{"pattern": "murmur/tool-*", "matches": [{"artifact": "murmur/tool-edit", "runtime": "tool", "description": "Replaces text in a file. Edits apply in order, each to the result of the one before; each old must match exactly once. All or nothing: if one fails, the file is unchanged and the error names its position, from 1. path is relative to the work folder; an absolute path is refused.\n", "versions": ["0.2.0"], "newest_fitting": "0.2.0", "held_in": ["github:murmur-nexus/default-artifacts"]}], "omitted": 0, "notes": []}
It pulls tool-edit by name. With no version, the runtime takes the newest version that fits this mur:
{"artifact": "murmur/tool-edit"}
<untrusted-content source=tool:pull-artifact>
{"artifact":"murmur/tool-edit","fetched_from":"github:murmur-nexus/default-artifacts","runtime":"tool","tool_name":"tool-edit","version":"0.2.0"}
</untrusted-content>
tool-edit is in your tool list from your next turn.
On its next turn the model is offered tool-edit, its description opening with [origin: runtime, trust: untrusted], and calls it to patch the file. Once cargo test passes, it removes the tool:
{"artifact": "tool-edit"}
<untrusted-content source=tool:remove-artifact>
{"artifact":"murmur/tool-edit","removed":true}
</untrusted-content>
tool-edit is gone from your tool list from your next turn; a call to it is refused.
A pull the grant does not admit, of a version that does not fit this mur, or of a hook or driver, comes back as a tool error opening with not-granted:, not-found:, does-not-fit: or not-fetched:, and writes nothing. The full list is in pull-artifact.
Step 6 — read the lock and the trace
While the tool was installed, murmur.lock held its pin, written by the runtime:
- name: murmur/tool-edit
resolved_version: "0.2.0"
sha256:
any: "<sha256>"
origin: runtime
session: ses_019ed2af76b57c42912b612f07dd4d51
owner_id: 190196424
origin: runtime and session say which session pulled it. owner_id is the GitHub account that owns the murmur scope; a later pull or install from another owner of the scope is refused. The removal deleted the entry. An artifact the capsule pulled and did not remove keeps its pin, but the next launch stages it only if murmur.yaml declares it — see Pin origin.
Read the trace:
mur trace show
Different ways to identify a session
mur trace show with no argument reads the most recent session:
mur trace show
To name another one, pass an ordinal counting back from the newest (@2), the last 4 or more
characters of its ID (3e4b), the full ID, or a path to its trace.jsonl:
mur trace show @2
mur trace show 3e4b
mur trace show ses_6801f81dd28b4a9daf434e8324c4793e
mur trace show path/to/trace.jsonl
Use --workdir <path> if your session directories are not under ./workdir. Every command
that names a session takes the same addresses — see
Session addresses.
Other trace exploration commands
mur trace has four subcommands for exploring session output:
mur trace show — print the full trace for a session to the terminal.
mur trace steps — show a turn-by-turn summary of what the agent did in a session. Pass --verbose to include a truncated summary of each tool's input.
mur trace diff — compare the traces of two sessions side by side, or with no arguments the two most recent. Useful for spotting behavioural regressions between runs.
mur trace report — generate a structured summary report from a session's trace. Covers token usage, tool calls, latency, and other session-level metrics.
── Pulled at runtime ────────────────────────────
murmur/tool-edit@0.2.0 tool runtime/untrusted from github:murmur-nexus/default-artifacts
trace.jsonl holds the detail: an artifact_pulled line with fetched_from and owner_id, and a tools_refreshed line with trigger: "agent" before each inference call whose tool list the model changed — see Session trace.
A pulled artifact is untrusted
The operator never vetted a pulled artifact. The model is told so in its tool list, and its results are fenced as untrusted data. Two controls sit on top of the grant:
- An
on-tool-callpolicy hook decides on everypull-artifactcall, as on any other tool call, so it can deny a pull by name or version. - To make a pulled tool part of the capsule, declare it in
murmur.yamland runmur install murmur/tool-edit@0.2.0. The install adopts the pin asorigin: operator, and the tool is no longer marked untrusted.
Summary
| Feature / setting | How it works |
|---|---|
capabilities.install.tool |
Names the tools the model may pull; hooks and drivers are never pullable |
capabilities.install.from |
[stores] by default; [stores, sources] also fetches from the scope's GitHub source, writing to no store |
pull-artifact |
Pulls <scope>/<name> or <scope>/<name>@<version>, usable from the next turn; a pattern ending in * lists instead |
remove-artifact |
Removes a tool the session pulled; later calls to it are refused |
murmur.lock |
Pins each pull with origin: runtime, the session and the scope's owner id |
trace.jsonl |
artifact_pulled per pull, tools_refreshed with trigger: "agent" per change |
| Trust | Pulled artifacts are marked untrusted; a policy hook can gate pulls, and mur install adopts a pin |