Cartridge Development Guide

Build capabilities for new file types

Overview

Cartridges are standalone executables that add capabilities to MachineFabric. A cartridge can handle one file type (like PDFs) or many. It declares what it can do via a manifest, and MachineFabric calls it when those capabilities are needed.

What a cartridge does

  • Responds to manifest command with its capabilities
  • Implements one or more capability commands (extract-metadata, grind, etc.)
  • Reads input from command-line arguments or stdin
  • Writes JSON output to stdout

Languages

Cartridges can be written in any language that produces a native executable. We provide SDKs for:

  • Rust - machfab-cartridge-sdk crate
  • Go - machfab-cartridge-sdk-go module
  • Swift/Objective-C - MACINACartridgeSDK framework

The SDK is optional—you can write a cartridge in Python, C++, or anything else as long as it follows the protocol.

Architecture

Cartridges run in a sandboxed XPC service, separate from the main app. MachineFabric never loads cartridge code directly.

Execution flow

  1. MachineFabric’s XPC service discovers cartridges at startup
  2. For each executable, it runs cartridge manifest
  3. Parses the JSON manifest to learn what capabilities exist
  4. Registers capabilities with the router
  5. When a capability is needed, spawns the cartridge with arguments
  6. Cartridge writes output to stdout, errors to stderr
  7. XPC service captures output and returns it to the app

Cartridge locations

The XPC service looks for cartridges in two places:

  • MachineFabric.app/Contents/Resources/cartridgesrv-signed/cartridges/ - Bundled
  • /Library/Application Support/MachineFabric/Cartridges/ - User-installed

Security requirements

Cartridges distributed via MachineFabric must be:

  • Code-signed with a Developer ID certificate
  • Notarized by Apple
  • Packaged as a .pkg installer

For development, you can test unsigned cartridges by placing them in the cartridges directory and allowing them in System Settings.

Cartridge Manifest

When called with manifest as the first argument, your cartridge must print a JSON object describing itself.

Manifest structure

{
  "name": "mycartridge",
  "version": "1.0.0",
  "description": "Processes XYZ files",
  "authors": ["Your Name"],
  "caps": [
    {
      "urn": "cap:op=extract;format=xyz;target=metadata",
      "title": "Extract XYZ Metadata",
      "description": "Extract metadata from XYZ files",
      "command": "extract-metadata",
      "arguments": {
        "required": [],
        "optional": []
      },
      "output": {},
      "accepts_stdin": true
    }
  ]
}

Fields

Field Description
name Cartridge identifier (lowercase, no spaces)
version Semantic version (e.g., “1.0.0”)
description What the cartridge does
authors List of author names (optional)
caps Array of capability definitions

Example: Rust implementation

use clap::{Parser, Subcommand};

#[derive(Parser)]
struct Args {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    Manifest,
    ExtractMetadata { file: String },
}

fn main() {
    let args = Args::parse();
    match args.command {
        Commands::Manifest => {
            let manifest = get_cartridge_manifest();
            println!("{}", serde_json::to_string_pretty(&manifest).unwrap());
        }
        Commands::ExtractMetadata { file } => {
            // ... implementation
        }
    }
}

Declaring Capabilities

Each capability in your manifest describes one operation. Capabilities are identified using CapDAG URNs.

Standard capabilities

MachineFabric recognizes these standard operations:

Command Description
extract-metadata Extract file metadata (title, author, page count, etc.)
extract-outline Extract table of contents / document structure
grind Extract text content by page or section
generate-thumbnail Generate preview image

Capability URN format

URNs are built from key-value tags that describe the operation. See the CapDAG URN syntax specification for the full format, canonicalization rules, and wildcard semantics.

Capability definition

{
  "urn": "cap:op=extract;format=xyz;target=metadata",
  "title": "Extract XYZ Metadata",
  "description": "Extract metadata from XYZ files",
  "command": "extract-metadata",
  "metadata": {
    "file_types": "xyz,abc"
  },
  "arguments": {
    "required": [
      {
        "name": "file_path",
        "media_spec": "media:string",
        "arg_description": "Path to input file",
        "cli_flag": "file_path",
        "position": 0
      }
    ],
    "optional": [
      {
        "name": "output",
        "media_spec": "media:string",
        "arg_description": "Output file path",
        "cli_flag": "--output"
      }
    ]
  },
  "output": {
    "media_spec": "media:object",
    "output_description": "Extracted metadata as JSON"
  },
  "accepts_stdin": false
}

Argument types (media_spec)

  • media:string - String
  • media:integer - Integer
  • media:number - Number (float)
  • media:boolean - Boolean
  • media:object - JSON object
  • media:binary - Binary data

Implementing Commands

Each capability maps to a command that your cartridge handles. The command field in the capability definition tells MachineFabric how to invoke your cartridge.

Command invocation

MachineFabric calls your cartridge like this:

# Extract metadata
./mycartridge extract-metadata /path/to/file.xyz

# With optional output file
./mycartridge extract-metadata /path/to/file.xyz --output metadata.json

# Generate thumbnail
./mycartridge generate-thumbnail /path/to/file.xyz --width 256 --height 256

Example: extract-metadata

fn extract_metadata(file_path: &str) -> Result<FileMetadata> {
    // 1. Validate file exists and is readable
    let path = Path::new(file_path);
    if !path.exists() {
        anyhow::bail!("File not found: {}", file_path);
    }

    // 2. Read and parse the file
    let content = fs::read(file_path)?;
    let parsed = parse_xyz_format(&content)?;

    // 3. Build metadata structure
    let mut metadata = FileMetadata::new(
        file_path.to_string(),
        "xyz".to_string(),
        content.len() as u64,
    );
    metadata.title = parsed.title;
    metadata.authors = parsed.authors;
    metadata.page_count = Some(parsed.pages.len());

    Ok(metadata)
}

fn main() {
    match args.command {
        Commands::ExtractMetadata { file, output } => {
            match extract_metadata(&file) {
                Ok(metadata) => {
                    let json = serde_json::to_string_pretty(&metadata).unwrap();
                    if let Some(out_path) = output {
                        fs::write(out_path, &json).unwrap();
                    } else {
                        println!("{}", json);
                    }
                }
                Err(e) => {
                    eprintln!("Error: {}", e);
                    std::process::exit(1);
                }
            }
        }
    }
}

Example: disbind

fn disbind(file_path: &str, index_range: Option<&str>) -> Result<Vec<DisboundPage>> {
    let parsed = parse_xyz_file(file_path)?;
    let pages_to_extract = parse_index_range(index_range, parsed.pages.len())?;

    let mut pages = Vec::new();

    for page_num in pages_to_extract {
        let page_content = &parsed.pages[page_num];
        let page = DisboundPage::new_with_text(
            page_num + 1,  // 1-indexed
            page_content.text.clone(),
        );
        pages.push(page);
    }

    Ok(pages)
}

Input/Output

Input

Cartridges receive input through:

  • Command-line arguments - File paths, flags, options
  • Stdin - If accepts_stdin: true, can receive file content

Output

Cartridges write output to:

  • Stdout - JSON data or binary content
  • Stderr - Progress messages, warnings, errors
  • File - If --output flag is provided

Exit codes

  • 0 - Success
  • 1 - General error
  • 2 - Invalid arguments

Output formats

FileMetadata output

{
  "file_path": "/path/to/file.xyz",
  "file_size_bytes": 1048576,
  "document_type": "xyz",
  "title": "Document Title",
  "authors": ["Author Name"],
  "page_count": 42,
  "creation_date": "2024-01-10T15:30:00Z",
  "keywords": ["tag1", "tag2"],
  "extended_metadata": {
    "custom_field": "value"
  }
}

DocumentOutline output

{
  "source_file": "/path/to/file.xyz",
  "document_type": "xyz",
  "total_pages": 42,
  "has_outline": true,
  "entries": [
    {
      "title": "Chapter 1",
      "level": 0,
      "page": 1,
      "children": [
        {
          "title": "Section 1.1",
          "level": 1,
          "page": 5,
          "children": []
        }
      ]
    }
  ],
  "extraction_info": {
    "extractor_name": "mycartridge",
    "extractor_version": "1.0.0"
  }
}

Disbind output (DisboundPage array)

[
  {
    "order_index": 1,
    "text_content": "Content of page 1...",
    "word_count": 250,
    "character_count": 1500
  },
  {
    "order_index": 2,
    "text_content": "Content of page 2...",
    "word_count": 300,
    "character_count": 1800
  }
]

Using the SDK

The SDK provides standard data structures and helpers. Using it ensures your output matches what MachineFabric expects.

Rust SDK

# Cargo.toml
[dependencies]
machfab-cartridge-sdk = { git = "https://github.com/machfab/machfab-cartridge-sdk" }
capdag = { git = "https://github.com/machfab/capdag" }
clap = { version = "4.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
use machfab_cartridge_sdk::{
    FileMetadata, DocumentOutline,
    OutlineEntry, DisboundPage,
};

// Create metadata
let mut metadata = FileMetadata::new(path, "xyz", file_size);
metadata.title = Some("Title".into());
metadata.add_author("Author");

// Create outline
let mut outline = DocumentOutline::new(path, "xyz", page_count);
let entry = OutlineEntry::new("Chapter 1", 0).with_page(1);
outline.add_entry(entry);

// Create pages (simple Vec)
let mut pages: Vec<DisboundPage> = Vec::new();
let page = DisboundPage::new_with_text(1, "Content...");
pages.push(page);

Go SDK

import sdk "github.com/machfab/machfab-cartridge-sdk-go"

// Create metadata
metadata := sdk.NewFileMetadata(path, "xyz", fileSize)
metadata.Title = "Title"
metadata.AddAuthor("Author")

// Create outline
outline := sdk.NewDocumentOutline(path, "xyz", pageCount)
entry := sdk.NewOutlineEntry("Chapter 1", 0).WithPage(1)
outline.AddEntry(entry)

// Create pages (simple slice)
var pages []sdk.DisboundPage
page := sdk.NewDisboundPageWithText(1, "Content...")
pages = append(pages, *page)

SDK data structures

See SDK Reference for complete documentation.

Testing

Manual testing

# Test manifest
./mycartridge manifest | jq .

# Test extract-metadata
./mycartridge extract-metadata test-file.xyz | jq .

# Test with output file
./mycartridge extract-metadata test-file.xyz -o output.json
cat output.json | jq .

Validate manifest

The SDK includes a validator tool:

cartridge-validator ./mycartridge

Test with MachineFabric

  1. Build your cartridge
  2. Copy to /Library/Application Support/MachineFabric/Cartridges/
  3. Restart MachineFabric
  4. Add a file of your supported type
  5. Check that grinding works

Packaging

Cartridges are distributed as .pkg installers. This handles macOS security requirements (Gatekeeper, quarantine).

Build requirements

  • Universal binary (arm64 + x86_64) or Apple Silicon only
  • Signed with Developer ID certificate
  • Notarized by Apple

Creating a .pkg

# Build universal binary (Rust example)
cargo build --release --target aarch64-apple-darwin
cargo build --release --target x86_64-apple-darwin
lipo -create -output mycartridge \
  target/aarch64-apple-darwin/release/mycartridge \
  target/x86_64-apple-darwin/release/mycartridge

# Sign the binary
codesign --sign "Developer ID Application: Your Name (TEAMID)" \
  --options runtime \
  --timestamp \
  mycartridge

# Create installer package
pkgbuild --root ./pkg-root \
  --identifier com.yourcompany.mycartridge \
  --version 1.0.0 \
  --install-location "/Library/Application Support/MachineFabric/Cartridges" \
  mycartridge.pkg

# Sign the package
productsign --sign "Developer ID Installer: Your Name (TEAMID)" \
  mycartridge.pkg \
  mycartridge-signed.pkg

# Notarize
xcrun notarytool submit mycartridge-signed.pkg \
  --apple-id your@email.com \
  --team-id TEAMID \
  --password "@keychain:AC_PASSWORD" \
  --wait

# Staple
xcrun stapler staple mycartridge-signed.pkg

Publishing

To make your cartridge available through MachineFabric’s cartridge browser:

Requirements

  • Cartridge source code in a public GitHub repository
  • Signed and notarized .pkg installer
  • README with description and usage

Submit for inclusion

Email cartridges@machinefabric.com with:

  • GitHub repository URL
  • Link to downloadable .pkg file
  • Brief description of what file types it handles

We review submissions and add approved cartridges to the registry. Future versions will automate this process.

Cartridge registry format

The registry is a JSON file listing available cartridges:

{
  "cartridges": [
    {
      "name": "mycartridge",
      "version": "1.0.0",
      "description": "Processes XYZ files",
      "download_url": "https://github.com/.../releases/download/v1.0.0/mycartridge.pkg",
      "sha256": "abc123...",
      "file_types": ["xyz", "abc"],
      "repository": "https://github.com/yourname/mycartridge"
    }
  ]
}