← Back to Co-do

WebAssembly Tool Packaging Guide

Learn how to create, compile, and package custom WebAssembly tools for use in Co-do.

Overview

Co-do supports custom WebAssembly (WASM) tools that extend the AI assistant's capabilities. Tools are packaged as ZIP files containing a compiled .wasm binary and a manifest.json that describes the tool's interface. Once installed, the AI can invoke your tool to perform operations like encoding, hashing, text processing, and more.

Tools are compiled using the WASI SDK (WebAssembly System Interface), which means you can write standard C/C++ code that uses stdio, string, and other standard library functions. Rust and Go are also supported via their respective WASM targets.

Package Format (ZIP)

A tool package is a standard ZIP file with the following structure:

your-tool.zip
├── manifest.json          # Required: tool metadata and interface definition
└── your-tool.wasm         # Required: compiled WebAssembly binary

Key rules: The ZIP must contain exactly one manifest.json and exactly one .wasm file. Additional files are allowed but ignored. Files can be at any depth within the ZIP.

The Manifest File

The manifest.json tells Co-do what your tool does, what parameters it accepts, and how to run it. Here is the full schema:

{
  "name": "my-tool",
  "version": "1.0.0",
  "description": "A clear description of what the tool does (shown to the AI).",
  "parameters": {
    "type": "object",
    "properties": {
      "input": {
        "type": "string",
        "description": "The data to process"
      },
      "mode": {
        "type": "string",
        "enum": ["encode", "decode"],
        "description": "Operation mode"
      }
    },
    "required": ["input"]
  },
  "returns": {
    "type": "string",
    "description": "The processed output"
  },
  "execution": {
    "argStyle": "positional",
    "fileAccess": "none",
    "timeout": 5000
  },
  "pipeable": true,
  "category": "crypto",
  "author": "Your Name",
  "license": "MIT",
  "homepage": "https://example.com"
}

Manifest Field Reference

Field Required Description
name Yes Tool identifier. Must start with a letter, lowercase, may contain hyphens/underscores. Pattern: ^[a-z][a-z0-9]*(?:[_-][a-z0-9]+)*$
version Yes Semantic version string (e.g. "1.0.0")
description Yes What the tool does. This is shown to the AI so it knows when to use it.
parameters Yes JSON Schema object defining the tool's input parameters. Each property can have type (string, number, boolean, array), description, optional enum, and optional default.
returns Yes Object with type ("string" or "object") and description of the output.
execution.argStyle Yes How arguments are passed: "positional" (ordered args), "cli" (--flag value), or "json" (JSON via stdin).
execution.fileAccess Yes File system access level: "none", "read", "write", or "readwrite".
execution.timeout No Maximum execution time in milliseconds (default: 30000).
execution.memoryLimit No Maximum WASM memory in pages (each page = 64 KB).
pipeable No If true, the tool is registered as a pipeable command and can participate in pipe chains. The primary text input parameter receives piped stdin. Default: false.
category Yes Tool category (e.g. "crypto", "text", "data", "file", "code").
author No Author name.
license No License identifier (e.g. "MIT").
homepage No URL for the tool's homepage or repository.

Argument Styles Explained

Compiling to WebAssembly

C/C++ with WASI SDK

The recommended approach for C/C++ tools. Install the WASI SDK v24:

# Download WASI SDK v24 (or check releases for newer versions)
# https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-24
export WASI_SDK_PATH=/path/to/wasi-sdk

# Compile a C program to WASM
$WASI_SDK_PATH/bin/clang \
  --target=wasm32-wasi \
  -O2 \
  -o my-tool.wasm \
  main.c

Important: Do not use the --no-entry flag for WASI programs with a main() function. WASI programs export _start as the entry point, which calls main().

Example Makefile

WASI_SDK_PATH ?= /opt/wasi-sdk
CC = $(WASI_SDK_PATH)/bin/clang
CFLAGS = --target=wasm32-wasi -O2
LDFLAGS = -Wl,--export-all

OUTPUT_DIR = ../../binaries

all: $(OUTPUT_DIR)/my-tool.wasm

$(OUTPUT_DIR)/my-tool.wasm: main.c | $(OUTPUT_DIR)
	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<

$(OUTPUT_DIR):
	mkdir -p $(OUTPUT_DIR)

clean:
	rm -f $(OUTPUT_DIR)/my-tool.wasm

Rust

Rust has first-class WASM support via the wasm32-wasi target:

# Add the WASI target
rustup target add wasm32-wasi

# Build your project
cargo build --release --target wasm32-wasi

# The binary will be at:
# target/wasm32-wasi/release/your_tool.wasm

Go (via TinyGo)

Co-do runs WebAssembly modules in a WASI runtime. To build Go tools that are compatible with WASI (and therefore with Co-do), use TinyGo with the WASI target:

# Build using TinyGo with the WASI target (required for Co-do)
tinygo build -o my-tool.wasm -target=wasi main.go

Important: The standard Go WebAssembly target (GOOS=js GOARCH=wasm) produces binaries that require the wasm_exec.js JavaScript shim and are designed for browser/JavaScript environments, not WASI. These binaries will not run in Co-do's WASI runtime. Always use TinyGo with -target=wasi when building Go tools for Co-do.

Creating the ZIP Package

Once you have your compiled .wasm binary and your manifest.json, create the ZIP:

# Create a ZIP containing both files
zip my-tool.zip manifest.json my-tool.wasm

Or if you're using the Co-do build system:

# From the wasm-tools directory, the build script handles this automatically
./build.sh my-tool

# Packages are output to wasm-tools/dist/
# Each tool gets: my-tool.zip, my-tool.manifest.json

Tip: The Co-do repository includes a full build system at wasm-tools/build.sh that automatically downloads the WASI SDK, compiles tools, generates manifests, and creates ZIP packages.

Installing in Co-do

  1. Open Co-do and click the Settings gear icon in the header.
  2. Scroll to the WebAssembly Tools section.
  3. Click the Install Tool button.
  4. Select your .zip package file.
  5. The tool is validated, stored in your browser's IndexedDB, and immediately available to the AI.

Installed tools appear in the WebAssembly Tools list where you can enable/disable or delete them. Each tool's permission defaults to "Ask Each Time", meaning the AI will request your approval before running it.

How Tools Run

When the AI invokes your tool, Co-do executes it in one of two modes:

I/O Model

WASI Compatibility Notes

Pipeable Commands

WASM tools can participate in pipe chains, where the output of one command flows as input to the next — just like Unix pipes. To make your tool pipeable, add "pipeable": true to your manifest.json.

Not all tools are pipeable. Only tools that accept text input and produce text output are good candidates. Tools that require multiple non-text inputs, produce binary output, or have no natural stdin concept should omit this field (or set it to false).

How It Works

When a tool is marked as pipeable, Co-do automatically registers it in the pipe command registry. In a pipe chain, the output of the previous command is injected as the tool's primary text input parameter (typically named input). All other parameters can still be passed explicitly via args.

Manifest Example

{
  "name": "base64",
  "version": "1.0.0",
  "description": "Encode or decode data using Base64 encoding.",
  "parameters": {
    "type": "object",
    "properties": {
      "mode": {
        "type": "string",
        "enum": ["encode", "decode"],
        "description": "Whether to encode or decode"
      },
      "input": {
        "type": "string",
        "description": "The text to encode, or base64 string to decode"
      }
    },
    "required": ["mode", "input"]
  },
  "returns": { "type": "string", "description": "The encoded or decoded result" },
  "execution": { "argStyle": "positional", "fileAccess": "none", "timeout": 5000 },
  "pipeable": true,
  "category": "crypto"
}

Pipe Usage Example

With pipeable: true, the AI can chain your tool with other commands:

// Read a file, then base64-encode the content
{
  "commands": [
    { "tool": "cat", "args": { "path": "secret.txt" } },
    { "tool": "base64", "args": { "mode": "encode" } }
  ]
}

// Decode base64 content, then count lines
{
  "commands": [
    { "tool": "base64", "args": { "mode": "decode", "input": "SGVsbG8KV29ybGQ=" } },
    { "tool": "wc", "args": {} }
  ]
}

Stdin Parameter Resolution

Co-do determines which parameter receives piped input using these rules (in order):

  1. A required string parameter named input or text
  2. The first required string parameter
  3. An optional parameter named input or text

When piped input is available and the resolved parameter is not explicitly provided in args, the piped input is automatically injected.

When to Use pipeable

Good Candidates Not Suitable
Text processors (grep, sed, awk, sort) Multi-input tools (diff requires two texts)
Encoders/decoders (base64, xxd) Generators with no input (uuid)
Format converters (toml2json, markdown) Binary I/O tools (gzip)
Code formatters (shfmt, minify) Database engines (sqlite3)
Hash functions (md5sum, sha256sum) File metadata tools (stat, du)

Limits & Security

Constraint Limit
Maximum ZIP file size 50 MB
Maximum WASM binary size 20 MB
Maximum files in ZIP 100
Default execution timeout 30 seconds (configurable per-tool)
WASM magic number Must start with 00 61 73 6d (\0asm)

Additional security measures:

Example: Base64 Tool

Here's a complete example of a simple base64 encode/decode tool written in C.

Source Code (main.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static const char b64_table[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

static int b64_index(char c) {
    if (c >= 'A' && c <= 'Z') return c - 'A';
    if (c >= 'a' && c <= 'z') return c - 'a' + 26;
    if (c >= '0' && c <= '9') return c - '0' + 52;
    if (c == '+') return 62;
    if (c == '/') return 63;
    return -1;
}

void encode(const char *input) {
    size_t len = strlen(input);
    for (size_t i = 0; i < len; i += 3) {
        unsigned char b0 = input[i];
        unsigned char b1 = (i + 1 < len) ? input[i + 1] : 0;
        unsigned char b2 = (i + 2 < len) ? input[i + 2] : 0;

        putchar(b64_table[b0 >> 2]);
        putchar(b64_table[((b0 & 0x03) << 4) | (b1 >> 4)]);
        putchar((i + 1 < len) ? b64_table[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=');
        putchar((i + 2 < len) ? b64_table[b2 & 0x3f] : '=');
    }
    putchar('\n');
}

void decode(const char *input) {
    size_t len = strlen(input);
    for (size_t i = 0; i < len; i += 4) {
        int a = b64_index(input[i]);
        int b = b64_index(input[i + 1]);
        int c = (i + 2 < len && input[i + 2] != '=') ? b64_index(input[i + 2]) : 0;
        int d = (i + 3 < len && input[i + 3] != '=') ? b64_index(input[i + 3]) : 0;

        if (a < 0 || b < 0) break;

        putchar((a << 2) | (b >> 4));
        if (i + 2 < len && input[i + 2] != '=')
            putchar(((b & 0x0f) << 4) | (c >> 2));
        if (i + 3 < len && input[i + 3] != '=')
            putchar(((c & 0x03) << 6) | d);
    }
    putchar('\n');
}

int main(int argc, char **argv) {
    if (argc < 3) {
        fprintf(stderr, "Usage: base64 <encode|decode> <input>\n");
        return 1;
    }

    if (strcmp(argv[1], "encode") == 0) {
        encode(argv[2]);
    } else if (strcmp(argv[1], "decode") == 0) {
        decode(argv[2]);
    } else {
        fprintf(stderr, "Unknown mode: %s\n", argv[1]);
        return 1;
    }

    return 0;
}

Manifest (manifest.json)

{
  "name": "base64",
  "version": "1.0.0",
  "description": "Encode or decode data using Base64 encoding. Use 'encode' to convert text to Base64, or 'decode' to convert Base64 back to text.",
  "parameters": {
    "type": "object",
    "properties": {
      "mode": {
        "type": "string",
        "enum": ["encode", "decode"],
        "description": "Whether to encode or decode"
      },
      "input": {
        "type": "string",
        "description": "The text to encode, or base64 string to decode"
      }
    },
    "required": ["mode", "input"]
  },
  "returns": {
    "type": "string",
    "description": "The encoded or decoded result"
  },
  "execution": {
    "argStyle": "positional",
    "fileAccess": "none",
    "timeout": 5000
  },
  "category": "crypto",
  "author": "Co-do",
  "license": "MIT"
}

Build & Package

# Compile
$WASI_SDK_PATH/bin/clang --target=wasm32-wasi -O2 -o base64.wasm main.c

# Package
zip base64.zip manifest.json base64.wasm

# Install via Co-do UI: Settings → WebAssembly Tools → Install Tool

Source Code

Co-do is open source. The WebAssembly tool system's source code, including the loader, runtime, and example tools, is available on GitHub:

github.com/PaulKinlan/Co-do

Key Source Files

File Purpose
src/wasm-tools/loader.ts ZIP loading, validation, and manifest parsing
src/wasm-tools/manager.ts Tool lifecycle management and execution
src/wasm-tools/types.ts TypeScript interfaces and Zod schemas
src/wasm-tools/runtime.ts WASM execution runtime environment
src/wasm-tools/vfs.ts Virtual file system for sandboxed I/O
src/wasm-tools/worker-manager.ts Web Worker isolation for tool execution
wasm-tools/build.sh Automated build script for all tools
wasm-tools/src/ Source code for built-in tools (C)
wasm-tools/manifests/ Manifest files for built-in tools