Hacking
Cowboy’s core is a headless state machine shared by the Zellij plugin and Montana. Hosts supply asynchronous effects and the filesystem access the guest uses. The optional OCI runtime embeds Montana. This guide explains where to extend that implementation.
| You want to | Start from |
|---|---|
| Run the agent under your own UI, transport or runtime | The WIT world and templates/host |
| Ship the agent to a machine without linking Rust | packages.agent-component |
| See a complete host with threads, sockets and peers | crates/montana/ |
| Add an effect the agent can perform | The Host trait in crates/core/src/host.rs |
The contract: the WIT world
specs/wit/cowboy-agent.wit defines cowboy:agent@0.2.0, the one interface
every non-Zellij host programs against. It exports agent — initialize,
handle-command, handle-host-event, snapshot, describe — and imports
host — exec, http, set-timer, close-self, spawn-peer,
own-agent-id. Commands and host events are separate exports so a transport
message can never be mistaken for an effect completion; effect results carry
an opaque correlation the host echoes back unchanged; and an effect request
is not an authorization, so the host stays responsible for policy. The
Component ABI chapter walks through each of those
rules; the file itself is short enough to read first.
WIT defines the custom interface; the guest also needs WASI filesystem grants.
The JSON inside snapshot is an opaque display model, not a stable schema.
The config map given to initialize is the flat string map core parses;
crates/contracts/src/keys.rs inventories recognized keys. A new host-provided
capability needs an import and implementations in the affected adapters.
The artifact: packages.agent-component
Build from source; see Installation for
release availability. nix build .#agent-component produces share/cowboy/cowboy-agent.wasm, the
agent core compiled to wasm32-wasip2 and wrapped by crates/component/ so
it exports exactly the world above. The wrapper, crates/component/src/lib.rs,
is small: it maps initialize onto AgentHarness::on_load, handle-command
onto AgentHarness::handle_intent, handle-host-event onto
AgentHarness::handle_event, and snapshot and describe onto
AgentHarness::frame_json and AgentHarness::describe_json, and it
implements the core’s Host trait by calling the world’s imports. A second
output, agent-component-lite, is the portable build without the NixOS-only
pieces.
An external flake gets it as cowboy.packages.<system>.agent-component. That
is how a host stays on one cowboy revision for both the component and the WIT
it was built from.
The starting point: templates/host
nix flake init -t github:dmadisetti/cowboy#host
nix develop --command cargo run
The template is the smallest embedder that runs: a flake taking cowboy as an
input, and one Rust binary (templates/host/src/main.rs) that instantiates
the component with wasmtime, answers the six imports in-process, calls
initialize with a tiny config map, submits one prompt, prints every
snapshot as a JSON line and exits. Its HTTP handler returns an error, timers
are delivered without waiting, and peer spawning is ignored. Replace those
stubs before using it for real agent work. Its devShell links wit/ to the cowboy
input’s specs/wit so the bindgen! bindings track the same revision as the
component. templates/host/README.md explains each export, each import, and
what to replace to put a real UI, HTTP client and clock around the loop.
The headless reference: montana
crates/montana/ is the host cowboy itself ships: the daemon under systemd on
NixOS, the process inside the container image, and the library the OCI
runtime (crates/runtime/) calls in-process. crates/montana/src/agent.rs implements the imports with threads —
exec and http run off the guest thread and post their results back into
one channel, timers are additive sleepers — and crates/montana/src/lib.rs
assembles the config map, resolves and preopens the agent’s directories at
their own paths, and starts the control socket. Frames stream out as ndjson
and intents come in the same way (specs/CONTROL.md); the optional A2A front
in crates/a2a/ is a protocol adapter over describe and handle-command
(specs/A2A.md). Read it when the template’s answer to a question is “a real
host does more here”.
Inside the component: the Host seam
Hosts that can link Rust do not need the component at all. The Zellij plugin
(crates/harness/) links cowboy-core directly and supplies a Host
implementation of its own. That trait, in crates/core/src/host.rs, is the
seam the WIT world mirrors. Commands and HTTP requests return results later
as events correlated by an opaque Ctx map. Timers, pane operations and peer
lifecycle requests also go through the host. AgentHarness::with_host takes the implementation; the entry point
after that is AgentHarness::on_load, in crates/core/src/harness.rs.
For a new asynchronous effect, update the Rust host trait and its adapters. If component hosts need it, update WIT and the component and Montana bindings as well. Terminal-only view hooks can remain Rust-side; headless adapters have no terminal view to update.
What is not an extension point
- Configuration keys are parsed in one place,
crates/core/src/config/; a new knob is a new key there and a new line in the launcher that emits it, not a host-specific side channel. - Tools and skills have configuration and module interfaces; see Plugin Architecture.
- A host’s boundary includes effect policy, WASI preopens and the enclosing process permissions, mounts and network controls. The Security Model describes the managed deployment; a new embedder must supply its own controls.