Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

## Project Overview

ocaptain is a lightweight multi-coding agent control plane using sprites.dev VMs, Tailscale mesh networking, and Mutagen file sync.
ocaptain is a lightweight multi-coding agent control plane using Tailscale mesh networking and Mutagen file sync. Ships (VMs running Claude Code) can be provisioned on multiple backends:
- **sprites.dev** — Cloud VMs (default)
- **exe.dev** — Alternative cloud provider
- **boxlite** — Local micro-VMs for development

## Running Commands

Expand All @@ -26,15 +29,22 @@ Always use `uv run` to execute Python scripts and CLI commands (e.g., `uv run oc
- `ocaptain telemetry-start` - Start OTLP collector
- `ocaptain telemetry-stop` - Stop OTLP collector

### sprites.dev Commands
### Provider Commands

Ships run on sprites.dev. Use `sprite` CLI for debugging:
Ships can run on sprites.dev, exe.dev, or locally via boxlite.

**sprites.dev** (use `sprite` CLI for debugging):
```bash
sprite list -o <org> # List sprites
sprite exec -o <org> -s <name> # Run command on sprite
```

**boxlite** (local micro-VMs):
```bash
# Requires: pip install ocaptain[boxlite]
export OCAPTAIN_PROVIDER="boxlite"
```

## Architecture

- **Local Storage**: Voyages stored at `~/voyages/<voyage-id>/`
Expand Down
59 changes: 47 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

[![PyPI](https://img.shields.io/pypi/v/ocaptain.svg)](https://pypi.org/project/ocaptain/)
[![Python](https://img.shields.io/badge/Python-3.12+-blue.svg)](https://www.python.org/)
[![sprites.dev](https://img.shields.io/badge/Powered%20by-sprites.dev-purple.svg)](https://sprites.dev)

> O Captain! my Captain! our fearful Claude Code session is done, The repo has weather'd every rack, the prize we sought is won.

Minimalist multi-coding agent control plane built on [sprites.dev](https://sprites.dev) VMs with [Tailscale](https://tailscale.com) mesh networking. Orchestration is managed by Claude Code's built-in [task list](https://x.com/trq212/status/2014480496013803643?s=20) feature: work is distributed by the `ocaptain` CLI to cloud VMs ("ships") that work in parallel on a plan you generate with Claude.
Minimalist multi-coding agent control plane with [Tailscale](https://tailscale.com) mesh networking. Orchestration is managed by Claude Code's built-in [task list](https://x.com/trq212/status/2014480496013803643?s=20) feature: work is distributed by the `ocaptain` CLI to VMs ("ships") that work in parallel on a plan you generate with Claude.

No Kubernetes, no local sandboxes, no containers, no asking for permissions.
Ships can be provisioned on multiple backends:
- **[sprites.dev](https://sprites.dev)** — Cloud VMs with instant provisioning
- **[exe.dev](https://exe.dev)** — Alternative cloud VM provider
- **[BoxLite](https://github.com/anthropics/boxlite)** — Local micro-VMs for development (sub-second boot)

No Kubernetes, no containers, no asking for permissions.

## Table of Contents

Expand All @@ -23,10 +27,10 @@ No Kubernetes, no local sandboxes, no containers, no asking for permissions.

## What it does

Provisions a fleet of VMs on sprites.dev, each running an autonomous Claude Code agent. Ships sync files via Mutagen and coordinate through a shared task list—no central scheduler, just agents racing to complete work.
Provisions a fleet of VMs (cloud or local), each running an autonomous Claude Code agent. Ships sync files via Mutagen and coordinate through a shared task list—no central scheduler, just agents racing to complete work.

```
You (local) → ocaptain sail → sprites.dev VMs → Ships claim tasks → Code syncs back
You (local) → ocaptain sail → Ship VMs → Ships claim tasks → Code syncs back
```

## Why?
Expand All @@ -42,7 +46,10 @@ You (local) → ocaptain sail → sprites.dev VMs → Ships claim tasks → Code

### Prerequisites

1. [sprites.dev](https://sprites.dev) account with `sprite` CLI installed or [exe.dev](https://exe.dev) account.
1. **VM Provider** (choose one):
- [sprites.dev](https://sprites.dev) account with `sprite` CLI installed
- [exe.dev](https://exe.dev) account
- [BoxLite](https://github.com/anthropics/boxlite) for local micro-VMs (`pip install ocaptain[boxlite]`)
2. [Tailscale](https://tailscale.com) installed and running
3. [Mutagen](https://mutagen.io) installed (`brew install mutagen-io/mutagen/mutagen`)
4. Claude Code long-lived OAuth token (subscription required, from `claude setup-token`)
Expand All @@ -67,8 +74,9 @@ export CLAUDE_CODE_OAUTH_TOKEN="your-token-here"
# Tailscale OAuth secret (for ephemeral ship auth keys)
export OCAPTAIN_TAILSCALE_OAUTH_SECRET="tskey-client-xxxx"

# sprites.dev org
export OCAPTAIN_SPRITES_ORG="your-org"
# Provider-specific (choose one):
export OCAPTAIN_SPRITES_ORG="your-org" # For sprites.dev
export OCAPTAIN_PROVIDER="boxlite" # For local BoxLite VMs

# Optional: GitHub token for private repos
export GH_TOKEN="ghp_xxxx"
Expand Down Expand Up @@ -139,7 +147,7 @@ flowchart TB
TS[100.x.x.x]
end

subgraph Sprites["sprites.dev or exe.dev"]
subgraph Provider["VM Provider"]
subgraph Fleet["Ship VMs"]
S0[Ship 0<br/>Claude Code]
S1[Ship 1<br/>Claude Code]
Expand All @@ -153,12 +161,17 @@ flowchart TB
S0 & S1 & S2 -->|tmux sessions| CLI
```

**Supported Providers:**
- `sprites` — sprites.dev cloud VMs (default)
- `exedev` — exe.dev cloud VMs
- `boxlite` — Local micro-VMs for development

### Components

| Component | Description |
|-----------|-------------|
| **Local Voyages** | `~/voyages/<voyage-id>/` contains workspace, tasks, logs, and artifacts |
| **Ship VMs** | sprites.dev VMs running Claude Code autonomously in tmux sessions |
| **Ship VMs** | VMs (cloud or local) running Claude Code autonomously in tmux sessions |
| **Tailscale Mesh** | Ships join tailnet with ephemeral keys for direct connectivity |
| **Mutagen Sync** | Two-way file sync between laptop and ships (workspace + tasks) |
| **Task List** | Shared JSON files in `~/.claude/tasks/`. Ships race to claim pending tasks |
Expand Down Expand Up @@ -279,11 +292,14 @@ ocaptain telemetry-stop
|----------|----------|-------------|
| `CLAUDE_CODE_OAUTH_TOKEN` | Yes | Claude Code authentication token |
| `OCAPTAIN_TAILSCALE_OAUTH_SECRET` | Yes | Tailscale OAuth secret for ephemeral keys |
| `OCAPTAIN_SPRITES_ORG` | Yes | sprites.dev organization name |
| `OCAPTAIN_PROVIDER` | No | VM provider: `sprites`, `exedev`, or `boxlite` (default: `sprites`) |
| `OCAPTAIN_SPRITES_ORG` | Yes* | sprites.dev organization name (*required for sprites provider) |
| `GH_TOKEN` | No | GitHub token for private repos |
| `OCAPTAIN_DEFAULT_SHIPS` | No | Default ship count (default: `3`) |

### sprites.dev Setup
### Provider Setup

#### sprites.dev (default)

Install the `sprite` CLI and authenticate:

Expand All @@ -295,6 +311,25 @@ sprite list -o your-org
sprite create -o your-org test-sprite
```

#### BoxLite (local development)

BoxLite runs hardware-isolated micro-VMs locally with sub-second boot times. Ideal for testing and development without cloud costs.

```bash
# Install with BoxLite support
pip install ocaptain[boxlite]

# Or with uv
uv pip install ocaptain[boxlite]

# Set provider
export OCAPTAIN_PROVIDER="boxlite"
```

**Requirements:**
- macOS 12+ or Linux with KVM
- Tailscale running locally

### Tailscale Setup

1. Create an OAuth client in the Tailscale admin console with `devices:write` scope
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [
]

[project.optional-dependencies]
boxlite = ["boxlite>=0.3.0"]
dev = [
"pytest>=8.0",
"pytest-mock>=3.0",
Expand Down Expand Up @@ -54,6 +55,10 @@ strict = true
module = "fabric.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "boxlite.*"
ignore_missing_imports = true

[tool.bandit]
exclude_dirs = ["tests", ".venv"]
skips = ["B101"] # Skip assert warnings (used in tests)
10 changes: 10 additions & 0 deletions src/ocaptain/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,16 @@ def doctor() -> None:
else:
console.print(f" [yellow]![/yellow] {var} — optional, not set")

# Check optional providers
console.print("\n[bold]Optional providers:[/bold]\n")

try:
import boxlite # noqa: F401

console.print(" [green]✓[/green] boxlite (local micro-VMs)")
except ImportError:
console.print(" [dim]○[/dim] boxlite not installed (pip install ocaptain\\[boxlite])")

# Summary
console.print()
if all_ok:
Expand Down
5 changes: 5 additions & 0 deletions src/ocaptain/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ def load_config() -> OcaptainConfig:
return OcaptainConfig(**data)


def get_ssh_private_key_path() -> Path:
"""Return the path to the ocaptain SSH private key."""
return Path.home() / ".config" / "ocaptain" / "id_ed25519"


def get_ssh_keypair() -> tuple[str, str]:
"""Get or create the ocaptain SSH keypair for VM-to-VM communication.

Expand Down
16 changes: 14 additions & 2 deletions src/ocaptain/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,17 @@ def get_connection(vm: VM, provider: Provider) -> "Generator[Any, None, None]":
else:
from fabric import Connection

with Connection(vm.ssh_dest) as c:
yield c
from .config import CONFIG, get_ssh_keypair, get_ssh_private_key_path

# BoxLite uses port 2222 and needs explicit key
if CONFIG.provider == "boxlite":
get_ssh_keypair() # Ensure key exists
connect_kwargs = {
"key_filename": str(get_ssh_private_key_path()),
"look_for_keys": False,
}
with Connection(vm.ssh_dest, port=2222, connect_kwargs=connect_kwargs) as c:
yield c
else:
with Connection(vm.ssh_dest) as c:
yield c
6 changes: 6 additions & 0 deletions src/ocaptain/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"""VM provider implementations."""

import contextlib

from . import (
exedev, # noqa: F401 - triggers registration
sprites, # noqa: F401 - triggers registration
)

# Optional provider - only register if boxlite is installed
with contextlib.suppress(ImportError):
from . import boxlite # noqa: F401 - triggers registration
Loading
Loading