Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 toStart from
Run the agent under your own UI, transport or runtimeThe WIT world and templates/host
Ship the agent to a machine without linking Rustpackages.agent-component
See a complete host with threads, sockets and peerscrates/montana/
Add an effect the agent can performThe 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.