In this section

The structure of a cartridge: entry point, manifest, cap definitions, and handler registration.

What is a cartridge

A cartridge is a standalone binary that communicates with the engine via the CapDAG protocol over stdin/stdout. Each cartridge provides one or more capabilities (caps). The engine discovers cartridges, spawns them on demand when their caps are needed, and manages their lifecycle through the CartridgeHostRuntime.

Existing cartridges:

Name Language Framework Caps
ggufcartridge Rust llama.cpp LLM text generation, vision, embeddings, vocab, model info
candlecartridge Rust Candle BERT embeddings, CLIP, BLIP, Whisper
mlxcartridge Swift MLX LLM text generation, vision, embeddings
pdfcartridge Rust — Thumbnail, metadata, outline, disbind
txtcartridge Rust — Thumbnail, metadata, outline for text files
modelcartridge Rust — Model downloading and caching

Entry point

Rust

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let manifest = build_manifest();
    let mut runtime = CartridgeRuntime::with_manifest(manifest);

    runtime.register_op("cap:in=...;out=...;generate", || Box::new(GenerateOp));
    runtime.register_op("cap:in=...;out=...;extract;target=metadata", || Box::new(MetadataOp));

    runtime.run().await?;
    Ok(())
}

The #[tokio::main] attribute sets up the multi-threaded async runtime. run() detects whether the binary was launched with CLI arguments (CLI mode) or without (CapDAG cartridge mode) and handles both.

Source: ggufcartridge/src/main.rs, candlecartridge/src/main.rs, pdfcartridge/src/main.rs, txtcartridge/src/main.rs.

Swift

@main
struct MLXCartridge {
    static func main() {
        let manifest = buildManifest()
        let manifestData = try! JSONEncoder().encode(manifest)
        let runtime = CartridgeRuntime(manifest: manifestData)

        runtime.register_op_type(capUrn: CAP_GENERATE, make: GenerateTextOp.init)
        runtime.register_op_type(capUrn: CAP_DESCRIBE, make: DescribeImageOp.init)

        try! runtime.run()
    }
}

Swift cartridges use @main for the entry point. run() blocks until stdin closes.

Source: mlxcartridge/Sources/mlxcartridge/main.swift.

Manifest

The manifest tells the engine what this cartridge can do. It is a CapManifest struct serialized to JSON and sent during the HELLO handshake:

let caps = vec![
    identity_cap(),        // Required: identity verification
    thumbnail_cap,
    metadata_cap,
    outline_cap,
    disbind_cap,
];

let manifest = CapManifest::new(
    "pdfcartridge".to_string(),
    "1.0.0".to_string(),
    "PDF document processing".to_string(),
    vec![CapGroup::default_group(caps)],
);

Fields:

  • name: Cartridge name.
  • version: Semver version string.
  • description: What this cartridge does.
  • cap_groups: List of CapGroup instances, each containing a named set of Cap definitions and optional adapter_urns. Use CapGroup::default_group(caps) for a single unnamed group.
  • author (optional): Maintainer name.
  • page_url (optional): Documentation or repository URL.

The cap_groups structure exists to support adapter routing: a group can declare adapter_urns that identify which media adapters are available for that group’s caps. For most cartridges, a single default group with no adapter URNs is correct.

Source: capdag-rs/src/bifaci/manifest.rs, capdag-rs/src/cap/definition.rs.

Identity cap

Every manifest must include the identity cap, identity_cap() in Rust or the equivalent in Swift. This cap handles the identity verification step during handshake (see Handshake). CartridgeRuntime::new() panics if CAP_IDENTITY (cap:effect=none) is missing from the manifest.

The identity handler is registered automatically by the runtime. You only need to include identity_cap() in the manifest’s cap list.

Source: capdag-rs/src/standard/caps.rs.

Cap definition

A Cap defines a single capability:

let cap = Cap::new(
    CapUrn::from_string("cap:in=\"media:document;ext=pdf\";out=\"media:record\";extract;target=metadata").unwrap(),
    "Extract Metadata".to_string(),
    "extract-metadata".to_string(),  // slug (CLI subcommand name)
);

Fields:

  • urn: The CapUrn identifying this capability. Defines the input/output media types and operation tags.
  • title: Human-readable name for display.
  • slug: Short name used as the CLI subcommand.
  • args: List of CapArg definitions (input arguments).
  • metadata: Optional key-value pairs (e.g., activity_timeout_secs for custom timeouts).

Source: capdag-rs/src/cap/definition.rs.

CapArg

Each argument a cap accepts is described by a CapArg:

let arg = CapArg::new(
    "media:document;ext=pdf",          // media URN identifying this argument
    true,                 // required
    vec![ArgSource::Stdin { stdin: "media:document;ext=pdf".to_string() }],
);

Fields:

  • media_urn: The media URN that identifies this argument in the stream. The handler uses this to find the argument via find_stream() or require_stream().
  • required: Whether the argument must be provided.
  • sources: How the argument can be provided (for CLI mode).
  • default_value (optional): Default value as serde_json::Value.
  • description (optional): Human-readable description.
  • is_sequence: Cardinality, whether the argument carries a sequence of items (emit_list_item framing) or a single value.
  • streaming (.streaming(true)): Boundedness: the argument is consumed without a length promise (a feed); the cap processes items as they arrive and may be wired live to an unbounded producer. Orthogonal to is_sequence, not a URN axis, and allowed on the main input only (RULE14). The output has the same flag (CapOutput::streaming): the cap may open an unbounded output stream (start_unbounded). Declare both honestly; the executor audits every emitted STREAM_START against them and decides from them whether a hop pipelines or materialises (15.2 §Streaming contracts). A transcriber that reads a microphone and emits segments as it goes declares streaming = true on its input and output; a summariser that needs the whole text declares neither.

Argument sources

ArgSource describes how an argument can be provided in CLI mode:

Variant Description Example
Stdin { stdin } Primary input stream. The stdin media URN identifies the data type. A PDF file piped via stdin.
Position { position } Positional CLI argument (0-based index). mycart extract doc.pdf (position 0 = file path).
CliFlag { cli_flag } Named CLI flag. mycart generate --model gpt2.

Source: capdag-rs/src/cap/definition.rs.

Cap URN construction

Builder pattern (Rust)

CapUrnBuilder constructs cap URNs programmatically:

use capdag::CapUrnBuilder;

let urn = CapUrnBuilder::new()
    .in_urn("media:document;ext=pdf")
    .out_urn("media:record")
    .tag("op", "extract")
    .tag("target", "metadata")
    .build()?;

Standard helpers

For common operations, standard helpers in capdag::standard::caps generate the appropriate cap URN:

use capdag::standard::caps::*;

let thumb_urn = generate_thumbnail_urn("media:document;ext=pdf");   // thumbnail generation
let meta_urn  = extract_metadata_urn("media:document;ext=pdf");     // metadata extraction
let outline_urn = extract_outline_urn("media:document;ext=pdf");    // outline extraction
let disbind_urn = disbind_urn("media:document;ext=pdf");            // page extraction

Each helper takes a media URN and returns the canonical cap URN string for that operation on that media type.

Hardcoded constants (Swift)

Swift cartridges typically use hardcoded string constants for cap URNs rather than builder patterns.

Handler registration

Each cap in the manifest must have a corresponding handler registered with CartridgeRuntime. The cap URN string used for registration must match what dispatch will resolve: it must be is_dispatchable against incoming requests for the caps declared in the manifest.

// Factory function — creates new handler per request
runtime.register_op("cap:in=...;out=...;generate", || Box::new(GenerateOp));

// Default type — creates via Default::default()
runtime.register_op_type::<MetadataOp>("cap:in=...;out=...;extract;target=metadata");

Registering a handler for a cap URN that is not in the manifest is harmless but useless, since no request will arrive for it. Failing to register a handler for a cap in the manifest means requests for that cap will get a NoHandler error.

Cartridge lifecycle

stateDiagram-v2
    [*] --> Registered : Discovery
    Registered --> Spawning : First REQ for this cap
    Spawning --> Handshaking : Process started
    Handshaking --> Live : HELLO + identity OK
    Handshaking --> Failed : HELLO failed
    Live --> Live : Handle requests
    Live --> Dead : Process died
    Live --> Killed : Engine shutdown / idle timeout
    Dead --> Spawning : Next REQ triggers respawn
    Killed --> Registered : Available for respawn
    Failed --> [*] : Permanent failure
  1. Discovery: The engine’s cartridge registry or dev_cartridges mapping identifies which cartridge binary provides a given cap. The cartridge registry is versioned: binaries bake a CARTRIDGE_REGISTRY_VERSION (currently 2, manifest URL prefix .../v2/manifest), and cartridges resolve on disk at {registry_slug}/v{cartridge_registry_version}/{channel}/{name}/{version}/cartridge.json, with the version directory pinned to the host’s baked version like the channel. The registry_slug is a path-safe transform of the registry URL’s authority (host plus optional :port): lowercased, non-[a-z0-9.-] characters replaced by -, null URL → dev, not a hash, so all versions of one registry share a slug. A registry is ingested only if its manifest’s fabricRegistryUrl matches the client’s baked fabric registry, keeping caps comparable across every registry a client uses (see Execution).
  2. First request: When a REQ arrives for a cap this cartridge provides, the CartridgeHostRuntime spawns the binary with no arguments (triggering cartridge CBOR mode).
  3. Handshake: HELLO exchange and identity verification (see Handshake).
  4. Request handling: The engine sends REQ frames; the cartridge dispatches them to registered handlers.
  5. Idle or shutdown: The cartridge may be killed after an idle timeout or when the engine shuts down.
  6. Respawn: The next request for this cartridge’s caps triggers a fresh spawn. Model caches are on disk, so reloading is the only cost.

Source: host_runtime.rs, executor.rs.