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
manifestcommand 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-sdkcrate - Go -
machfab-cartridge-sdk-gomodule - Swift/Objective-C -
MACINACartridgeSDKframework
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
- MachineFabric’s XPC service discovers cartridges at startup
- For each executable, it runs
cartridge manifest - Parses the JSON manifest to learn what capabilities exist
- Registers capabilities with the router
- When a capability is needed, spawns the cartridge with arguments
- Cartridge writes output to stdout, errors to stderr
- 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
.pkginstaller
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- Stringmedia:integer- Integermedia:number- Number (float)media:boolean- Booleanmedia:object- JSON objectmedia: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
--outputflag is provided
Exit codes
0- Success1- General error2- 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
- Build your cartridge
- Copy to
/Library/Application Support/MachineFabric/Cartridges/ - Restart MachineFabric
- Add a file of your supported type
- 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
.pkginstaller - README with description and usage
Submit for inclusion
Email cartridges@machinefabric.com with:
- GitHub repository URL
- Link to downloadable
.pkgfile - 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"
}
]
}