From 0a471ef2afa2282ff26da31c24768a1e154561af Mon Sep 17 00:00:00 2001 From: Bhautik Date: Wed, 5 Aug 2026 16:04:52 +0530 Subject: [PATCH 1/6] feat(codex): add Codex plugin with skill + cos driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same skill, references, and cos driver as claude-code-plugin, adapted for Codex's manifest.json format and shell-based session hook. No MCP, no custom tools — uses skills + bash tool like Claude Code. --- README.md | 118 ++- packages/codex-plugin/README.md | 87 ++ packages/codex-plugin/manifest.json | 29 + packages/codex-plugin/scripts/cos | 985 ++++++++++++++++++ .../codex-plugin/scripts/session-start.sh | 31 + .../skills/using-createos-sandbox/SKILL.md | 187 ++++ .../references/lifecycle-and-images.md | 153 +++ .../references/networking.md | 82 ++ .../references/offload-and-egress.md | 86 ++ 9 files changed, 1704 insertions(+), 54 deletions(-) create mode 100644 packages/codex-plugin/README.md create mode 100644 packages/codex-plugin/manifest.json create mode 100755 packages/codex-plugin/scripts/cos create mode 100755 packages/codex-plugin/scripts/session-start.sh create mode 100644 packages/codex-plugin/skills/using-createos-sandbox/SKILL.md create mode 100644 packages/codex-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md create mode 100644 packages/codex-plugin/skills/using-createos-sandbox/references/networking.md create mode 100644 packages/codex-plugin/skills/using-createos-sandbox/references/offload-and-egress.md diff --git a/README.md b/README.md index 46c4ef3..4ea17c0 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,13 @@ # CreateOS Integrations -**[Claude Code](https://docs.claude.com/en/docs/claude-code) plugin, [Pi](https://github.com/anthropics/pi) extension & [OpenCode](https://opencode.ai) plugin for disposable sandbox compute.** +**[Claude Code](https://docs.claude.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [Pi](https://github.com/anthropics/pi) & [OpenCode](https://opencode.ai) plugins for disposable sandbox compute.** -Run code **off your machine** in disposable [CreateOS](https://createos.sh) Sandboxes — from Claude Code, Pi, or OpenCode. +Run code **off your machine** in disposable [CreateOS](https://createos.sh) Sandboxes — from Claude Code, Codex, Pi, or OpenCode. [![Claude Code](https://img.shields.io/badge/Claude%20Code-plugin-6E56CF)](https://docs.claude.com/en/docs/claude-code) [![Pi](https://img.shields.io/badge/Pi-extension-F97316)](https://github.com/anthropics/pi) +[![Codex](https://img.shields.io/badge/Codex-plugin-10A37F)](https://github.com/openai/codex) [![OpenCode](https://img.shields.io/badge/OpenCode-plugin-0EA5E9)](https://opencode.ai) [![CreateOS](https://img.shields.io/badge/CreateOS-Sandboxes-0EA5E9)](https://createos.sh) [![Spawn](https://img.shields.io/badge/create%20to%20first%20command-~200ms-22C55E)](https://createos.sh) @@ -30,7 +31,6 @@ Heavy builds, flaky test suites, and untrusted code don't belong on your laptop. ## Quick start **Claude Code:** - ```bash # 1. Add the marketplace + install the plugin /plugin marketplace add NodeOps-app/createos-claude-plugins @@ -41,7 +41,6 @@ Heavy builds, flaky test suites, and untrusted code don't belong on your laptop. ``` **Pi:** - ```bash # 1. Install the extension pi install npm:@createos/pi @@ -50,8 +49,15 @@ pi install npm:@createos/pi pi --createos ``` -**OpenCode:** +**Codex:** +```bash +# Copy plugin into your project +cp -r packages/codex-plugin .codex/plugins/createos-sandbox +# Then launch codex — the skill and cos driver are available +codex +``` +**OpenCode:** ```bash # 1. Install the plugin opencode plugin @createos/opencode --global @@ -64,29 +70,30 @@ The `createos` CLI **auto-installs** on first use. Sign in once with `createos l ## Packages -| Package | What it does | -| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [**claude-code-plugin**](./packages/claude-code-plugin) | Hooks-based Claude Code plugin — offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public HTTPS expose, private-network clusters, BYO-S3 disk mounts, WireGuard VPN, and snapshot/fork — all driving the authed `createos` CLI. | -| [**pi-extension**](./packages/pi-extension) | Pi coding agent extension that transparently routes all built-in commands (bash, read, write, edit, ls, find, grep) to a remote CreateOS Sandbox, plus 40 additional tools for sandbox lifecycle, configuration, port tunnels, file sync, private networks, persistent disks, custom image templates, remote editors, and device VPN — 47 tools total. | -| [**@createos/opencode**](./packages/opencode-plugin) | OpenCode plugin with 33 sandbox tools (`sandbox_exec`, `sandbox_push`, `sandbox_pull`, networks, disks, VPN, sync) and system prompt injection for sandbox-first workflows. | +| Package | What it does | +|---|---| +| [**claude-code-plugin**](./packages/claude-code-plugin) | Hooks-based Claude Code plugin — offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public HTTPS expose, private-network clusters, BYO-S3 disk mounts, WireGuard VPN, and snapshot/fork — all driving the authed `createos` CLI. | +| [**pi-extension**](./packages/pi-extension) | Pi coding agent extension that transparently routes all built-in commands (bash, read, write, edit, ls, find, grep) to a remote CreateOS Sandbox, plus 40 additional tools for sandbox lifecycle, configuration, port tunnels, file sync, private networks, persistent disks, custom image templates, remote editors, and device VPN — 47 tools total. | +| [**codex-plugin**](./packages/codex-plugin) | Codex plugin — same skill + `cos` driver as Claude Code, adapted for Codex's manifest and hook format. | +| [**@createos/opencode**](./packages/opencode-plugin) | OpenCode plugin with 33 sandbox tools (`sandbox_exec`, `sandbox_push`, `sandbox_pull`, networks, disks, VPN, sync) and system prompt injection for sandbox-first workflows. | ## Claude Code — commands at a glance -| Command | What | -| ------------------------------------------------ | ----------------------------------------------------------------------- | -| `/createos-sandbox:offload ` | one-shot: stage → run → pull artifacts → destroy | -| `/createos-sandbox:fanout [cmd2 …]` | run each command in its own throwaway box, in parallel | -| `/createos-sandbox:shell` | instant throwaway interactive Linux (destroyed on exit) | -| `/createos-sandbox:up` · `run` · `sync` · `down` | reusable per-repo box + file sync for live dev loops | -| `/createos-sandbox:tunnel ` | forward a box port to `127.0.0.1` (private) | -| `/createos-sandbox:expose ` | public HTTPS URL for a box port | -| `/createos-sandbox:cluster …` | N boxes on one private network, name-addressable | -| `/createos-sandbox:disk …` | mount your own S3 bucket into the project box | -| `/createos-sandbox:vpn …` | WireGuard L3 into your private networks | -| `/createos-sandbox:fork` | snapshot the project box → independent clone | -| `/createos-sandbox:pause` · `resume` | park the warm box at zero compute cost, then restore it exactly | -| `/createos-sandbox:template …` | build a custom image so boxes boot with the toolchain already installed | -| `/createos-sandbox:status` | show active box + sync + tunnels + cluster | +| Command | What | +|---|---| +| `/createos-sandbox:offload ` | one-shot: stage → run → pull artifacts → destroy | +| `/createos-sandbox:fanout [cmd2 …]` | run each command in its own throwaway box, in parallel | +| `/createos-sandbox:shell` | instant throwaway interactive Linux (destroyed on exit) | +| `/createos-sandbox:up` · `run` · `sync` · `down` | reusable per-repo box + file sync for live dev loops | +| `/createos-sandbox:tunnel ` | forward a box port to `127.0.0.1` (private) | +| `/createos-sandbox:expose ` | public HTTPS URL for a box port | +| `/createos-sandbox:cluster …` | N boxes on one private network, name-addressable | +| `/createos-sandbox:disk …` | mount your own S3 bucket into the project box | +| `/createos-sandbox:vpn …` | WireGuard L3 into your private networks | +| `/createos-sandbox:fork` | snapshot the project box → independent clone | +| `/createos-sandbox:pause` · `resume` | park the warm box at zero compute cost, then restore it exactly | +| `/createos-sandbox:template …` | build a custom image so boxes boot with the toolchain already installed | +| `/createos-sandbox:status` | show active box + sync + tunnels + cluster | Full flags, networking guide, and heavy-build tips live in the [**Claude Code Plugin README**](./packages/claude-code-plugin/README.md). @@ -94,55 +101,53 @@ Full flags, networking guide, and heavy-build tips live in the [**Claude Code Pl All built-in tools (bash, read, write, edit, ls, find, grep) transparently route to the sandbox — plus 40 additional tools for lifecycle, networking, disks, image templates, remote editors, and device VPN (47 tools total). -| Command | What | -| -------------------------- | ------------------------------------- | -| `/sandbox` | Show sandbox status | -| `/network create ` | Create a private network | -| `/network ls` | List your networks | -| `/network show ` | Show network members + IPs | -| `/network attach ` | Join this sandbox to a network | -| `/network detach ` | Leave a network | -| `/network rm ` | Delete a network | -| `/device status` | Show registered devices | +| Command | What | +|---------|------| +| `/sandbox` | Show sandbox status | +| `/network create ` | Create a private network | +| `/network ls` | List your networks | +| `/network show ` | Show network members + IPs | +| `/network attach ` | Join this sandbox to a network | +| `/network detach ` | Leave a network | +| `/network rm ` | Delete a network | +| `/device status` | Show registered devices | | `/device attach ` | Give your machine access to a network | -| `/device detach ` | Remove access | +| `/device detach ` | Remove access | ### Flags -| Flag | Purpose | -| ------------------ | ------------------------------------- | -| `--createos` | Activate the extension | -| `--shape ` | Sandbox size (default: `s-2vcpu-2gb`) | -| `--rootfs ` | Base image or template | -| `--network ` | Network(s) to join at creation | +| Flag | Purpose | +|------|---------| +| `--createos` | Activate the extension | +| `--shape ` | Sandbox size (default: `s-2vcpu-2gb`) | +| `--rootfs ` | Base image or template | +| `--network ` | Network(s) to join at creation | Full tool inventory lives in the [**Pi Extension README**](./packages/pi-extension/README.md). ## OpenCode — tools at a glance (40) -| Category | Tools | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| **Execute & Files** | `sandbox_exec`, `sandbox_pull`, `sandbox_push` | -| **Lifecycle** | `sandbox_create`, `sandbox_list`, `sandbox_info`, `sandbox_pause`, `sandbox_resume`, `sandbox_fork`, `sandbox_destroy` | -| **Config** | `sandbox_ingress`, `sandbox_firewall`, `sandbox_bandwidth`, `sandbox_shapes`, `sandbox_images` | -| **Ports & Sync** | `sandbox_preview_url`, `sandbox_tunnel`, `sandbox_sync` | -| **Networks** | `sandbox_network_create/list/show/attach/detach/delete` | -| **Disks** | `sandbox_disk_create/list/show/delete/attach/detach` | -| **Device VPN** | `sandbox_device_register/status/attach/detach`, `sandbox_vpn_up` | +| Category | Tools | +|---|---| +| **Execute & Files** | `sandbox_exec`, `sandbox_pull`, `sandbox_push` | +| **Lifecycle** | `sandbox_create`, `sandbox_list`, `sandbox_info`, `sandbox_pause`, `sandbox_resume`, `sandbox_fork`, `sandbox_destroy` | +| **Config** | `sandbox_ingress`, `sandbox_firewall`, `sandbox_bandwidth`, `sandbox_shapes`, `sandbox_images` | +| **Ports & Sync** | `sandbox_preview_url`, `sandbox_tunnel`, `sandbox_sync` | +| **Networks** | `sandbox_network_create/list/show/attach/detach/delete` | +| **Disks** | `sandbox_disk_create/list/show/delete/attach/detach` | +| **Device VPN** | `sandbox_device_register/status/attach/detach`, `sandbox_vpn_up` | Full reference in [opencode-plugin/README.md](./packages/opencode-plugin/README.md). ## Install **From GitHub (recommended):** - ``` /plugin marketplace add NodeOps-app/createos-claude-plugins /plugin install @createos/claude-code@createos ``` **From a local checkout:** - ``` git clone https://github.com/NodeOps-app/createos-claude-plugins /plugin marketplace add /path/to/createos-claude-plugins @@ -150,7 +155,6 @@ git clone https://github.com/NodeOps-app/createos-claude-plugins ``` **Dev (instant, no install):** - ```bash claude --plugin-dir /path/to/createos-claude-plugins/packages/claude-code-plugin /reload-plugins # after editing plugin files @@ -187,6 +191,11 @@ createos-claude-plugins/ # marketplace root │ │ ├─ index.ts # extension entry point │ │ ├─ src/ # tools, CLI wrappers, ops │ │ └─ README.md +│ ├─ codex-plugin/ # Codex plugin +│ │ ├─ manifest.json +│ │ ├─ scripts/cos, session-start.sh +│ │ ├─ skills/using-createos-sandbox/ +│ │ └─ README.md │ └─ opencode-plugin/ # OpenCode plugin │ ├─ index.ts # plugin entry (CreateOSPlugin) │ ├─ src/cli.ts # createos CLI wrappers @@ -210,4 +219,5 @@ Issues and PRs welcome. All three plugins are thin surfaces over the [`createos` - [OpenCode plugins](https://opencode.ai/docs/plugins/) — OpenCode plugin docs - [Claude Code plugin README](./packages/claude-code-plugin/README.md) - [Pi extension README](./packages/pi-extension/README.md) +- [Codex plugin README](./packages/codex-plugin/README.md) - [OpenCode plugin README](./packages/opencode-plugin/README.md) diff --git a/packages/codex-plugin/README.md b/packages/codex-plugin/README.md new file mode 100644 index 0000000..e743325 --- /dev/null +++ b/packages/codex-plugin/README.md @@ -0,0 +1,87 @@ +# createos-sandbox (Codex plugin) + +Codex plugin that offloads code to disposable [CreateOS](https://createos.sh) Sandboxes. +Gives Codex a skill + session hook that teach the agent how to use the `cos` driver +for one-shot offloads, reusable dev boxes, parallel fanout, tunnels, clusters, and more. + +## Install + +Copy or symlink the plugin into your project's `.codex/` directory: + +```bash +# From this repo +cp -r packages/codex-plugin /path/to/your-project/.codex/plugins/createos-sandbox + +# Or symlink for development +ln -s /path/to/createos-claude-plugins/packages/codex-plugin \ + /path/to/your-project/.codex/plugins/createos-sandbox +``` + +## Prerequisites + +1. **createos CLI** — auto-installs on first use, or manually: + ```bash + curl -sfL https://raw.githubusercontent.com/NodeOps-app/createos-cli/main/install.sh | sh + ``` + +2. **Login** (one-time): + ```bash + createos login + ``` + +3. **Host tools:** `jq`, `tar`, `bash`, `base64`, `perl`, `curl` + +## How it works + +1. On session start, the hook publishes the `cos` driver's absolute path into context +2. The skill teaches Codex when and how to use sandbox commands +3. Codex runs `cos offload`, `cos up`, `cos run`, etc. via the bash tool +4. All commands execute inside remote CreateOS Sandboxes + +## Commands + +| Command | What | +|---|---| +| `cos offload ` | One-shot: stage dir → run → pull artifacts → destroy | +| `cos fanout [cmd2 …]` | Parallel: each command in its own box | +| `cos shell` | Instant throwaway Linux (user runs interactively) | +| `cos up` / `cos run` / `cos down` | Reusable project box | +| `cos sync ` | File sync (laptop → box) | +| `cos tunnel ` | Forward box port to localhost | +| `cos expose ` | Public HTTPS URL | +| `cos cluster up N` | N boxes on one private network | +| `cos disk create/attach/detach` | S3 disk mounts | +| `cos vpn register/up` | WireGuard VPN | +| `cos pause` / `cos resume` | Park/restore warm box | +| `cos fork` | Snapshot → independent clone | + +## Architecture + +``` +packages/codex-plugin/ +├── manifest.json # Plugin manifest +├── scripts/ +│ ├── cos # CLI driver (same as claude-code-plugin) +│ └── session-start.sh # Session hook — publishes cos path +├── skills/ +│ └── using-createos-sandbox/ +│ ├── SKILL.md # Main skill +│ └── references/ +│ ├── offload-and-egress.md +│ ├── networking.md +│ └── lifecycle-and-images.md +└── README.md +``` + +## Differences from Claude Code plugin + +Minimal. The Codex plugin uses the same skill, references, and `cos` driver. +The only differences are: + +- **Manifest format**: `manifest.json` (Codex) vs `.claude-plugin/plugin.json` (Claude Code) +- **Hook format**: Shell script output (Codex) vs JSON hook response (Claude Code) +- **No slash commands**: Codex uses skills + bash tool, not slash commands + +## License + +Apache-2.0 diff --git a/packages/codex-plugin/manifest.json b/packages/codex-plugin/manifest.json new file mode 100644 index 0000000..1deaacd --- /dev/null +++ b/packages/codex-plugin/manifest.json @@ -0,0 +1,29 @@ +{ + "name": "createos-sandbox", + "version": "0.1.0", + "description": "Run code off your machine in disposable CreateOS Sandboxes — offload, fanout, scratch shell, reusable box with sync, tunnels, clusters, disks, and VPN.", + "keywords": ["createos", "sandbox", "offload", "remote", "compute"], + "paths": { + "skills": ["skills/using-createos-sandbox"], + "hooks": { + "Inline": [ + { + "event_name": "session-start", + "hooks": [ + { + "type": "command", + "command": "./scripts/session-start.sh" + } + ] + } + ] + } + }, + "interface": { + "display_name": "CreateOS Sandbox", + "short_description": "Offload code to disposable remote sandboxes", + "developer_name": "CreateOS", + "category": "compute", + "website_url": "https://createos.sh" + } +} diff --git a/packages/codex-plugin/scripts/cos b/packages/codex-plugin/scripts/cos new file mode 100755 index 0000000..5e0b440 --- /dev/null +++ b/packages/codex-plugin/scripts/cos @@ -0,0 +1,985 @@ +#!/usr/bin/env bash +# cos — drive a CreateOS sandbox as remote compute for Claude / agents. +# +# Patterns: +# offload (one-shot, safe): stage a dir → run (keepalive+retry) → pull artifacts → destroy +# up/run/sync/down (reusable): a per-repo box + one-way/two-way/mirror file sync +# +# `cos` is NOT on PATH by default — run `cos install` once (symlinks into +# ~/.local/bin), or invoke it by full path: "$CLAUDE_PLUGIN_ROOT/scripts/cos". +# +# Subcommands: +# cos install [target] symlink this script onto PATH (default ~/.local/bin/cos) +# cos offload [flags] throwaway box: stage→run→pull→destroy +# cos up [-s][-r][-n][-e|-p|-E][-a] create/reuse a project box (-a to adopt) +# cos run run in the project box (keepalive) +# cos sync [-2|-M][-x] [remote] start file sync into the project box (bg) +# cos pause | cos resume park the warm box at zero compute cost / bring it back +# cos template submit [-f Dockerfile] build a custom rootfs (bake the toolchain once) +# cos down [-f] stop sync + destroy box + clear state (-f reaps forks) +# cos status show active box + sync state +# +# Driver is the authed `createos` CLI. `createos sandbox create` renders a TTY +# spinner even under `-o json`, so every create tags a unique --name (≤22 chars, +# CreateOS cap) and resolves the id from `createos -o json sandbox ls`. +set -euo pipefail + +CLI=${COS_CLI:-createos} +CLI_INSTALL_URL_DEFAULT=https://raw.githubusercontent.com/NodeOps-app/createos-cli/main/install.sh +CLI_INSTALL_URL=${COS_CLI_INSTALL_URL:-$CLI_INSTALL_URL_DEFAULT} +STATE_DIR=${COS_STATE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/createos-sandbox} +mkdir -p "$STATE_DIR" + +proj_key(){ local root; root=$(git rev-parse --show-toplevel 2>/dev/null || pwd) + if command -v shasum >/dev/null 2>&1; then printf '%s' "$root" | shasum | cut -c1-12 + elif command -v sha1sum >/dev/null 2>&1; then printf '%s' "$root" | sha1sum | cut -c1-12 + else printf '%s' "$root" | sha256sum | cut -c1-12; fi; } +STATE="$STATE_DIR/$(proj_key).json" +TUNLIST="${STATE%.json}.tunnels" + +die(){ echo "cos: $*" >&2; exit 1; } +have(){ command -v "$1" >/dev/null 2>&1; } +numeric(){ case "$1" in ''|*[!0-9]*) return 1;; *) return 0;; esac; } + +# big / regenerable dirs never worth copying laptop→box (applies to offload upload AND sync) +DEFAULT_EXCLUDES=(.git target node_modules __pycache__ .venv .mypy_cache .pytest_cache + .gradle .cargo/registry dist build .next .turbo '*.gif' '*.mp4' '*.mov' '*.zst') +strip_ansi(){ perl -pe 's/\e\[[0-9;?]*[ -\/]*[@-~]//g' 2>/dev/null || cat; } + +# ── long-option normalizer (getopts is short-only; translate --foo → -f) ────── +NORMA=() +_mapl(){ case "$1" in + --shape) echo -s;; --rootfs) echo -r;; --out|--output) echo -o;; --name) echo -n;; + --egress) echo -e;; --egress-preset) echo -p;; --egress-all) echo -E;; --adopt) echo -a;; + --exclude) echo -x;; --keep-on-fail) echo -K;; --swap) echo -w;; + --two-way) echo -2;; --mirror) echo -M;; --forks) echo -f;; *) echo "$1";; esac; } +_norm(){ NORMA=(); while [ $# -gt 0 ]; do case "$1" in + --) shift; NORMA+=(-- "$@"); break;; + --*=*) NORMA+=("$(_mapl "${1%%=*}")" "${1#*=}");; + --*) NORMA+=("$(_mapl "$1")");; + *) NORMA+=("$1");; esac; shift; done; } + +# ── egress presets: registries + CDNs a build actually reaches ──────────────── +egress_preset(){ case "$1" in + python-uv) echo "astral.sh releases.astral.sh pypi.org files.pythonhosted.org";; + rust-cargo) echo "crates.io static.crates.io index.crates.io static.rust-lang.org cdn.pyke.io";; + npm) echo "registry.npmjs.org";; + github) echo "github.com objects.githubusercontent.com raw.githubusercontent.com codeload.github.com";; + *) return 1;; esac; } + +resolve_id(){ "$CLI" -o json sandbox ls 2>/dev/null | jq -r --arg n "$1" '.[]|select(.name==$n).id' | head -1; } +box_status(){ "$CLI" -o json sandbox ls 2>/dev/null | jq -r --arg i "$1" '.[]|select(.id==$i)|.status'; } + +# A box idles into `paused` after 30m (auto-pause), and the API still holds its name. +# Treat paused as live: it is reusable (resume it) and it blocks a same-name create. +box_live(){ case "$(box_status "$1")" in running|paused) return 0;; *) return 1;; esac; } +box_resume_if_paused(){ [ "$(box_status "$1")" = paused ] || return 0 + echo "cos: resuming paused box $1…" >&2 + "$CLI" sandbox resume "$1" >/dev/null 2>&1 || die "resume of $1 failed — try: createos sandbox resume $1" + wait_running "$1" 30 || die "box $1 did not reach running after resume"; } + +# poll until the box is running (race guard before first push/exec) +wait_running(){ local id=$1 to=${2:-30} i s + for ((i=0;i/dev/null | jq -r --arg i "$id" '.[]|select(.id==$i)|.status') + [ "$s" = running ] && return 0 + sleep 1 + done + return 1 +} + +create_box(){ # $1=name $2=shape $3=rootfs ; extra --egress in COS_EGRESS array + local name=$1 shape=$2 rootfs=$3 + local args=(sandbox create --name "$name" --shape "$shape" --rootfs "$rootfs" --auto-pause 30m) + [ -n "${COS_NET:-}" ] && args+=(--network "${COS_NET}") + [ "${#COS_EGRESS[@]}" -gt 0 ] && args+=("${COS_EGRESS[@]}") + [ "${#COS_EGRESS[@]}" -eq 0 ] && echo "cos: ⚠ egress UNRESTRICTED — box can reach any host (restrict with -p or -e )" >&2 + if ! NO_COLOR=1 TERM=dumb "$CLI" "${args[@]}" >"$STATE_DIR/last-create.log" 2>&1; then + local err; err=$(strip_ansi <"$STATE_DIR/last-create.log" | tr -d '\r' \ + | grep -iE 'not allowed|no space|invalid|denied|quota|exceed|error|fail' | tail -1) + if printf '%s' "$err" | grep -q 'choices:'; then + local choices; choices=$(printf '%s' "$err" | sed -E 's/.*choices: ?\[([^]]*)\].*/\1/') + die "shape '$shape' not allowed on your plan. Allowed: $choices (set with -s; list via 'createos sandbox shapes')" + fi + [ -n "$err" ] && die "create failed: $err" + die "create failed for $name — see $STATE_DIR/last-create.log" + fi + local id; id=$(resolve_id "$name"); [ -n "$id" ] && [ "$id" != null ] || die "could not resolve $name" + printf '%s' "$id" +} + +# ── state lock ──────────────────────────────────────────────────────────────── +# Concurrent agents in one project share $STATE. Without a lock, `up` (read → +# create → write) races `down` (read → destroy → unlink) and boxes leak or get +# destroyed out from under a live sync. mkdir is the portable atomic primitive — +# `flock(1)` is absent on macOS. Reentrant: `up`/`down` hold it across their +# whole read-modify-write, and the `state_set` calls nested inside just bump depth. +LOCK="${STATE%.json}.lock" +LOCK_DEPTH=0 +lock_state(){ + [ "$LOCK_DEPTH" -gt 0 ] && { LOCK_DEPTH=$((LOCK_DEPTH+1)); return 0; } + local i=0 holder holder2 + until mkdir "$LOCK" 2>/dev/null; do + holder=$(cat "$LOCK/pid" 2>/dev/null || true) + if [ -n "$holder" ] && ! kill -0 "$holder" 2>/dev/null; then + # Re-read after a beat before stealing: the holder may have exited cleanly and + # a third process may already hold a fresh lock. Steal only if the pid file + # still names the same dead process — a live successor writes its own pid, and + # a successor mid-acquire leaves it empty. Either way the pid changes and we back off. + sleep 0.2; holder2=$(cat "$LOCK/pid" 2>/dev/null || true) + if [ "$holder" = "$holder2" ] && ! kill -0 "$holder" 2>/dev/null; then + echo "cos: clearing stale lock from dead pid $holder" >&2; rm -rf "$LOCK"; continue + fi + fi + i=$((i+1)); [ "$i" -ge 300 ] && die "state lock busy 30s (pid ${holder:-?}) — stale? rm -rf $LOCK" + sleep 0.1 + done + echo $$ > "$LOCK/pid" + LOCK_DEPTH=1 + trap unlock_state EXIT INT TERM +} +unlock_state(){ + [ "$LOCK_DEPTH" -le 0 ] && return 0 + LOCK_DEPTH=$((LOCK_DEPTH-1)) + [ "$LOCK_DEPTH" -le 0 ] && rm -rf "$LOCK" 2>/dev/null + return 0 +} + +state_get(){ [ -f "$STATE" ] && jq -r --arg k "$1" '.[$k] // empty' "$STATE" 2>/dev/null || true; } +state_set(){ lock_state; local tmp; tmp=$(mktemp); [ -f "$STATE" ] || echo '{}' > "$STATE" + local jqargs=() prog='.' + while [ $# -ge 2 ]; do jqargs+=(--arg "k$#" "$1" --arg "v$#" "$2"); prog="$prog | .[\$k$#]=\$v$#"; shift 2; done + jq "${jqargs[@]}" "$prog" "$STATE" > "$tmp" && mv "$tmp" "$STATE" + unlock_state +} + +# ── set up a swapfile in-box (OOM headroom for compiled-extension builds) ────── +setup_swap(){ local id=$1 gb=$2 + echo "cos: ensuring ${gb}G swap in box…" >&2 + "$CLI" sandbox exec "$id" -- bash -lc " + swapon --show 2>/dev/null | grep -q /cos.swap && { free -m | awk '/Swap/{print \"swap MB: \"\$2}'; exit 0; } + ( fallocate -l ${gb}G /cos.swap 2>/dev/null || dd if=/dev/zero of=/cos.swap bs=1M count=\$(( ${gb}*1024 )) status=none 2>/dev/null ) \ + && chmod 600 /cos.swap && mkswap /cos.swap >/dev/null 2>&1 && swapon /cos.swap 2>/dev/null \ + && free -m | awk '/Swap/{print \"swap MB: \"\$2}' \ + || echo 'swap setup failed (continuing without swap)' + " 2>&1 | sed 's/^/cos: /' >&2 || true +} + +# ── run a command in-box with keepalive + transient-retry ───────────────────── +# Detaches the real command (survives stream death), then a heartbeat watcher +# tails it. If the watch stream drops mid-build, the build keeps running on the +# box and the watcher re-attaches. Sets BUILD_RC (real exit) or INFRA_FAIL=1. +# shellcheck disable=SC2016 # remote scripts are single-quoted on purpose ($ stays remote) +run_keepalive(){ local id=$1 cmd=$2 wd=${3:-} llog=${4:-$STATE_DIR/last-exec.log}; BUILD_RC=""; INFRA_FAIL=0 + local b64; b64=$(printf '%s' "$cmd" | base64 | tr -d '\n') + # start: decode cmd, run detached, record pid; write exit code to /tmp/.cos-run.rc + local runner='CMD=$(printf %s "$1" | base64 -d); cd "${2:-$HOME}" 2>/dev/null || cd /; rm -f /tmp/.cos-run.rc /tmp/.cos-run.pid; nohup bash -c '\''bash -lc "$0"; echo $? > /tmp/.cos-run.rc'\'' "$CMD" >/tmp/.cos-run.log 2>&1 /tmp/.cos-run.pid; sleep 0.3; echo "[cos] started $(cat /tmp/.cos-run.pid)"' + "$CLI" sandbox exec "$id" -- bash -lc "$runner" _ "$b64" "$wd" >/dev/null 2>&1 \ + || { echo "cos: failed to start remote command" >&2; INFRA_FAIL=1; return 1; } + + local watch='p=$(cat /tmp/.cos-run.pid 2>/dev/null); [ -n "$p" ] || { echo "[cos] no pidfile"; exit 0; }; while kill -0 "$p" 2>/dev/null; do echo "[cos hb $(date -u +%H:%M:%S)]"; tail -n 2 /tmp/.cos-run.log 2>/dev/null; sleep 10; done; rc=$(cat /tmp/.cos-run.rc 2>/dev/null); echo "[cos done rc=${rc:-?}]"; tail -n 40 /tmp/.cos-run.log 2>/dev/null' + local tries=0 max=6 rc alive + while :; do + set +e + "$CLI" sandbox exec --stream "$id" -- bash -lc "$watch" 2>&1 | tee "$llog" + set -e + rc=$("$CLI" sandbox exec "$id" -- bash -c 'cat /tmp/.cos-run.rc 2>/dev/null' 2>/dev/null | tr -dc '0-9' | head -c4 || true) + [ -n "$rc" ] && { BUILD_RC=$rc; return 0; } + tries=$((tries+1)); [ "$tries" -ge "$max" ] && { INFRA_FAIL=1; return 1; } + alive=$("$CLI" sandbox exec "$id" -- bash -lc 'p=$(cat /tmp/.cos-run.pid 2>/dev/null); { [ -n "$p" ] && kill -0 "$p" 2>/dev/null && echo ALIVE; } || echo DEAD' 2>/dev/null || true) + if printf '%s' "$alive" | grep -q ALIVE; then + echo "cos: watch stream dropped (attempt $tries/$max) — build still running, re-attaching…" >&2 + sleep 3; continue + fi + INFRA_FAIL=1; return 1 # build process gone with no exit code → infra failure + done +} + +# ─────────────────────────────────────────────────────────────── one-shot offload +OFFLOAD_ID=""; KEEP=0 +on_offload_exit(){ [ -n "$OFFLOAD_ID" ] || return 0 + [ "$KEEP" = 1 ] && return 0 + "$CLI" sandbox rm -y "$OFFLOAD_ID" >/dev/null 2>&1 && echo "cos: destroyed $OFFLOAD_ID" >&2 || true; } + +cmd_offload(){ + _norm "$@"; set -- ${NORMA[@]+"${NORMA[@]}"} + local shape=s-1vcpu-1gb rootfs=devbox:1 out="" swap="" keep_on_fail=0 egress_all=0 + COS_EGRESS=(); local -a excl=() _d; local OPTIND=1 o doms d + while getopts "s:r:e:o:p:x:w:EKh" o; do case $o in + s) shape=$OPTARG;; r) rootfs=$OPTARG;; o) out=$OPTARG;; w) swap=$OPTARG;; + e) COS_EGRESS+=(--egress "$OPTARG");; + p) doms=$(egress_preset "$OPTARG") || die "unknown egress preset '$OPTARG' (have: python-uv rust-cargo npm github)" + read -ra _d <<<"$doms"; for d in "${_d[@]}"; do COS_EGRESS+=(--egress "$d"); done;; + x) excl+=("$OPTARG");; + E) egress_all=1;; + K) keep_on_fail=1;; + h) offload_usage; exit 0;; + *) offload_usage >&2; exit 2;; esac; done + shift $((OPTIND-1)) + # no args (e.g. empty slash-command injection) → usage + exit 0, never die + if [ $# -lt 2 ]; then offload_usage; exit 0; fi + local dir=$1 cmd=$2 + [ -d "$dir" ] || die "no such dir: $dir" + [ "$egress_all" = 1 ] && COS_EGRESS=() + + # warn: heavy compiled build on a small box + local heavy=0 small=0 + case "$cmd" in *cargo*|*maturin*|*torch*|*"pip install"*|*"uv sync"*|*"uv run"*|*pyo3*) heavy=1;; esac + case "$shape" in *256mb|*512mb|*-1gb) small=1;; esac + [ "$heavy" = 1 ] && [ "$small" = 1 ] && [ -z "$swap" ] && \ + echo "cos: ⚠ heavy build on small box ($shape) — risk of OOM/ENOSPC. Try -s s-2vcpu-2gb or --swap 4." >&2 + + local name="cos-o-$$-${RANDOM}" + OFFLOAD_ID=$(create_box "$name" "$shape" "$rootfs") + KEEP=0; trap on_offload_exit EXIT + echo "cos: $OFFLOAD_ID ($name, $shape/$rootfs)" >&2 + wait_running "$OFFLOAD_ID" 30 || die "box $OFFLOAD_ID not running after 30s" + + # stage with excludes (.git/build artifacts/large media skipped by default) + local -a tarx=(); local p + for p in "${DEFAULT_EXCLUDES[@]}" ${excl[@]+"${excl[@]}"}; do tarx+=(--exclude "$p"); done + echo "cos: staging $dir → box:/work (default excludes + ${excl[*]:-none})" >&2 + tar "${tarx[@]}" -c -C "$dir" . | "$CLI" sandbox push "$OFFLOAD_ID" - /work.tar >/dev/null 2>&1 || die "push failed" + "$CLI" sandbox exec "$OFFLOAD_ID" -- bash -lc 'mkdir -p /work && tar -C /work -xf /work.tar && rm -f /work.tar && echo ok' >/dev/null 2>&1 || die "extract failed" + + [ -n "$swap" ] && setup_swap "$OFFLOAD_ID" "$swap" + + run_keepalive "$OFFLOAD_ID" "$cmd" /work || true + local rc=${BUILD_RC:-1} + if [ "${INFRA_FAIL:-0}" = 1 ]; then + KEEP=1; rc=1 + echo "cos: ⚠ infra/stream failure — box kept so the build cache survives." >&2 + echo "cos: reconnect: createos sandbox exec --stream $OFFLOAD_ID -- bash -lc 'tail -f /tmp/.cos-run.log'" >&2 + echo "cos: destroy: createos sandbox rm -y $OFFLOAD_ID" >&2 + elif [ "$rc" != 0 ] && [ "$keep_on_fail" = 1 ]; then + KEEP=1 + echo "cos: command exited $rc — box kept (--keep-on-fail). destroy: createos sandbox rm -y $OFFLOAD_ID" >&2 + fi + + if [ -n "$out" ]; then + echo "cos: pulling /work/$out → $dir/" >&2 + if "$CLI" sandbox exec "$OFFLOAD_ID" -- bash -lc "cd /work && tar -c $out" 2>/dev/null | tar -x -C "$dir"; then :; else + echo "cos: ⚠ pull of '$out' FAILED — artifacts NOT retrieved (does '$out' exist under /work?). Box is about to be destroyed." >&2 + fi + fi + return "$rc" +} +offload_usage(){ cat <<'EOF' +cos offload — run a command in a throwaway sandbox (stage → run → pull → destroy). + cos offload [flags] +flags: + -s shape -r rootfs -o out (tar dir to pull back) -w GB (swap) -K keep box on failure + -e allow one egress domain (repeatable) + -p egress preset: python-uv | rust-cargo | npm | github (repeatable, composes with -e) + -E unrestricted egress (trusted offload) + -x extra upload exclude (repeatable; .git/target/node_modules/__pycache__/.venv/media excluded by default) +example: + cos offload -p python-uv -p rust-cargo -x target . 'uv sync --frozen --group dev && uv run pytest -q' +EOF +} + +# ───────────────────────────────────────────────────────────── reusable project box +cmd_up(){ + _norm "$@"; set -- ${NORMA[@]+"${NORMA[@]}"} + local shape=s-2vcpu-2gb rootfs=devbox:1 name="" egress_all=0 adopt=0; COS_EGRESS=(); local -a _d; local OPTIND=1 o doms d + while getopts "s:r:n:e:p:Eah" o; do case $o in + s) shape=$OPTARG;; r) rootfs=$OPTARG;; n) name=$OPTARG;; + e) COS_EGRESS+=(--egress "$OPTARG");; + p) doms=$(egress_preset "$OPTARG") || die "unknown egress preset '$OPTARG'" + read -ra _d <<<"$doms"; for d in "${_d[@]}"; do COS_EGRESS+=(--egress "$d"); done;; + E) egress_all=1;; + a) adopt=1;; + h) echo "cos up [-s shape] [-r rootfs] [-n name] [-e dom|-p preset|-E] [-a]"; exit 0;; + *) die "usage: cos up [-s shape] [-r rootfs] [-n name] [-e dom|-p preset|-E] [-a]";; esac; done + [ "$egress_all" = 1 ] && COS_EGRESS=() + lock_state # held across check → create → write; released on return via EXIT trap + local cur curname; cur=$(state_get id); curname=$(state_get name) + if [ -n "$cur" ] && box_live "$cur"; then + # Reuse only what was asked for. An explicit -n naming a different box must not + # silently hand back the current one — state holds a single project box, so the + # caller has to tear the old one down rather than orphan it. + [ -z "$name" ] || [ "$name" = "$curname" ] || die "project box $cur ($curname) is already up — one project box per directory. + Use it: cos run … + Replace it: cos down && cos up -n $name" + box_resume_if_paused "$cur" + echo "cos: reusing $cur ($curname)" >&2; unlock_state; return 0 + fi + [ -n "$name" ] || name="cos-proj-$(proj_key)" + # A live box under our name with no local state was created by someone else + # (other checkout, other agent, by hand). Adopting it silently means `cos down` + # later destroys a box this project never created — require explicit consent, + # and mark it unowned so `down` refuses to destroy it. + local existing; existing=$(resolve_id "$name") + if [ -n "$existing" ] && [ "$existing" != null ] && box_live "$existing"; then + [ "$adopt" = 1 ] || die "box '$name' ($existing, $(box_status "$existing")) already exists, but this project has no state for it. + It was not created by this checkout — 'cos down' would destroy a box you don't own. + Adopt it: cos up -a (adopted boxes are never destroyed by 'cos down') + Destroy it: createos sandbox rm -y $existing" + box_resume_if_paused "$existing" + state_set id "$existing" name "$name" shape "$shape" owned 0 + echo "cos: adopted existing box $existing ($name) — 'cos down' will NOT destroy it" >&2 + unlock_state; return 0 + fi + local id; id=$(create_box "$name" "$shape" "$rootfs") + state_set id "$id" name "$name" shape "$shape" owned 1 + echo "cos: project box up: $id ($name, $shape)" >&2 + unlock_state +} + +cmd_run(){ + # `run` takes the command as a plain string — there is no `--` separator and no + # per-command flags. Catch the help probes explicitly; otherwise they get sent to + # the box as a command and come back as a confusing shell error. + case "${1:-}" in + -h|--help|"") echo "cos run run a command in the project box (keepalive, state persists)" + echo " the command is ONE string, no '--' separator: cos run 'npm ci && npm test'" + echo " one-shot work in a throwaway box instead: cos offload [flags] " + return 0;; + esac + local id; id=$(state_get id); [ -n "$id" ] || die "no active box — run 'cos up' first" + # An idle box auto-pauses. Say so plainly instead of letting wait_running time + # out into a generic "not running" — and leave the resume as the caller's call, + # since resuming restarts compute billing. + [ "$(box_status "$id")" = paused ] && die "box $id is paused (idle auto-pause, or 'cos pause') — 'cos resume' first" + wait_running "$id" 15 || die "box $id not running" + run_keepalive "$id" "$*" "$(state_get sync_remote)" || true + local rc=${BUILD_RC:-1} + [ "${INFRA_FAIL:-0}" = 1 ] && { echo "cos: stream failure; build may still be running. Check: cos run 'cat /tmp/.cos-run.rc'" >&2; rc=1; } + return "$rc" +} + +cmd_sync(){ + _norm "$@"; set -- ${NORMA[@]+"${NORMA[@]}"} + local id; id=$(state_get id); [ -n "$id" ] || die "no active box — run 'cos up' first" + local mode=one-way; local -a excl=(); local OPTIND=1 o + while getopts "2Mx:h" o; do case $o in + 2) mode=two-way;; M) mode=mirror;; x) excl+=(--exclude "$OPTARG");; + h) echo "cos sync [-2|-M] [-x glob]... [remote-dir]"; exit 0;; + *) die "usage: cos sync [-2|-M] [-x glob]... [remote-dir]";; esac; done + shift $((OPTIND-1)) + local dir=${1:?local-dir required}; local remote=${2:-/work} + [ -d "$dir" ] || die "no such dir: $dir" + dir=$(cd "$dir" && pwd -P) || die "cannot resolve local dir: $dir" + case "$dir" in "$HOME"/*|/tmp/*|/private/tmp/*) :;; *) die "sync local dir must resolve under \$HOME or /tmp: $dir";; esac + case "$remote" in + *..*) die "remote path must not contain '..': $remote";; + /|/etc|/etc/*|/usr|/usr/*|/bin|/bin/*|/sbin|/sbin/*|/lib|/lib/*|/boot|/boot/*|/dev|/dev/*|/proc|/proc/*|/sys|/sys/*|/root|/root/*|/var|/var/*) die "refusing sync to system dir: $remote";; + esac + + # `pwd -P` above resolves symlinks so the guard can't be walked around — but on + # macOS /tmp IS a symlink to /private/tmp, and createos-cli's own guard accepts + # only $HOME or /tmp. Left as-is, every sync of a /tmp path is rejected by the CLI + # with a confusing "must be under $HOME or /tmp" about a path the user never typed. + # Validate the resolved form, hand the CLI the canonical one. + # Gate on /tmp and /private/tmp genuinely being the same directory. On Linux they + # are distinct, and rewriting blind would point the CLI at a different tree — which + # under `-M` (mirror) deletes box-side files based on the wrong source. + local cli_dir=$dir + if [ "$(cd /tmp 2>/dev/null && pwd -P)" = /private/tmp ]; then + case "$dir" in /private/tmp|/private/tmp/*) cli_dir="/tmp${dir#/private/tmp}";; esac + fi + + local -a args=(sandbox sync --local "$cli_dir" --remote "$remote" -y) + if "$CLI" sandbox sync --help 2>/dev/null | grep -q -- '--mode'; then + args+=(--mode "$mode") + local p; for p in "${DEFAULT_EXCLUDES[@]}"; do args+=(--exclude "$p"); done # skip big/regenerable dirs + [ "${#excl[@]}" -gt 0 ] && args+=("${excl[@]}") + else + [ "$mode" != two-way ] && echo "cos: ⚠ installed createos CLI lacks --mode; falling back to TWO-WAY (box writes bleed back). Upgrade the CLI for one-way/mirror." >&2 + echo "cos: ⚠ installed CLI lacks --exclude; syncing WITHOUT default excludes (node_modules/target/… WILL copy). Upgrade the CLI." >&2 + mode=two-way + fi + case "$mode" in + one-way) echo "cos: one-way sync $dir → $id:$remote (laptop wins; box changes NOT pulled back)" >&2;; + two-way) echo "cos: ⚠ two-way sync — sandbox-side writes (build output, deps) flow BACK to $dir" >&2;; + mirror) echo "cos: ⚠ mirror — files on the box not present in $dir will be DELETED" >&2;; + esac + echo "cos: first run downloads Mutagen (~60-90s before edits propagate); .git + node_modules/target/.venv/… excluded by default (build deps INSIDE the box via 'cos run')" >&2 + # The sandbox positional goes LAST: it was missing entirely (every sync died with + # "please provide a sandbox ID or name"), and it has to follow the flags because + # createos-cli stops parsing flags at the first positional — same trap as `tunnel`. + args+=("$id") + nohup "$CLI" "${args[@]}" >"$STATE_DIR/sync-$id.log" 2>&1 & + state_set sync_pid "$!" sync_dir "$dir" sync_remote "$remote" sync_mode "$mode" + echo "cos: sync pid $! ($mode, log: $STATE_DIR/sync-$id.log)" >&2 +} + +stop_tunnels(){ [ -f "$TUNLIST" ] || return 0; local pid rest + while read -r pid rest; do [ -n "$pid" ] && kill "$pid" 2>/dev/null && echo "cos: stopped tunnel $pid ($rest)" >&2 || true; done < "$TUNLIST" + rm -f "$TUNLIST"; } + +cmd_down(){ + _norm "$@"; set -- ${NORMA[@]+"${NORMA[@]}"} + local destroy_forks=0; local OPTIND=1 o + while getopts "fh" o; do case $o in + f) destroy_forks=1;; + h) echo "cos down [-f] (-f also destroys forks taken from this box)"; exit 0;; + *) die "usage: cos down [-f]";; esac; done + lock_state # held across read → destroy → unlink, so a concurrent `up` can't interleave + local id pid owned forks f; id=$(state_get id); pid=$(state_get sync_pid); owned=$(state_get owned) + forks=$(state_get fork_ids) + [ -n "$pid" ] && kill "$pid" 2>/dev/null && echo "cos: stopped sync $pid" >&2 || true + stop_tunnels + [ -n "$(state_get cluster_net)" ] && cluster_down + if [ -n "$forks" ]; then + if [ "$destroy_forks" = 1 ]; then + for f in $forks; do "$CLI" sandbox rm -y "$f" >/dev/null 2>&1 && echo "cos: destroyed fork $f" >&2 || true; done + else + echo "cos: ⚠ forks of this box survive (independent clones): $forks" >&2 + echo "cos: reap them: cos down -f | createos sandbox rm -y $forks" >&2 + fi + fi + # pre-`owned` state files predate ownership tracking; they only ever held boxes + # `up` created, so absent flag ⇒ owned. + if [ -n "$id" ]; then + if [ "${owned:-1}" = 0 ]; then + echo "cos: ⚠ $id was adopted, not created here — NOT destroying it." >&2 + echo "cos: destroy it yourself: createos sandbox rm -y $id" >&2 + else + "$CLI" sandbox rm -y "$id" >/dev/null 2>&1 && echo "cos: destroyed $id" >&2 || true + fi + fi + rm -f "$STATE"; echo "cos: state cleared" >&2 + unlock_state +} + +cmd_status(){ + [ -f "$STATE" ] || { echo "cos: no active box for this project"; return 0; } + local id; id=$(state_get id) + [ -n "$id" ] && echo "active: $id ($(state_get name)) shape=$(state_get shape) $([ "$(state_get owned)" = 0 ] && echo '[adopted — down will NOT destroy]' || echo '[created here]')" + local forks; forks=$(state_get fork_ids) + [ -n "$forks" ] && echo "forks: $forks (survive 'cos down'; reap with 'cos down -f')" + local pid; pid=$(state_get sync_pid) + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && echo "sync: pid $pid [$(state_get sync_mode)] $(state_get sync_dir) → $(state_get sync_remote)" || echo "sync: none" + if [ -f "$TUNLIST" ]; then echo "tunnels:" + while read -r pid rest; do kill -0 "$pid" 2>/dev/null \ + && echo " pid $pid 127.0.0.1:${rest%%:*} → box:${rest##*:}" || echo " (dead $pid $rest)"; done < "$TUNLIST" + fi + [ -n "$id" ] && "$CLI" -o json sandbox ls 2>/dev/null | jq -r --arg i "$id" '.[]|select(.id==$i)|"status: \(.status) ip=\(.ip)"' || true + [ -n "$(state_get cluster_net)" ] && cluster_ls +} + +# ───────────────────────────────────────── tunnel: box port → local (background) +cmd_tunnel(){ + local id; id=$(state_get id); [ -n "$id" ] || die "no active box — run 'cos up' first" + local remote=${1:?remote port required (port your service listens on INSIDE the box)} + local local_p=${2:-$remote} + numeric "$remote" || die "remote port not numeric: $remote" + numeric "$local_p" || die "local port not numeric: $local_p" + wait_running "$id" 15 || die "box $id not running" + local log="$STATE_DIR/tunnel-$id-$remote.log" + # flags MUST precede the positional — createos-cli's tunnel command + # stops parsing flags at the first positional arg, so `tunnel --remote X` + # drops --remote and dies "--remote is required". + nohup "$CLI" sandbox tunnel --remote "$remote" --local "$local_p" "$id" >"$log" 2>&1 & + local pid=$! + printf '%s %s:%s\n' "$pid" "$local_p" "$remote" >> "$TUNLIST" + sleep 1 + kill -0 "$pid" 2>/dev/null || { echo "cos: tunnel failed to start — see $log" >&2; tail -n 3 "$log" >&2 || true; return 1; } + echo "cos: tunnel pid $pid — http://127.0.0.1:$local_p → box:$remote (background; 'cos down' or 'cos status' to manage)" >&2 +} + +# ───────────────────────────────────────── expose: public HTTPS URL for a box port +cmd_expose(){ + local id; id=$(state_get id); [ -n "$id" ] || die "no active box — run 'cos up' first" + local port=${1:?port required (bind your service to 0.0.0.0:)} + numeric "$port" || die "port not numeric: $port" + "$CLI" sandbox edit "$id" --ingress on >/dev/null 2>&1 || die "failed to enable ingress on $id" + local tmpl; tmpl=$("$CLI" -o json sandbox get "$id" 2>/dev/null | jq -r '.ingress_url_template // empty') + [ -n "$tmpl" ] || die "ingress enabled but no URL template — check: createos sandbox get $id" + local url=${tmpl///$port} + echo "cos: public URL (service MUST bind 0.0.0.0:$port, not 127.0.0.1) — lives while the box does:" >&2 + echo "$url" + # Reachability probe. Ingress 502s until the service actually answers on + # 0.0.0.0:$port, so tell the caller now whether the box side is live instead + # of letting them discover a 502 in the browser. Non-fatal: the URL is valid + # for the box's lifetime and a not-yet-started service may come up later. + if have curl; then + local code=000 i + for i in 1 2 3; do + code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url" 2>/dev/null) # curl prints 000 on no-response + case $code in 2??|3??|4??) break;; esac + [ "$i" -lt 3 ] && sleep 2 + done + case $code in + 2??|3??|4??) echo "cos: ✓ reachable (HTTP $code)" >&2;; + *) echo "cos: ⚠ ingress on, but nothing answered on 0.0.0.0:$port (HTTP $code) — start the service / rebind it to 0.0.0.0, then reload (URL stays valid)." >&2;; + esac + fi +} +cmd_unexpose(){ + local id; id=$(state_get id); [ -n "$id" ] || die "no active box — run 'cos up' first" + "$CLI" sandbox edit "$id" --ingress off >/dev/null 2>&1 && echo "cos: ingress disabled for $id" >&2 || die "failed to disable ingress" +} + +# ───────────────────────────── cluster: N boxes on one private network (by-name DNS) +cluster_ls(){ + local net; net=$(state_get cluster_net); [ -n "$net" ] || { echo "cos: no cluster for this project"; return 0; } + echo "cluster net: $net members: $(state_get cluster_names)" + "$CLI" -o json sandbox network show "$net" 2>/dev/null \ + | jq -r '.members[]? | " \(.name // .sandbox_id) ip=\(.ip // "?") \(.status // "?")"' 2>/dev/null || true +} +cluster_up(){ + _norm "$@"; set -- ${NORMA[@]+"${NORMA[@]}"} + local shape=s-1vcpu-1gb rootfs=devbox:1 egress_all=0; COS_EGRESS=(); local -a _d; local OPTIND=1 o doms d + while getopts "s:r:e:p:Eh" o; do case $o in + s) shape=$OPTARG;; r) rootfs=$OPTARG;; + e) COS_EGRESS+=(--egress "$OPTARG");; + p) doms=$(egress_preset "$OPTARG") || die "unknown egress preset '$OPTARG'" + read -ra _d <<<"$doms"; for d in "${_d[@]}"; do COS_EGRESS+=(--egress "$d"); done;; + E) egress_all=1;; + h) echo "cos cluster up [-s shape] [-r rootfs] [-e dom|-p preset|-E]"; exit 0;; + *) die "usage: cos cluster up [-s shape] [-r rootfs] [-e dom|-p preset|-E]";; esac; done + shift $((OPTIND-1)) + local n=${1:?count required: cos cluster up } + numeric "$n" || die "count not numeric: $n" + [ "$n" -ge 2 ] || die "cluster needs N≥2 (use 'cos up' for one box)" + [ "$n" -le 8 ] || die "refusing N>8 — external keys allow 2 running at once; pick a small N and budget quota" + [ "$egress_all" = 1 ] && COS_EGRESS=() + [ -z "$(state_get cluster_net)" ] || die "cluster already up ('$(state_get cluster_net)') — 'cos cluster down' first" + local net; net="cos-net-$(proj_key)" + "$CLI" sandbox network create "$net" >/dev/null 2>&1 || true # tolerate 'already exists' + local ids="" names="" i nm boxid + export COS_NET="$net" # create_box picks this up and adds --network + for ((i=1;i<=n;i++)); do + nm="cos-cl-$(proj_key)-$i" + boxid=$(create_box "$nm" "$shape" "$rootfs") || die "failed to create $nm" + ids="$ids $boxid"; names="$names $nm" + echo "cos: member $i/$n up: $boxid ($nm)" >&2 + done + unset COS_NET + state_set cluster_net "$net" cluster_ids "${ids# }" cluster_names "${names# }" + # Peers resolve on the FQDN only — the bare short name is NXDOMAIN in the guest. + echo "cos: cluster '$net' up ($n boxes) — peers reach each other by name, e.g. curl http://cos-cl-$(proj_key)-2.fc.local:PORT" >&2 + cluster_ls +} +cluster_run(){ + local net; net=$(state_get cluster_net); [ -n "$net" ] || die "no cluster — 'cos cluster up ' first" + local all=0; case "${1:-}" in -a|--all) all=1; shift;; esac + local -a IDA NMA; read -ra IDA <<<"$(state_get cluster_ids)"; read -ra NMA <<<"$(state_get cluster_names)" + if [ "$all" = 1 ]; then + local cmd="$*"; [ -n "$cmd" ] || die "command required" + local k agg=0; for k in "${!IDA[@]}"; do + echo "cos: ── ${NMA[$k]} (${IDA[$k]}) ──" >&2 + run_keepalive "${IDA[$k]}" "$cmd" "" || true + { [ "${INFRA_FAIL:-0}" = 1 ] || [ "${BUILD_RC:-1}" != 0 ]; } && agg=1 + done + return "$agg" + fi + local target=${1:?target required: member index (1..N) or name, or -a for all}; shift + local cmd="$*"; [ -n "$cmd" ] || die "command required" + local id="" k + if numeric "$target"; then [ "$target" -ge 1 ] || die "member index must be ≥1 (have: $(state_get cluster_names))"; id="${IDA[$((target-1))]:-}" + else for k in "${!NMA[@]}"; do [ "${NMA[$k]}" = "$target" ] && id="${IDA[$k]}"; done; fi + [ -n "$id" ] || die "member '$target' not in cluster ($(state_get cluster_names))" + wait_running "$id" 15 || die "member $id not running" + run_keepalive "$id" "$cmd" "" || true + return "${BUILD_RC:-1}" +} +cluster_down(){ + local net ids id; net=$(state_get cluster_net); ids=$(state_get cluster_ids) + [ -n "$net$ids" ] || { echo "cos: no cluster" >&2; return 0; } + for id in $ids; do "$CLI" sandbox rm -y "$id" >/dev/null 2>&1 && echo "cos: destroyed $id" >&2 || true; done + [ -n "$net" ] && { "$CLI" sandbox network rm "$net" -y >/dev/null 2>&1 && echo "cos: deleted net $net" >&2 || true; } + state_set cluster_net "" cluster_ids "" cluster_names "" + echo "cos: cluster torn down" >&2 +} +cmd_cluster(){ + local action=${1:-}; shift || true + case "$action" in + up) cluster_up "$@";; + run) cluster_run "$@";; + ls|status) cluster_ls;; + down) cluster_down;; + *) die "usage: cos cluster up [-s|-r|-e|-p|-E] | run [|-a] | ls | down";; + esac +} + +# ───────────────────────────── vpn: WireGuard L3 from this machine into private nets +cmd_vpn(){ + local sub=${1:-up}; shift || true + have wg-quick || die "WireGuard 'wg-quick' missing — install wireguard-tools (macOS: brew install wireguard-tools; Debian: apt install wireguard-tools), then retry" + case "$sub" in + register) "$CLI" sandbox devices register "$@";; + up) echo "cos: WireGuard up — needs sudo for routes, blocks until Ctrl-C. If it says 'not registered', run: cos vpn register " >&2 + exec "$CLI" sandbox vpn up;; + *) die "usage: cos vpn [up | register [name]]";; + esac +} + +# ───────────────────────────── fork: snapshot the project box into an independent clone +cmd_fork(){ + local id; id=$(state_get id); [ -n "$id" ] || die "no active project box — 'cos up' first" + wait_running "$id" 15 || die "box $id not running" + echo "cos: pausing $id to snapshot (brief; needed to fork)…" >&2 + "$CLI" sandbox pause "$id" >/dev/null 2>&1 || die "pause failed" + local before after newid + before=$("$CLI" -o json sandbox ls 2>/dev/null | jq -r '.[].id' | sort) + echo "cos: forking…" >&2 + if ! "$CLI" sandbox fork "$id" >"$STATE_DIR/last-fork.log" 2>&1; then + "$CLI" sandbox resume "$id" >/dev/null 2>&1 || true + die "fork failed — see $STATE_DIR/last-fork.log (project box resumed)" + fi + after=$("$CLI" -o json sandbox ls 2>/dev/null | jq -r '.[].id' | sort) + # brace the expansion: a bare `$id` followed directly by a multibyte char (the + # ellipsis) is parsed as part of the variable NAME, so `set -u` kills the script + # here — after the fork has already happened, orphaning the clone. + echo "cos: resuming project box ${id}…" >&2 + "$CLI" sandbox resume "$id" >/dev/null 2>&1 || echo "cos: ⚠ resume of $id failed — run 'createos sandbox resume $id'" >&2 + newid=$(comm -13 <(printf '%s\n' "$before") <(printf '%s\n' "$after") | head -1) + [ -n "$newid" ] || { echo "cos: forked, but couldn't auto-resolve the new id — see 'createos sandbox ls'" >&2; return 0; } + # A fork is an independent clone, so `down` won't destroy it — but record it, or + # nothing ever will and it leaks silently. `down` reports it; `down -f` reaps it. + local forks; forks=$(state_get fork_ids) + state_set fork_ids "${forks:+$forks }$newid" + echo "cos: fork ready: $newid (independent clone; NOT tracked as the project box)" >&2 + echo "cos: exec: createos sandbox exec --stream $newid -- bash -lc '…'" >&2 + echo "cos: destroy: createos sandbox rm -y $newid (or 'cos down -f' to reap forks with the box)" >&2 + echo "$newid" +} + +# ───────────────────────────── pause/resume: park the warm box at zero compute cost +# `down` destroys — the next session reinstalls every dependency. `pause` snapshots +# disk + memory to storage and frees the host, so a box with a warm toolchain costs +# nothing while idle and comes back with its processes intact. Both operations take +# ~6-8s end to end here (the CLI polls for the state transition); resume is slower +# when the snapshot has to be pulled to a host other than the one it was taken on. +cmd_pause(){ + case "${1:-}" in -h|--help) echo "cos pause snapshot the project box; compute billing stops, state survives"; return 0;; + ?*) die "cos pause takes no arguments (got '$1')";; esac + lock_state # held across status check → helper teardown → pause, so a concurrent + # up/down/sync can't interleave and strand a sync pid we just cleared + local id; id=$(state_get id); [ -n "$id" ] || { unlock_state; die "no active project box — 'cos up' first"; } + case "$(box_status "$id")" in + paused) :;; # still tear helpers down: an idle auto-pause leaves them running + running) :;; + '') unlock_state; die "box $id no longer exists — 'cos down' to clear stale state";; + *) unlock_state; die "box $id is '$(box_status "$id")' — only a running box can be paused";; + esac + # A paused box serves no traffic, so a live sync would spin on errors and the + # tunnel pids are already dead sockets. Tear both down rather than leave them + # half-alive; the caller re-establishes them after resume. + local pid; pid=$(state_get sync_pid) + [ -n "$pid" ] && kill "$pid" 2>/dev/null && echo "cos: stopped sync $pid (re-run 'cos sync' after resume)" >&2 || true + state_set sync_pid "" + stop_tunnels + if [ "$(box_status "$id")" = paused ]; then + echo "cos: $id already paused (helpers torn down)" >&2; unlock_state; return 0 + fi + echo "cos: pausing $id — snapshotting disk+memory, compute billing stops…" >&2 + "$CLI" sandbox pause "$id" >/dev/null 2>&1 || { unlock_state; die "pause failed — try: createos sandbox pause $id"; } + echo "cos: $id paused. Deps, files and disks survive; 'cos resume' (or 'cos up') brings it back." >&2 + unlock_state +} + +cmd_resume(){ + case "${1:-}" in -h|--help) echo "cos resume restore the paused project box (restarts compute billing)"; return 0;; + ?*) die "cos resume takes no arguments (got '$1')";; esac + local id; id=$(state_get id); [ -n "$id" ] || die "no active project box — 'cos up' first" + case "$(box_status "$id")" in + running) echo "cos: $id already running" >&2; return 0;; + paused) :;; + '') die "box $id no longer exists — 'cos down' to clear stale state, then 'cos up'";; + *) die "box $id is '$(box_status "$id")' — cannot resume from that state";; + esac + box_resume_if_paused "$id" + echo "cos: $id running. Re-run 'cos sync' / 'cos tunnel' / 'cos expose' if you had them up." >&2 +} + +# ───────────────────────────── template: build a custom rootfs from a Dockerfile +# Bake the toolchain once instead of reinstalling it on every offload. The build +# service enforces constraints that are easy to trip and only surface as a 400 +# after upload, so preflight them locally and fail with the actual reason. +template_validate(){ + local f=$1 size froms copies + [ -f "$f" ] || die "no such Dockerfile: $f" + size=$(wc -c <"$f" | tr -d ' ') + [ "$size" -le 65536 ] || die "Dockerfile is ${size} bytes — the build service caps source at 64 KiB" + # `--` on every grep: a Dockerfile path starting with '-' would otherwise be + # consumed as options, silently changing (or skipping) validation. + froms=$(grep -ciE -- '^[[:space:]]*FROM[[:space:]]' "$f" || true) + [ "$froms" = 1 ] || die "Dockerfile has $froms FROM line(s) — template builds are single-stage, exactly one FROM" + if grep -qE -- '^[[:space:]]*[Ff][Rr][Oo][Mm][[:space:]]+.*\$' "$f"; then + die "FROM uses a variable — ARG-substituted base images are rejected; write the base image literally" + fi + copies=$(grep -ciE -- '^[[:space:]]*(COPY|ADD)[[:space:]]' "$f" || true) + [ "$copies" = 0 ] || die "Dockerfile has $copies COPY/ADD line(s) — no build context is uploaded, so they cannot work. Fetch what you need inside a RUN instead." + echo "cos: Dockerfile preflight ok — single-stage, no COPY/ADD, ${size}B" >&2 + echo "cos: note — FROM must be an operator-allowlisted base (e.g. nodeops/sandbox:debian); the build rejects anything else." >&2 +} + +cmd_template(){ + local action=${1:-}; shift || true + case "$action" in + submit|build|create) + local name="" file=Dockerfile; local -a pass=() + # -f/--file is consumed here so the Dockerfile can be preflighted; the rest passes through + while [ $# -gt 0 ]; do + case "$1" in + -f|--file) file=${2:?path required after -f}; shift 2;; + # -f=X / --file=X are valid CLI syntax; without these the wrapper would + # preflight the default Dockerfile and submit a different, unvalidated one + -f=*|--file=*) file=${1#*=}; shift;; + -*) pass+=("$1"); shift;; + *) if [ -n "$name" ]; then pass+=("$1"); else name=$1; fi; shift;; + esac + done + [ -n "$name" ] || die "usage: cos template submit [-f Dockerfile]" + template_validate "$file" + echo "cos: submitting '$name' — build is async; logs stream below (2 concurrent builds per account)" >&2 + # flags before the positional — urfave/cli stops flag parsing at the first + # positional arg, the same trap that bit `sandbox tunnel` + "$CLI" sandbox template submit -f "$file" ${pass[@]+"${pass[@]}"} "$name" + echo "cos: once status is 'ready', boot from it: cos up -r $name | cos offload -r $name . ''" >&2;; + ls|list) "$CLI" sandbox template ls "$@";; + show|get) "$CLI" sandbox template show "$@";; + logs) "$CLI" sandbox template logs "$@";; + rm|delete) "$CLI" sandbox template rm "$@";; + *) die "usage: cos template submit [-f Dockerfile] | ls | show | logs [-f] | rm + A custom rootfs is slow on its FIRST boot per host (image fetch) and fast after. + Built-ins (devbox:1, ubuntu:26.04, debian:13, alpine:3.20) are kept warm — no pull.";; + esac +} + +# ───────────────────────────── shell: instant throwaway interactive Linux (keyless) +SHELL_ID="" +on_shell_exit(){ [ -n "$SHELL_ID" ] || return 0 + "$CLI" sandbox rm -y "$SHELL_ID" >/dev/null 2>&1 && echo "cos: destroyed scratch $SHELL_ID" >&2 || true; } +cmd_shell(){ + _norm "$@"; set -- ${NORMA[@]+"${NORMA[@]}"} + local shape=s-1vcpu-1gb rootfs=devbox:1 egress_all=0; COS_EGRESS=(); local -a _d; local OPTIND=1 o doms d + while getopts "s:r:e:p:Eh" o; do case $o in + s) shape=$OPTARG;; r) rootfs=$OPTARG;; + e) COS_EGRESS+=(--egress "$OPTARG");; + p) doms=$(egress_preset "$OPTARG") || die "unknown egress preset '$OPTARG'" + read -ra _d <<<"$doms"; for d in "${_d[@]}"; do COS_EGRESS+=(--egress "$d"); done;; + E) egress_all=1;; + h) echo "cos shell [-s shape] [-r rootfs] [-e dom|-p preset|-E]"; exit 0;; + *) die "usage: cos shell [-s shape] [-r rootfs] [-e dom|-p preset|-E]";; esac; done + [ "$egress_all" = 1 ] && COS_EGRESS=() + SHELL_ID=$(create_box "cos-sh-$$-${RANDOM}" "$shape" "$rootfs") + trap on_shell_exit EXIT INT TERM # destroy on exit / Ctrl-C / error + echo "cos: scratch box $SHELL_ID ($shape/$rootfs) — exit the shell to destroy it" >&2 + wait_running "$SHELL_ID" 30 || die "scratch box not running" + "$CLI" sandbox shell "$SHELL_ID" || true +} + +# ───────────────────────────── fanout: run commands across N throwaway boxes (parallel) +fanout_usage(){ cat <<'EOF' +cos fanout — run commands across N throwaway boxes in parallel (stage once → run each → collect → destroy). + cos fanout [flags] [cmd2] [cmd3] ... +Each runs in its OWN isolated box, staged from . Per-job logs + exit codes summarized at the end. +flags: -j N (max concurrent, default 2 = external-key quota) · -s shape · -r rootfs · -p preset · -e dom · -E · -x glob +example: + cos fanout -j 2 -p python-uv . 'pytest -q tests/a' 'pytest -q tests/b' 'pytest -q tests/c' +EOF +} +fanout_one(){ # $1=idx $2=cmd $3=shape $4=rootfs $5=staged-tar $6=resdir + local i=$1 cmd=$2 shape=$3 rootfs=$4 staged=$5 resdir=$6 id + id=$(create_box "cos-fo-$$-$i-${RANDOM}" "$shape" "$rootfs" 2>/dev/null) \ + || { echo 127 > "$resdir/$i.rc"; echo "create failed" > "$resdir/$i.log"; return; } + if wait_running "$id" 30 \ + && "$CLI" sandbox push "$id" - /work.tar < "$staged" >/dev/null 2>&1 \ + && "$CLI" sandbox exec "$id" -- bash -lc 'mkdir -p /work && tar -C /work -xf /work.tar && rm -f /work.tar' >/dev/null 2>&1; then + run_keepalive "$id" "$cmd" /work "$resdir/$i.log" >/dev/null 2>&1 || true + echo "${BUILD_RC:-1}" > "$resdir/$i.rc" + else + echo "stage/boot failed" >> "$resdir/$i.log"; echo 1 > "$resdir/$i.rc" + fi + "$CLI" sandbox rm -y "$id" >/dev/null 2>&1 || true +} +cmd_fanout(){ + _norm "$@"; set -- ${NORMA[@]+"${NORMA[@]}"} + local shape=s-1vcpu-1gb rootfs=devbox:1 jobs=2 egress_all=0; COS_EGRESS=(); local -a excl=() _d; local OPTIND=1 o doms d + while getopts "s:r:j:e:p:x:Eh" o; do case $o in + s) shape=$OPTARG;; r) rootfs=$OPTARG;; j) jobs=$OPTARG;; + e) COS_EGRESS+=(--egress "$OPTARG");; + p) doms=$(egress_preset "$OPTARG") || die "unknown egress preset '$OPTARG'" + read -ra _d <<<"$doms"; for d in "${_d[@]}"; do COS_EGRESS+=(--egress "$d"); done;; + x) excl+=("$OPTARG");; + E) egress_all=1;; + h) fanout_usage; exit 0;; + *) fanout_usage >&2; exit 2;; esac; done + shift $((OPTIND-1)) + [ $# -ge 2 ] || { fanout_usage; exit 0; } + local dir=$1; shift + [ -d "$dir" ] || die "no such dir: $dir" + numeric "$jobs" && [ "$jobs" -ge 1 ] || die "-j must be a positive integer" + [ "$egress_all" = 1 ] && COS_EGRESS=() + local -a cmds=("$@"); local n=${#cmds[@]} + echo "cos: fanout $n job(s), ≤$jobs concurrent ($shape). External keys allow 2 running — keep -j ≤2." >&2 + + # stage the input tree once; every job pushes the same tarball + local -a tarx=(); local p + for p in "${DEFAULT_EXCLUDES[@]}" ${excl[@]+"${excl[@]}"}; do tarx+=(--exclude "$p"); done + local staged resdir; staged=$(mktemp "$STATE_DIR/fanout-stage.XXXXXX"); resdir=$(mktemp -d "$STATE_DIR/fanout.XXXXXX") + tar "${tarx[@]}" -c -C "$dir" . > "$staged" 2>/dev/null || die "staging tar failed" + + local i; local -a pids=() # bash-3.2-safe throttle: cap concurrency by waiting on the oldest pid + for ((i=0;i/dev/null || true; pids=("${pids[@]:1}"); fi + done + wait + rm -f "$staged" + + echo "cos: ── fanout results ──" >&2 + local rc all=0 + for ((i=0;i/dev/null || echo '?') + echo " job $i rc=$rc log=$resdir/$i.log cmd: ${cmds[$i]}" >&2 + [ "$rc" = 0 ] || all=1 + done + return "$all" +} + +# ───────────────────────────── disk: BYO S3 bucket mounts (attach/detach on the project box) +cmd_disk(){ + local action=${1:-}; shift || true + case "$action" in + create) echo "cos: ⚠ secret keys passed on the command line are visible to other local users (ps) and saved in shell history — prefer the CLI's interactive prompts" >&2; "$CLI" sandbox disk create "$@";; + ls|list) "$CLI" sandbox disk ls "$@";; + show|get) "$CLI" sandbox disk show "$@";; + rm|delete) "$CLI" sandbox disk rm "$@";; + attach) local id; id=$(state_get id); [ -n "$id" ] || die "no active box — 'cos up' first" + local disk=${1:?disk name/id required} mp=${2:?mount path required (e.g. /mnt/data)} + "$CLI" sandbox disk attach "$id" "$disk" "$mp";; + detach) local id; id=$(state_get id); [ -n "$id" ] || die "no active box — run 'cos up' first" + local disk=${1:?disk name/id required} mp=${2:?mount path required} + "$CLI" sandbox disk detach "$id" "$disk" "$mp";; + *) die "usage: cos disk create --bucket --endpoint --access-key --secret-key [--region] [--path-style] | ls | show | attach | detach | rm ";; + esac +} + +# ── auto-install the createos CLI (official one-liner) when it's missing ─────── +ensure_cli(){ + have "$CLI" && return 0 + # only auto-install the stock binary; a custom COS_CLI path is the user's to manage + [ "$CLI" = createos ] || die "createos CLI '$CLI' (COS_CLI) not found — install it or unset COS_CLI" + [ -z "${COS_NO_AUTOINSTALL:-}" ] || die "createos CLI not found (COS_NO_AUTOINSTALL set) — install: $CLI_INSTALL_URL" + have curl || die "createos CLI not found and curl missing — install manually: $CLI_INSTALL_URL" + echo "cos: createos CLI not found — installing via $CLI_INSTALL_URL" >&2 + if [ "$CLI_INSTALL_URL" != "$CLI_INSTALL_URL_DEFAULT" ] && [ -z "${COS_AUTOINSTALL_ALLOW_CUSTOM_URL:-}" ]; then + die "refusing to auto-install createos from a custom COS_CLI_INSTALL_URL ($CLI_INSTALL_URL) — install manually, or set COS_AUTOINSTALL_ALLOW_CUSTOM_URL=1 to trust it" + fi + curl -sfL "$CLI_INSTALL_URL" | sh - >&2 || die "createos install failed — try manually: curl -sfL $CLI_INSTALL_URL | sh -" + # installer may land in ~/.local/bin, which isn't on this shell's PATH yet + case ":$PATH:" in *":$HOME/.local/bin:"*) :;; *) PATH="$HOME/.local/bin:$PATH";; esac + hash -r 2>/dev/null || true + have "$CLI" || die "createos installed but not on PATH — add /usr/local/bin or ~/.local/bin to PATH and retry" + echo "cos: createos installed ($("$CLI" version 2>/dev/null | head -1 || echo ok))." >&2 +} + +# ── auth preflight ──────────────────────────────────────────────────────────── +# `createos login` is a TTY prompt (interactive select → browser OAuth), so an +# agent shell can never drive it. Detect credentials locally instead and hand +# the sign-in off to the human. CREATEOS_API_KEY wins: set it and no browser +# login is needed at all. +CREATEOS_DIR=$HOME/.createos + +auth_method(){ + [ -n "${CREATEOS_API_KEY:-}" ] && { echo env; return 0; } + [ -f "$CREATEOS_DIR/.token" ] && { echo token; return 0; } + [ -f "$CREATEOS_DIR/.oauth" ] && { echo oauth; return 0; } + return 1 +} + +ensure_auth(){ + auth_method >/dev/null && return 0 + cat >&2 <<'EOF' +cos: not signed in to CreateOS. + + Ask the user to run this in their own terminal. It needs a TTY and a browser, + so it cannot be run from an agent shell: + + createos login # then pick "Sign in with browser (recommended)" + + Or, for headless / CI / no-browser setups, an API key from + https://createos.sh skips browser login entirely: + + export CREATEOS_API_KEY= + + Never ask the user to paste an API key into the conversation — it would be + written to the transcript. Have them export it, or sign in with the browser. + + Then re-run: cos auth +EOF + exit 1 +} + +cmd_auth(){ + local m + m=$(auth_method) || ensure_auth + case "$m" in + env) echo "cos: signed in via CREATEOS_API_KEY (browser login not needed)";; + token) echo "cos: signed in via API token ($CREATEOS_DIR/.token)";; + oauth) echo "cos: signed in via browser OAuth session ($CREATEOS_DIR/.oauth)";; + esac +} + +cmd_install(){ + local target=${1:-$HOME/.local/bin/cos} self dir + self=$(perl -MCwd=abs_path -e 'print abs_path(shift)' "$0" 2>/dev/null || printf '%s' "$0") + dir=$(dirname "$target"); mkdir -p "$dir"; ln -sf "$self" "$target" + echo "cos: linked $target → $self" >&2 + case ":$PATH:" in *":$dir:"*) echo "cos: $dir on PATH ✓" >&2;; + *) echo "cos: ⚠ $dir not on PATH — add: export PATH=\"$dir:\$PATH\"" >&2;; esac +} + +main_usage(){ cat <<'EOF' +cos — CreateOS sandbox as remote compute. (run `cos install` to put `cos` on PATH) + cos auth check sign-in (CREATEOS_API_KEY, or `createos login` in a real terminal) + cos offload [flags] one-shot: stage→run(keepalive)→pull→destroy (cos offload -h for flags) + cos fanout [-j N][flags] ... run each in its own throwaway box, in parallel (cos fanout -h) + cos shell [-s][-r][-e|-p|-E] instant throwaway interactive Linux (destroyed on exit) + cos up [-s][-r][-n][-e|-p|-E][-a] create/reuse project box (-a adopt a box cos didn't create) + cos run run in project box (keepalive; ONE string, no '--' separator) + cos sync [-2|-M][-x glob] [remote] file sync (default one-way, -2 two-way, -M mirror; big dirs excluded) + cos tunnel [local] forward box port → 127.0.0.1 (background) + cos expose public HTTPS URL for a box port (unexpose to revoke) + cos cluster up [-s|-r|-e|-p|-E] | run [|-a] | ls | down N boxes on one private net + cos disk create|ls|attach |detach|rm BYO S3 bucket mounts + cos vpn [up|register [name]] WireGuard L3 into your private networks (needs wg-quick) + cos fork snapshot project box → independent clone (survives down; -f reaps) + cos pause | cos resume park the warm box at zero compute cost / bring it back + cos template submit [-f Dockerfile] | ls | show | logs | rm custom rootfs (bake the toolchain once) + cos down [-f] stop sync/tunnels + destroy box (+ cluster; -f also destroys forks) + cos status show active box + sync + tunnels + cluster + +One-shot work belongs in `offload` (auto-destroys), not `up`+`run` (box persists until `down`). +egress: default = unrestricted (box reaches any host); restrict with -e or -p ; -E forces unrestricted +EOF +} + +# install needs no createos; auth needs the CLI but is itself the sign-in check; +# help must work when signed OUT, or a new user can't discover how to sign in. +sub=${1:-}; shift || true +case "$sub" in + install) :;; + auth) ensure_cli;; + help|-h|--help|'') main_usage; exit 0;; + *) ensure_cli; have jq || die "jq required"; ensure_auth;; +esac +case "$sub" in + install) cmd_install "$@";; + auth) cmd_auth "$@";; + offload) cmd_offload "$@";; + fanout) cmd_fanout "$@";; + shell) cmd_shell "$@";; + up) cmd_up "$@";; + run) cmd_run "$@";; + sync) cmd_sync "$@";; + tunnel) cmd_tunnel "$@";; + expose) cmd_expose "$@";; + unexpose) cmd_unexpose "$@";; + cluster) cmd_cluster "$@";; + disk) cmd_disk "$@";; + vpn) cmd_vpn "$@";; + fork) cmd_fork "$@";; + pause) cmd_pause "$@";; + resume) cmd_resume "$@";; + template) cmd_template "$@";; + down) cmd_down "$@";; + status) cmd_status "$@";; + *) echo "cos: unknown subcommand '$sub'" >&2; main_usage >&2; exit 2;; +esac diff --git a/packages/codex-plugin/scripts/session-start.sh b/packages/codex-plugin/scripts/session-start.sh new file mode 100755 index 0000000..2c207e3 --- /dev/null +++ b/packages/codex-plugin/scripts/session-start.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Session start hook — publish the absolute path of the `cos` driver into context. +set -euo pipefail + +# Resolve cos relative to this script +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cos="$SCRIPT_DIR/cos" + +if [ ! -x "$cos" ]; then + echo "[createos-sandbox] The sandbox driver is MISSING or not executable at: $cos" + echo "Do not attempt sandbox work. Tell the user the plugin looks broken and stop." + exit 0 +fi + +if command -v cos >/dev/null 2>&1; then + where="cos is already on PATH — call it bare." +else + where="cos is NOT on PATH. Call it by this absolute path: $cos" +fi + +cat < ''. ONE command. It creates the box, ships for you, runs, and destroys the box. +- Work that must outlive one command — a dev server, a watcher, a multi-command session: cos up, then cos run, then cos pause or cos down. +- cos run takes the command as one plain string; there is no -- separator. + +Run $cos help for the full verb list, and read the using-createos-sandbox skill before anything involving egress restriction, networking, or file sync. +CONTEXT diff --git a/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md b/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md new file mode 100644 index 0000000..a7d8083 --- /dev/null +++ b/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md @@ -0,0 +1,187 @@ +--- +name: using-createos-sandbox +description: Use when you need to run code OFF the user's machine — heavy/long builds or test suites, untrusted or unknown code, a parallel test/config matrix across many boxes, an instant clean Linux to try a tool, a live dev-server/watcher Claude edits against, reaching a box-side service from localhost (port tunnel) or sharing it on the public web (HTTPS preview URL), a multi-machine cluster on one private network, a WireGuard VPN into that network, or mounting an S3 bucket of data. Offloads to ephemeral CreateOS Sandboxes via the `cos` helper (stage → exec → pull → auto-destroy), plus fanout, a scratch shell, and an opt-in reusable box with sync, tunnel, expose, cluster, disk, vpn, pause/resume, custom images, and snapshot/fork. +--- + +# Using CreateOS Sandbox as remote compute + +A CreateOS Sandbox is an isolated Linux box that goes from create to running your first command in roughly 200 ms. Use it as throwaway compute instead of running risky or heavy work on the user's laptop. + +## Running the driver + +Everything goes through `cos`. **A SessionStart hook prints its absolute path into your context at the start of the session — use that literal path.** + +Do not write `${CLAUDE_PLUGIN_ROOT}` into a Bash command. That variable is set when slash commands are loaded but is **unset in the Bash tool's environment**, so the path collapses to `/scripts/cos` and dies with exit 127. + +If you cannot locate or run `cos`, **stop and say so.** Do not fall back to composing the job out of raw `createos sandbox create/push/exec` calls. That path looks equivalent and is not: it silently drops egress restriction, the keepalive that survives a dropped stream on a long build, guaranteed auto-destroy, and the auth preflight — so a "successful" run can leave an unrestricted box billing with no isolation ever applied. A missing driver is a hard stop, not a reason to improvise. + +`cos install` symlinks it into `~/.local/bin` if the user wants it on PATH permanently. It wraps the authed `createos` CLI and needs `jq`, `tar`, `perl`, and `curl`; if the `createos` CLI is missing it auto-installs it from the official script (opt out with `COS_NO_AUTOINSTALL=1`). + +## Setup — check this once per session, before the first offload + +```bash +cos auth +``` + +Healthy output names one of three credential sources: `CREATEOS_API_KEY`, a browser OAuth session, or an API token file. Anything else means not signed in. + +**You cannot fix that yourself.** `createos login` is an interactive TTY prompt that opens a browser, and an agent shell has no TTY. Do not try to run it and do not work around it with `--token`. Relay the two options to the user: + +1. **Browser (recommended)** — they run `createos login` in their own terminal and pick "Sign in with browser". +2. **API key** — they `export CREATEOS_API_KEY=` (from ) in the shell that launched Claude Code. + +**Never ask the user to paste an API key into the conversation** — it lands in the transcript. Export or browser, nothing else. + +Every `cos` command except `install` and `auth` runs this check first, so an unauthenticated box never gets tarballed and uploaded before failing. + +## When to reach for it + +| Situation | Why offload | +|---|---| +| **Untrusted / unknown code** — a snippet, a fresh npm/pip package, scraped code, a PoC exploit | Isolation. The blast radius is one disposable box, not the laptop. | +| **Heavy build or test suite** — big `make`, full test run, compile, benchmark | Keeps the laptop free; runs on a box sized for it. | +| **Parallel/matrix work** — same job across N configs, test shards, batch | `fanout` — each command in its own throwaway box, concurrently, results collected. | +| **Quick scratch Linux** — try a CLI/tool/snippet on a clean box | `shell` — instant keyless box, destroyed on exit (interactive; the user runs it). | +| **Clean-room repro** — "works on my machine" bugs, dependency conflicts | Fresh rootfs every time, no host state. | +| **Live dev loop** — dev server / test watcher / REPL that reacts to edits | Project box + `sync`; Claude edits locally, the box reacts. | +| **Reach a box-side service** — dev server, DB, API | `tunnel` (private, to `127.0.0.1`) or `expose` (public HTTPS link to share). | +| **Multi-machine** — distributed system, DB replication, p2p mesh, load test | `cluster up N` — boxes share one private net, reach each other by name. | +| **Same setup, many variants** — try N branches from one prepared box | `fork` the project box into independent clones. | +| **Repeated identical setup** — every offload starts with the same install prelude | `template` — bake the toolchain into an image once. | +| **Done for now, back tomorrow** — warm box you don't want to rebuild | `pause` — snapshot at zero compute cost, `resume` restores it exactly. | +| **Big data / weights / shared cache** | `disk` — BYO S3 bucket mounted into the box, survives box death. | + +Do NOT offload trivial commands, anything needing the user's local secrets/SSH/cloud creds, or work that must touch real local filesystem state. + +## Picking the verb — decide this before typing anything + +Almost every task is one of two shapes, and picking the wrong one wastes a lot of motion: + +- **"Run this and tell me the result"** — a test suite, a build, a script, anything with an end. → **`cos offload `.** One command. It creates the box, ships the directory, runs, and destroys the box. Nothing to clean up. +- **"Keep a box around while I work"** — a dev server you'll hit repeatedly, a watcher reacting to edits, a session spanning many commands. → **`cos up`**, then `run`/`sync`, then `pause` or `down`. + +If you find yourself doing any of the following, you have picked the wrong shape and should stop and use `offload` instead: + +- running `cos up` for a task that has a clear finish line +- tarring, base64-encoding, or `push`-ing files into the box by hand — **`offload` stages the directory for you**, with sensible excludes, in the same command +- reaching for `cos status` to decide what to do first — for one-shot work there is nothing to check, just offload + +`cos run` takes the command as one plain string. There is no `--` separator: `cos run 'npm ci && npm test'`. + +## Pattern A — one-shot offload (the default, and the safe one) + +Stage a directory, run, optionally pull artifacts back, **always auto-destroys**. Flags come **before** the ` ` positionals. + +```bash +# run a test suite off-machine (the preset opens the registries it needs) +cos offload -p python-uv . 'uv sync --frozen --group dev && uv run pytest -q' + +# Python + Rust, compose presets, exclude build dirs, pull artifacts back +cos offload -p python-uv -p rust-cargo -x target -o dist . 'uv sync --frozen && uv run pytest -q' + +# trusted heavy build, explicitly unrestricted egress +cos offload -E -s s-2vcpu-2gb . 'cargo build --release' + +# untrusted script, outbound locked to exactly what it needs +cos offload -e pypi.org -e files.pythonhosted.org ./suspect 'python3 main.py' +``` + +Two things about this that are easy to get wrong: + +- **Egress is unrestricted by default.** A fresh box can reach anything; `cos` prints a one-line notice. Restricting is opt-in with `-p ` or `-e `. So "run this untrusted thing in a sandbox" is only half done until you pass one of those. +- **Uploads are one-way.** Box-side changes never touch the local tree unless you ask with `-o `. `.git`, `node_modules`, `target`, `.venv` and friends are excluded from the upload by default — dependencies are meant to be built *inside* the box. + +Long, quiet builds survive a dropped connection: the command runs detached with a heartbeat watcher that re-attaches if the stream dies. The real exit code is preserved. + +For the full flag table, the egress presets, the enforcement caveats, fanout, and the OOM/disk/bandwidth traps on heavy builds → **`references/offload-and-egress.md`**. + +### Fanout — same input, many boxes, in parallel + +```bash +cos fanout -j 2 -p python-uv . 'pytest -q tests/unit' 'pytest -q tests/integration' 'ruff check' +``` + +Each job gets its own box with no shared network — that is what distinguishes it from `cluster`. `-j` defaults to 2 to match the concurrency external keys have been observed to allow; going higher just queues the extra jobs rather than failing. + +## Pattern B — reusable project box (opt-in) + +For repeated runs against a warm box, or a dev server Claude edits against. One box per git root, tracked in a statefile. + +```bash +cos up -s s-2vcpu-2gb # create/reuse this project's box +cos run 'npm ci' # warm it — deps persist across runs +cos sync ~/app /work # one-way by default (laptop → box), background +cos run 'npm run dev &' # start a watcher; it sees synced edits +cos status # box + sync + tunnels + forks +cos pause # park it at zero compute cost +cos resume # bring it back exactly as it was +cos down # stop sync + destroy the box +``` + +**`up` is for a box you intend to reuse and then tear down.** A bare "run this in a sandbox" is *not* Pattern B — use `cos offload` (one-shot, auto-destroys) or `cos shell`. Reaching for `up` to satisfy "create a sandbox" makes the box outlive the task, and a later `cos down` destroys it along with anything else sharing that statefile. + +**Ending a session: prefer `pause` over `down`** when the box has a warm toolchain the user will want again. `down` destroys and the next session reinstalls everything; `pause` snapshots disk *and* memory, stops compute billing, and brings everything back on `resume` — measured end-to-end at around 6–8 s each way through the CLI. Use `down` when the work is genuinely finished. + +If a box under this project's name is running but the statefile is gone (another checkout, another agent, created by hand), `up` **refuses** rather than adopting it — adopting silently would let a later `cos down` destroy a box this project never created. `cos up -a` adopts explicitly, and an adopted box is never destroyed by `cos down`. + +### Sync modes + +`cos sync` defaults to **one-way (laptop → box)** — the safe direction for a dev loop. + +| Flag | Mode | Behavior | +|---|---|---| +| *(default)* | `one-way` | laptop wins; box changes NOT pulled back. **No bleed-back.** | +| `-2` | `two-way` | bidirectional; box-side writes (build output, deps) **flow back** to the local dir | +| `-M` | `mirror` | one-way **and deletes** box-side files absent locally | +| `-x ` | — | exclude paths (repeatable) | + +`.git` and the big regenerable dirs are excluded by default — build deps inside the box with `cos run 'npm ci'` rather than syncing them up. Only reach for `-2` when a box-side process genuinely produces files you need back locally, and never on the user's repo root without saying so first; prefer `offload -o` for pulling artifacts. The local dir must resolve under `$HOME` or `/tmp`. The first sync downloads its sync engine, so allow a minute before edits propagate. + +## Pattern C — networking + +```bash +cos run 'npm run dev &' && cos tunnel 3000 # private → http://127.0.0.1:3000 +cos expose 8080 # public HTTPS URL to share +cos unexpose # revoke +cos cluster up 3 # 3 boxes on one private net, name-addressable +cos cluster run -a 'uname -a' # fan a command across every member +cos vpn register my-laptop && cos vpn up # WireGuard L3 into the private network +``` + +- **`tunnel` is private, `expose` is public.** Prefer `tunnel` for dev loops. Use `expose` to share a preview with the team or to give a webhook a target. +- **An exposed service must bind `0.0.0.0:`, not loopback** — ingress arrives on the box's interface. A loopback-bound server passes every in-box check and still returns nothing through the URL. +- **The expose URL is the credential.** No token, no auth layer — anyone with the link reaches the service. `cos unexpose` when the demo is done. +- **`cos vpn up` and `cos shell` block and need a real terminal** — hand them to the user (`!cos vpn up`) rather than launching them as agent commands. + +For the DNS names cluster members resolve each other by, and the rest of the expose/tunnel/VPN detail → **`references/networking.md`**. + +## Scratch box and data disks + +```bash +cos shell # instant clean Linux, destroyed on exit — HAND THIS TO THE USER +cos disk create data --bucket my-bucket --endpoint https://s3.amazonaws.com \ + --access-key … --secret-key … [--region us-east-1] [--path-style] +cos disk attach data /mnt/data # needs the project box; the bucket stays in the user's account +cos disk detach data /mnt/data # unmount; bucket untouched +``` + +Disk data lives in the user's own S3 account and region. `--path-style` is needed for MinIO and R2. Prefer scoped, least-privilege keys, and prefer the CLI's interactive prompts over passing secrets as arguments — command lines are visible to other local users and land in shell history. Detaching only unmounts; it never deletes bucket data. Note that **a fork does not carry disk mounts** — re-attach on the clone. + +## Lifecycle and cost + +- Ephemeral boxes self-destroy. The project box carries a 30-minute idle auto-pause as a backstop, so a forgotten box parks itself instead of billing overnight. Raise it with `createos sandbox edit --auto-pause 4h` when a box is serving an exposed URL people will hit intermittently — otherwise the demo will look dead between visitors. +- Finish a live session with `cos pause` (keeping the warm state) or `cos down` (done for good). Don't leave a running box behind either way. +- **Concurrency is limited** — external keys have been observed to allow 2 boxes running at once, with a daily creation cap. This is observed behaviour rather than published policy, so budget `cluster` and `fanout` against it and expect excess jobs to queue rather than fail. +- If a shape is rejected, the error names the allowed list — pick from it, or run `createos sandbox shapes`. +- Pre-existing boxes the user already runs are **not** yours. `cos` only ever destroys boxes it created itself; a box adopted with `cos up -a` survives `cos down`. +- CreateOS Sandbox is in alpha with no SLA. When a limit or a number matters to a decision, check it live rather than quoting it from here. + +## References + +Load these when the task actually needs the depth — the summaries above are enough for most work. + +| File | Read it for | +|---|---| +| `references/offload-and-egress.md` | offload flag table, egress presets and how enforcement really behaves, fanout, upload excludes, heavy-build OOM/disk/bandwidth traps | +| `references/networking.md` | choosing between tunnel/expose/cluster/vpn, cluster DNS names, expose gotchas, WireGuard setup | +| `references/lifecycle-and-images.md` | pause/resume, auto-pause tuning, fork caveats, built-in rootfs vs custom templates, env vars, remote editor, self-terminating jobs, single-file transfer, measured timings | diff --git a/packages/codex-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md b/packages/codex-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md new file mode 100644 index 0000000..0f62834 --- /dev/null +++ b/packages/codex-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md @@ -0,0 +1,153 @@ +# Box lifecycle, images, and the primitives `cos` doesn't wrap + +Read this when a box needs to outlive one command: parking it cheaply, branching it, booting it pre-provisioned, or driving it with a `createos` verb `cos` has no shortcut for. + +## Contents + +- [pause and resume](#pause-and-resume) +- [Idle auto-pause](#idle-auto-pause) +- [fork — branch a warm box](#fork--branch-a-warm-box) +- [Images: built-in rootfs and custom templates](#images-built-in-rootfs-and-custom-templates) +- [Environment variables](#environment-variables) +- [Remote editor: Zed, Cursor, VS Code](#remote-editor-zed-cursor-vs-code) +- [Letting a job end its own box](#letting-a-job-end-its-own-box) +- [Single-file transfer](#single-file-transfer) +- [What the numbers actually are](#what-the-numbers-actually-are) + +## pause and resume + +```bash +cos pause # snapshot disk + memory, free the host, stop compute billing +cos resume # restore exactly — files, deps, and running processes +``` + +Pause is the answer to "I'm done for now but I don't want to rebuild this tomorrow." It snapshots the whole VM — disk, memory, and process state — so a box with a warm toolchain costs nothing while parked and comes back as it was. `cos down` destroys instead, which means the next session reinstalls everything. + +Through the CLI, both `cos pause` and `cos resume` land around 6–8 seconds end to end (measured on a 1 GiB box). The platform-side restore itself is much faster than that when the snapshot lands back on the same host; it slows down when the memory image has to be pulled to a different one. + +`cos pause` stops the file sync and any tunnels first, since a paused box serves no traffic and they would only spin on errors. Re-run `cos sync` / `cos tunnel` / `cos expose` after resuming. + +`cos run` against a paused box stops with a clear message rather than resuming silently — resuming restarts compute billing, so it stays an explicit choice. + +## Idle auto-pause + +`cos up` creates the project box with a 30-minute idle auto-pause as a cost backstop, so a box forgotten at the end of a session parks itself rather than billing overnight. + +The platform accepts anything from 60 seconds to 24 hours, and it can be changed on a live box: + +```bash +createos sandbox edit --auto-pause 4h # longer leash for a running demo +createos sandbox edit --auto-pause off # never auto-pause +``` + +Raising it is the right move when a box is serving an exposed URL that people will hit intermittently — the default 30 minutes will park a demo box between visitors and the URL will appear dead until something resumes it. + +## fork — branch a warm box + +```bash +cos fork # pauses briefly, clones the snapshot, resumes the original +``` + +The clone is a fully independent box with its own id, IP, and quota ledger. The original is untouched. This is how you try N variants from one prepared state without redoing setup N times. + +Two things to know: + +- A fork is **not** tracked as the project box, so `cos down` leaves it running. It *is* recorded in the statefile: `cos status` lists forks, `cos down` names the survivors, and `cos down -f` reaps them. Otherwise destroy it yourself with `createos sandbox rm -y `. +- **Mounted disks do not carry across a fork.** If the source box had an S3 disk attached, re-attach it on the clone. + +## Images: built-in rootfs and custom templates + +Built-ins are kept warm on the hosts, so they boot with no image pull: + +| Rootfs | Notes | +|---|---| +| `devbox:1` | Debian, batteries included, the default — has `sshd`, which `sync` and the editor path need | +| `ubuntu:26.04` | plain Ubuntu | +| `debian:13` | trixie | +| `alpine:3.20` | musl + busybox, far smaller; expect glibc-linked binaries and wheels not to work | + +`createos sandbox rootfs` lists what the account can actually boot. + +When the same install prelude runs at the start of every offload, move it into a template instead: + +```bash +cos template submit myimage -f Dockerfile # preflights, submits, streams build logs +cos template ls +cos up -r myimage # or: cos offload -r myimage . '' +``` + +The build service enforces constraints that would otherwise only surface as a rejection after upload, so `cos template submit` checks them locally first: + +- exactly one `FROM` — builds are single-stage +- the base image must be an operator-allowlisted one, written literally (no `ARG` substitution) +- **no `COPY` or `ADD`** — no build context is uploaded, so fetch what you need inside a `RUN` +- 64 KiB of Dockerfile source at most +- two builds run concurrently per account + +Resubmitting the same name builds a new version; new boxes pick up the latest one that reached `ready`. A custom image is slow on its first boot on each host, because that host has to fetch it, and fast on every boot after. + +## Environment variables + +Values a command needs (API keys, tokens, config) have to be declared when the box is created: + +```bash +createos sandbox create --shape s-1vcpu-1gb --env OPENAI_API_KEY=… --env STAGE=dev +``` + +Per-exec overrides only work for keys that were declared at create time — passing `--env NEW_KEY=…` to `exec` for an undeclared key is rejected. For a one-off value, inline it instead: `bash -c 'TOKEN=… ./run.sh'`. + +Limits are 64 entries, 4 KiB per value, 64 KiB total. + +Do not put the user's real credentials in a box unless the task genuinely requires it, and never echo a secret into a command line that ends up in a transcript or in `ps`. + +## Remote editor: Zed, Cursor, VS Code + +`cos` has no wrapper for this; the CLI verb is direct: + +```bash +createos sandbox editor --via tunnel --editor zed --yes +createos sandbox editor --remove # tear down the ~/.ssh/config entry +``` + +It generates a per-box SSH key, starts `sshd` in the box, writes a `~/.ssh/config` block so plain `ssh ` works, and launches the editor pointed at the remote. It needs a rootfs with `sshd` (`devbox:1` has it) and a shape with more than 2 GiB of RAM — language servers OOM on a 1 GiB box. + +This is the right tool when the user wants to *work in* the sandbox rather than have an agent drive it. It launches a GUI editor, so it belongs to the user, not to an agent command. + +## Letting a job end its own box + +A long batch job can end its own box when it finishes, which is the cleanest defence against a box left billing because an agent run died halfway: + +```bash +# from inside the box, no credentials needed — loopback only +curl -sX POST 'http://127.0.0.1:1029/self/delete?reason=batch-done' +curl -sX POST 'http://127.0.0.1:1029/self/pause?reason=idle' + +# or via the FIFO +echo retire > /run/self # delete +echo park > /run/self # pause +``` + +Append this to the end of a long unattended command and the box cleans itself up whether or not anything is still watching. + +## Single-file transfer + +`offload -o` tars a whole directory back, which is usually what you want. For one file, the CLI is more direct: + +```bash +createos sandbox push ./local.py /work/local.py +createos sandbox pull /work/result.csv ./result.csv +createos sandbox pull /work/result.csv - | head -5 # '-' streams to stdout +``` + +Remote paths must be absolute; parent directories are created automatically; 500 MiB per file. + +## What the numbers actually are + +Useful for setting expectations, and for not overpromising to the user: + +- **Create to first command runs: roughly 200 ms** (median; the guest kernel itself boots in tens of milliseconds, but the round trip through the control plane dominates). +- **Pause and resume: around 6–8 seconds each, end to end through the CLI** (measured on a 1 GiB box; the platform-side operations are faster, the CLI polls for the state transition). Resume is slower when the snapshot has to move to a different host than it was taken on. **Fork: around a second** for the snapshot copy, plus the pause and resume around it. +- **Concurrency:** external API keys have been observed to allow 2 boxes running at once, with a daily creation cap. Neither number is published policy — treat them as observed behaviour, budget `cluster` and `fanout` against them, and expect excess jobs to queue rather than fail. +- **Bandwidth:** 5 GiB of box-initiated egress per box by default, topped up additively. + +CreateOS Sandbox is in alpha and carries no SLA. Behaviour and limits can change — when a number matters to a decision, check it live rather than quoting this file. diff --git a/packages/codex-plugin/skills/using-createos-sandbox/references/networking.md b/packages/codex-plugin/skills/using-createos-sandbox/references/networking.md new file mode 100644 index 0000000..a56a726 --- /dev/null +++ b/packages/codex-plugin/skills/using-createos-sandbox/references/networking.md @@ -0,0 +1,82 @@ +# Reaching the box, and wiring boxes together + +Read this when something needs to talk to a sandbox: a browser, a teammate, another box, or the user's laptop. + +## Contents + +- [Choosing between tunnel, expose, cluster, and VPN](#choosing-between-tunnel-expose-cluster-and-vpn) +- [tunnel — private, to 127.0.0.1](#tunnel--private-to-127001) +- [expose — public HTTPS URL](#expose--public-https-url) +- [cluster — N boxes on one private network](#cluster--n-boxes-on-one-private-network) +- [vpn — the laptop joins the private network](#vpn--the-laptop-joins-the-private-network) + +## Choosing between tunnel, expose, cluster, and VPN + +| Need | Reach for | +|---|---| +| Hit a box-side dev server from this machine's browser | `tunnel` | +| Give a teammate a link, or point a webhook at the box | `expose` | +| Boxes that need to talk to each other (replication, p2p, load generator → target) | `cluster` | +| The laptop needs the whole private network, not one port | `vpn` | + +`tunnel` and `expose` operate on the project box, so `cos up` first. `cluster` manages its own set of boxes. `cos down` tears down tunnels, the public URL, and the cluster along with the project box. + +## tunnel — private, to 127.0.0.1 + +```bash +cos run 'npm run dev &' # start the server in the box +cos tunnel 3000 # → http://127.0.0.1:3000 (local port defaults to the remote one) +cos tunnel 5432 15432 # box:5432 → 127.0.0.1:15432 +``` + +Private to this machine — nothing is published. It runs in the background and is tracked in the statefile, so `cos status` lists it and `cos down` stops it. A service reached this way can bind loopback inside the box; the tunnel terminates in the box's network namespace. + +Prefer this for dev loops. It needs no SSH key and no public exposure. + +## expose — public HTTPS URL + +```bash +cos expose 8080 # prints https://-8080. +cos unexpose # revoke +``` + +Things worth knowing before handing the link to anyone: + +- **The service must bind `0.0.0.0:`, not `127.0.0.1`.** Ingress arrives on the box's network interface, not loopback. A server on loopback will pass every in-box health check and still return nothing through the URL. `cos expose` probes the URL after enabling ingress and tells you which case you are in. +- **The URL is the credential.** There is no token and no auth layer — the unguessable id in the hostname is the entire access control. Anyone who has the link can reach the service, and a link pasted into a public channel is a public service. Treat it accordingly, and `cos unexpose` when the demo is over. +- **It lives as long as the box does.** The URL is stable for the box's lifetime, and dies with it. It does not survive a destroy, and a paused box serves nothing. +- **HTTP-aware, not raw TCP.** WebSockets and SSE work through it. The `Host` header is rewritten to `localhost:` on the way in, which means Django's `ALLOWED_HOSTS` and Rails' host authorization pass without configuration. +- **TLS on the wildcard domain may not be provisioned.** If `https://` fails to connect, try the same URL over `http://` before assuming the service is broken. + +Use `expose` for sharing a preview, demoing to the team, or giving an external service a webhook target. Use `tunnel` for everything else. + +## cluster — N boxes on one private network + +```bash +cos cluster up 3 # cos-cl--1..3, all on cos-net- +cos cluster run 1 'ip -4 addr' # exec on member 1 (index or name) +cos cluster run -a 'uname -a' # fan a command across every member +cos cluster ls # members + private IPs +cos cluster down # destroy members + delete the network +``` + +Members resolve each other by name over the shared overlay — no IP wrangling. **The name must be fully qualified**; the bare short name is not in the guest's search path and returns NXDOMAIN (verified — this is the single most common way to conclude, wrongly, that cluster networking is broken): + +- `.fc.local` — scoped to networks the caller belongs to +- `..fc.local` — when a name is ambiguous across networks +- `.fc.local` — always unambiguous + +So from member 1, `curl http://cos-cl--2.fc.local:8080` works, while `curl http://cos-cl--2:8080` does not resolve. Names in a network the caller does not belong to return NXDOMAIN too — cross-network traffic is blocked at the host, not merely unrouted. + +Good for distributed-system tests, database primary/replica setups, gossip and p2p meshes, and load generators pointed at a target box. Every member counts against the concurrent-box limit, so keep N small — `cos` refuses N > 8 outright. + +## vpn — the laptop joins the private network + +```bash +cos vpn register my-laptop # one-time per machine, no sudo +cos vpn up # connect; needs wg-quick + sudo; blocks until Ctrl-C +``` + +This is real kernel WireGuard, and it is a whole-network L3 connection rather than the single forwarded port `tunnel` gives you. The private key is generated locally and never sent anywhere. + +Two operational notes: it needs `wg-quick` installed and sudo for route changes, and **it blocks until interrupted** — hand `cos vpn up` to the user to run in their own terminal rather than launching it as an agent command. It refuses to start if its routes would collide with an existing VPN or LAN route (Tailscale on the same CGNAT range is the common case) instead of hijacking traffic. diff --git a/packages/codex-plugin/skills/using-createos-sandbox/references/offload-and-egress.md b/packages/codex-plugin/skills/using-createos-sandbox/references/offload-and-egress.md new file mode 100644 index 0000000..fddcdfb --- /dev/null +++ b/packages/codex-plugin/skills/using-createos-sandbox/references/offload-and-egress.md @@ -0,0 +1,86 @@ +# Offload, fanout, and the egress firewall + +Read this when an offload needs tuning: restricting what a build can reach, sizing a box for a heavy compile, controlling what gets uploaded, or fanning work across boxes. + +## Contents + +- [Offload flags](#offload-flags) +- [What gets uploaded](#what-gets-uploaded) +- [Egress: how the firewall actually behaves](#egress-how-the-firewall-actually-behaves) +- [Egress presets](#egress-presets) +- [Fanout](#fanout) +- [Heavy builds: OOM, disk, and bandwidth](#heavy-builds-oom-disk-and-bandwidth) + +## Offload flags + +Flags come **before** the ` ` positionals — `cos` parses with `getopts`, which stops at the first positional. + +| Flag | Effect | +|---|---| +| `-s ` | box size (default `s-1vcpu-1gb`); list with `createos sandbox shapes` | +| `-r ` | base image or custom template (default `devbox:1`) | +| `-o ` | tar this path out of `/work` back into the local dir after the run | +| `-w ` | try to add a swapfile (best-effort — see below) | +| `-K` | keep the box if the command exits non-zero, so the cache survives for a retry | +| `-e ` | allow one outbound destination (repeatable) | +| `-p ` | apply an egress preset (repeatable, composes with `-e`) | +| `-E` | explicitly unrestricted egress | +| `-x ` | extra upload exclude (repeatable) | + +The box is destroyed on success, failure, or interrupt. Two things override that: `-K` on a failing command, and an infra/stream error — both keep the box so the build cache survives, and `cos` prints the reconnect and destroy commands. + +Long, quiet builds survive a dropped connection. The command runs detached inside the box with a heartbeat watcher; if the exec stream dies the build keeps running and the watcher re-attaches. The real exit code is preserved either way. + +## What gets uploaded + +The local tree goes to `/work` in the box, one-way. Box-side changes never touch the local directory unless you ask for them with `-o `. + +Excluded from the upload by default: `.git`, `target`, `node_modules`, `__pycache__`, `.venv`, `.mypy_cache`, `.pytest_cache`, `.gradle`, `.cargo/registry`, `dist`, `build`, `.next`, `.turbo`, and large media (`*.gif`, `*.mp4`, `*.mov`, `*.zst`). Add more with `-x `. + +Dependencies are meant to be built *inside* the box, not shipped into it — that is why `node_modules` and friends are excluded rather than uploaded. + +## Egress: how the firewall actually behaves + +**The default is unrestricted.** A fresh box can reach any host on the internet. `cos` prints a one-line UNRESTRICTED notice so this is never silent. Restricting is opt-in: `-p `, `-e `, or both. + +The rule grammar is allow-list only — there is no deny token, so every destination a job needs must be enumerated. Rules can be `host`, `host:port`, `*.host`, `ip`, `ip:port`, `cidr`, `cidr:port`, or `*` (which means allow everything). + +Enforcement is not uniform, and the difference matters when the threat model is exfiltration: + +- **IP and CIDR rules take effect immediately** and cannot be bypassed from inside the box. +- **Domain rules take roughly 30 seconds to apply** after being set, and they are a strong control for HTTPS traffic but a weak one for cleartext HTTP. + +So: a domain allow-list is the right tool for "this build should only reach pypi and crates.io." For an adversarial workload where blocking exfiltration is the actual goal, prefer IP/CIDR rules, and expect the 30-second window after any domain-rule change. + +DNS keeps resolving even for blocked destinations — a blocked connection fails as a connection error (for example `curl` exit 35), not as a name-resolution failure. Debugging a restricted build by checking whether DNS works will mislead you. + +## Egress presets + +| Preset | Opens | +|---|---| +| `python-uv` | `astral.sh`, `releases.astral.sh`, `pypi.org`, `files.pythonhosted.org` | +| `rust-cargo` | `crates.io`, `static.crates.io`, `index.crates.io`, `static.rust-lang.org`, `cdn.pyke.io` | +| `npm` | `registry.npmjs.org` | +| `github` | `github.com`, `objects.githubusercontent.com`, `raw.githubusercontent.com`, `codeload.github.com` | + +`cdn.pyke.io` is the non-obvious one: `ort-sys` (ONNX Runtime, pulled in by a lot of ML crates) downloads prebuilt binaries from it, so a `cargo build` that looks pure-Rust fails without it. It is already in `rust-cargo`. + +Presets compose. A Python project with a Rust extension and a git dependency wants `-p python-uv -p rust-cargo -p github`. + +## Fanout + +`cos fanout [-j N] [flags] [cmd2] …` stages `` once and runs each command in its **own** throwaway box, concurrently, then reports per-job exit codes and log paths and destroys every box. + +The jobs share no network — that is the difference from `cluster`, where boxes are wired together on purpose. Fanout is for a test matrix, a config sweep, or a batch where isolation between jobs is the point. + +`-j` defaults to 2 because that matches the concurrent-box limit observed on external API keys. Raising it past what the account allows does not fail — the extra jobs just queue, so a 3-way fanout with `-j 3` silently serializes into 2 + 1. One very long build is still better served by a single `offload`. + +## Heavy builds: OOM, disk, and bandwidth + +**Shape rejection.** Picking a shape the account cannot use fails fast with a `not allowed … Allowed: [...]` line; pick from that list, or run `createos sandbox shapes`. (The rejection is real and reproducible on external keys; the exact policy behind it is not documented, so treat the allowed list as authoritative rather than guessing.) + +**Swap is best-effort.** `-w ` tries to add a swapfile, but `devbox:1` cannot currently `swapon` — it stays at 0 MB, `cos` warns, and continues. If a compiled-extension build (pyo3/maturin, torch) is OOMing, the fix is a bigger shape or less work per run: build the extension separately, or install only the extra/group you actually need. + +**Disk fills fast.** `pip install --all-extras` or an unconstrained `uv sync` can pull CUDA and torch wheels measured in gigabytes and hit `No space left on device` on a small box. Install only what the job needs; `--disk-mib` at create time raises the ceiling if you control it. + +**Bandwidth is quota'd.** Every box starts with a fixed egress allowance (5 GiB by default) covering traffic the box initiates. A big model download, a `docker pull`, or a large dataset fetch can exhaust it, after which the box is flagged capped and outbound traffic stops. The quota is topped up additively — `createos sandbox edit ` has an interactive top-up — and clears a few seconds after the top-up lands. The control channels that carry exec, file transfer, and tunnels are exempt, so a blown quota degrades the workload without cutting off access to the box. From 0d02effa3974b1ec3d18bfe488498b5a3b1a81d8 Mon Sep 17 00:00:00 2001 From: Bhautik Date: Wed, 5 Aug 2026 16:31:56 +0530 Subject: [PATCH 2/6] fix(codex): align naming, rewrite skill for createos CLI - Rename plugin to createos-codex (consistent with @createos/pi, @createos/opencode) - Rewrite SKILL.md to teach createos CLI commands instead of cos wrapper - Add .claude-plugin/plugin.json with skills reference - Update marketplace manifest --- .claude-plugin/marketplace.json | 7 +- .../codex-plugin/.claude-plugin/plugin.json | 17 ++ .../skills/using-createos-sandbox/SKILL.md | 217 ++++++------------ 3 files changed, 99 insertions(+), 142 deletions(-) create mode 100644 packages/codex-plugin/.claude-plugin/plugin.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index f621fcb..73d961f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,12 +1,17 @@ { "name": "createos", - "description": "NodeOps CreateOS plugins for Claude Code — disposable sandbox compute and live dev sessions.", + "description": "NodeOps CreateOS plugins — disposable sandbox compute for Claude Code, Codex, and more.", "owner": { "name": "NodeOps", "url": "https://createos.sh" }, "plugins": [ { "name": "@createos/claude-code", "source": "./packages/claude-code-plugin", "description": "Run ad-hoc/heavy/untrusted code in disposable CreateOS Sandboxes; offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public expose, network clusters, S3 disks, WireGuard VPN, fork, pause/resume, and custom images." + }, + { + "name": "createos-codex", + "source": "./packages/codex-plugin", + "description": "Codex plugin for disposable CreateOS Sandboxes — skill + createos CLI for sandbox lifecycle, networking, disks, and VPN." } ] } diff --git a/packages/codex-plugin/.claude-plugin/plugin.json b/packages/codex-plugin/.claude-plugin/plugin.json new file mode 100644 index 0000000..88a51d8 --- /dev/null +++ b/packages/codex-plugin/.claude-plugin/plugin.json @@ -0,0 +1,17 @@ +{ + "name": "createos-codex", + "displayName": "CreateOS Sandbox", + "version": "0.1.0", + "description": "Run code off your machine in disposable CreateOS Sandboxes via the createos CLI.", + "author": { "name": "NodeOps", "url": "https://createos.sh" }, + "homepage": "https://createos.sh", + "keywords": ["sandbox", "createos", "remote-exec", "isolation"], + "skills": "./skills/", + "interface": { + "displayName": "CreateOS Sandbox", + "shortDescription": "Offload code to disposable remote sandboxes", + "developerName": "NodeOps", + "category": "Developer Tools", + "websiteURL": "https://createos.sh" + } +} diff --git a/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md b/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md index a7d8083..f2a3918 100644 --- a/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md +++ b/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md @@ -1,187 +1,122 @@ --- name: using-createos-sandbox -description: Use when you need to run code OFF the user's machine — heavy/long builds or test suites, untrusted or unknown code, a parallel test/config matrix across many boxes, an instant clean Linux to try a tool, a live dev-server/watcher Claude edits against, reaching a box-side service from localhost (port tunnel) or sharing it on the public web (HTTPS preview URL), a multi-machine cluster on one private network, a WireGuard VPN into that network, or mounting an S3 bucket of data. Offloads to ephemeral CreateOS Sandboxes via the `cos` helper (stage → exec → pull → auto-destroy), plus fanout, a scratch shell, and an opt-in reusable box with sync, tunnel, expose, cluster, disk, vpn, pause/resume, custom images, and snapshot/fork. +description: Use when the user wants to run code in a remote sandbox, offload heavy builds or tests, run untrusted code safely, create disposable Linux boxes, set up multi-node clusters, or use createos sandbox commands. Triggers on keywords like createos, sandbox, offload, remote, isolated, disposable box. --- # Using CreateOS Sandbox as remote compute -A CreateOS Sandbox is an isolated Linux box that goes from create to running your first command in roughly 200 ms. Use it as throwaway compute instead of running risky or heavy work on the user's laptop. +A CreateOS Sandbox is an isolated Linux VM that goes from create to running your first command in roughly 200 ms. Use it as throwaway compute instead of running risky or heavy work on the user's laptop. -## Running the driver +## The createos CLI -Everything goes through `cos`. **A SessionStart hook prints its absolute path into your context at the start of the session — use that literal path.** - -Do not write `${CLAUDE_PLUGIN_ROOT}` into a Bash command. That variable is set when slash commands are loaded but is **unset in the Bash tool's environment**, so the path collapses to `/scripts/cos` and dies with exit 127. - -If you cannot locate or run `cos`, **stop and say so.** Do not fall back to composing the job out of raw `createos sandbox create/push/exec` calls. That path looks equivalent and is not: it silently drops egress restriction, the keepalive that survives a dropped stream on a long build, guaranteed auto-destroy, and the auth preflight — so a "successful" run can leave an unrestricted box billing with no isolation ever applied. A missing driver is a hard stop, not a reason to improvise. - -`cos install` symlinks it into `~/.local/bin` if the user wants it on PATH permanently. It wraps the authed `createos` CLI and needs `jq`, `tar`, `perl`, and `curl`; if the `createos` CLI is missing it auto-installs it from the official script (opt out with `COS_NO_AUTOINSTALL=1`). - -## Setup — check this once per session, before the first offload +All sandbox operations use the `createos` CLI. Check it's available: ```bash -cos auth +createos version ``` -Healthy output names one of three credential sources: `CREATEOS_API_KEY`, a browser OAuth session, or an API token file. Anything else means not signed in. - -**You cannot fix that yourself.** `createos login` is an interactive TTY prompt that opens a browser, and an agent shell has no TTY. Do not try to run it and do not work around it with `--token`. Relay the two options to the user: - -1. **Browser (recommended)** — they run `createos login` in their own terminal and pick "Sign in with browser". -2. **API key** — they `export CREATEOS_API_KEY=` (from ) in the shell that launched Claude Code. +If not installed, install it: +```bash +curl -sfL https://raw.githubusercontent.com/NodeOps-app/createos-cli/main/install.sh | sh +``` -**Never ask the user to paste an API key into the conversation** — it lands in the transcript. Export or browser, nothing else. +Check auth: +```bash +createos sandbox shapes +``` -Every `cos` command except `install` and `auth` runs this check first, so an unauthenticated box never gets tarballed and uploaded before failing. +If that fails, the user needs to run `createos login` in their own terminal (browser OAuth). **Never ask the user to paste an API key into the conversation.** -## When to reach for it +## When to use it -| Situation | Why offload | +| Situation | Why | |---|---| -| **Untrusted / unknown code** — a snippet, a fresh npm/pip package, scraped code, a PoC exploit | Isolation. The blast radius is one disposable box, not the laptop. | -| **Heavy build or test suite** — big `make`, full test run, compile, benchmark | Keeps the laptop free; runs on a box sized for it. | -| **Parallel/matrix work** — same job across N configs, test shards, batch | `fanout` — each command in its own throwaway box, concurrently, results collected. | -| **Quick scratch Linux** — try a CLI/tool/snippet on a clean box | `shell` — instant keyless box, destroyed on exit (interactive; the user runs it). | -| **Clean-room repro** — "works on my machine" bugs, dependency conflicts | Fresh rootfs every time, no host state. | -| **Live dev loop** — dev server / test watcher / REPL that reacts to edits | Project box + `sync`; Claude edits locally, the box reacts. | -| **Reach a box-side service** — dev server, DB, API | `tunnel` (private, to `127.0.0.1`) or `expose` (public HTTPS link to share). | -| **Multi-machine** — distributed system, DB replication, p2p mesh, load test | `cluster up N` — boxes share one private net, reach each other by name. | -| **Same setup, many variants** — try N branches from one prepared box | `fork` the project box into independent clones. | -| **Repeated identical setup** — every offload starts with the same install prelude | `template` — bake the toolchain into an image once. | -| **Done for now, back tomorrow** — warm box you don't want to rebuild | `pause` — snapshot at zero compute cost, `resume` restores it exactly. | -| **Big data / weights / shared cache** | `disk` — BYO S3 bucket mounted into the box, survives box death. | - -Do NOT offload trivial commands, anything needing the user's local secrets/SSH/cloud creds, or work that must touch real local filesystem state. - -## Picking the verb — decide this before typing anything +| Untrusted/unknown code | Isolation — blast radius is one disposable box | +| Heavy build or test suite | Keeps the laptop free | +| Quick scratch Linux | Instant clean box, destroyed when done | +| Clean-room repro | Fresh rootfs every time | +| Multi-machine setup | Private network clusters | -Almost every task is one of two shapes, and picking the wrong one wastes a lot of motion: - -- **"Run this and tell me the result"** — a test suite, a build, a script, anything with an end. → **`cos offload `.** One command. It creates the box, ships the directory, runs, and destroys the box. Nothing to clean up. -- **"Keep a box around while I work"** — a dev server you'll hit repeatedly, a watcher reacting to edits, a session spanning many commands. → **`cos up`**, then `run`/`sync`, then `pause` or `down`. - -If you find yourself doing any of the following, you have picked the wrong shape and should stop and use `offload` instead: - -- running `cos up` for a task that has a clear finish line -- tarring, base64-encoding, or `push`-ing files into the box by hand — **`offload` stages the directory for you**, with sensible excludes, in the same command -- reaching for `cos status` to decide what to do first — for one-shot work there is nothing to check, just offload - -`cos run` takes the command as one plain string. There is no `--` separator: `cos run 'npm ci && npm test'`. - -## Pattern A — one-shot offload (the default, and the safe one) - -Stage a directory, run, optionally pull artifacts back, **always auto-destroys**. Flags come **before** the ` ` positionals. +## Core commands +### Create a sandbox ```bash -# run a test suite off-machine (the preset opens the registries it needs) -cos offload -p python-uv . 'uv sync --frozen --group dev && uv run pytest -q' - -# Python + Rust, compose presets, exclude build dirs, pull artifacts back -cos offload -p python-uv -p rust-cargo -x target -o dist . 'uv sync --frozen && uv run pytest -q' - -# trusted heavy build, explicitly unrestricted egress -cos offload -E -s s-2vcpu-2gb . 'cargo build --release' - -# untrusted script, outbound locked to exactly what it needs -cos offload -e pypi.org -e files.pythonhosted.org ./suspect 'python3 main.py' +createos sandbox create --shape s-2vcpu-2gb --ingress ``` -Two things about this that are easy to get wrong: - -- **Egress is unrestricted by default.** A fresh box can reach anything; `cos` prints a one-line notice. Restricting is opt-in with `-p ` or `-e `. So "run this untrusted thing in a sandbox" is only half done until you pass one of those. -- **Uploads are one-way.** Box-side changes never touch the local tree unless you ask with `-o `. `.git`, `node_modules`, `target`, `.venv` and friends are excluded from the upload by default — dependencies are meant to be built *inside* the box. - -Long, quiet builds survive a dropped connection: the command runs detached with a heartbeat watcher that re-attaches if the stream dies. The real exit code is preserved. - -For the full flag table, the egress presets, the enforcement caveats, fanout, and the OOM/disk/bandwidth traps on heavy builds → **`references/offload-and-egress.md`**. - -### Fanout — same input, many boxes, in parallel - +### Run a command inside a sandbox ```bash -cos fanout -j 2 -p python-uv . 'pytest -q tests/unit' 'pytest -q tests/integration' 'ruff check' +createos sandbox exec -- sh -c 'hostname && uname -a' ``` -Each job gets its own box with no shared network — that is what distinguishes it from `cluster`. `-j` defaults to 2 to match the concurrency external keys have been observed to allow; going higher just queues the extra jobs rather than failing. - -## Pattern B — reusable project box (opt-in) - -For repeated runs against a warm box, or a dev server Claude edits against. One box per git root, tracked in a statefile. - +### List sandboxes ```bash -cos up -s s-2vcpu-2gb # create/reuse this project's box -cos run 'npm ci' # warm it — deps persist across runs -cos sync ~/app /work # one-way by default (laptop → box), background -cos run 'npm run dev &' # start a watcher; it sees synced edits -cos status # box + sync + tunnels + forks -cos pause # park it at zero compute cost -cos resume # bring it back exactly as it was -cos down # stop sync + destroy the box +createos sandbox list ``` -**`up` is for a box you intend to reuse and then tear down.** A bare "run this in a sandbox" is *not* Pattern B — use `cos offload` (one-shot, auto-destroys) or `cos shell`. Reaching for `up` to satisfy "create a sandbox" makes the box outlive the task, and a later `cos down` destroys it along with anything else sharing that statefile. - -**Ending a session: prefer `pause` over `down`** when the box has a warm toolchain the user will want again. `down` destroys and the next session reinstalls everything; `pause` snapshots disk *and* memory, stops compute billing, and brings everything back on `resume` — measured end-to-end at around 6–8 s each way through the CLI. Use `down` when the work is genuinely finished. - -If a box under this project's name is running but the statefile is gone (another checkout, another agent, created by hand), `up` **refuses** rather than adopting it — adopting silently would let a later `cos down` destroy a box this project never created. `cos up -a` adopts explicitly, and an adopted box is never destroyed by `cos down`. - -### Sync modes - -`cos sync` defaults to **one-way (laptop → box)** — the safe direction for a dev loop. +### Get sandbox info +```bash +createos sandbox get +``` -| Flag | Mode | Behavior | -|---|---|---| -| *(default)* | `one-way` | laptop wins; box changes NOT pulled back. **No bleed-back.** | -| `-2` | `two-way` | bidirectional; box-side writes (build output, deps) **flow back** to the local dir | -| `-M` | `mirror` | one-way **and deletes** box-side files absent locally | -| `-x ` | — | exclude paths (repeatable) | +### Destroy a sandbox +```bash +createos sandbox rm --yes +``` -`.git` and the big regenerable dirs are excluded by default — build deps inside the box with `cos run 'npm ci'` rather than syncing them up. Only reach for `-2` when a box-side process genuinely produces files you need back locally, and never on the user's repo root without saying so first; prefer `offload -o` for pulling artifacts. The local dir must resolve under `$HOME` or `/tmp`. The first sync downloads its sync engine, so allow a minute before edits propagate. +### Pause / Resume +```bash +createos sandbox pause +createos sandbox resume +``` -## Pattern C — networking +## File transfer +### Push a file to sandbox ```bash -cos run 'npm run dev &' && cos tunnel 3000 # private → http://127.0.0.1:3000 -cos expose 8080 # public HTTPS URL to share -cos unexpose # revoke -cos cluster up 3 # 3 boxes on one private net, name-addressable -cos cluster run -a 'uname -a' # fan a command across every member -cos vpn register my-laptop && cos vpn up # WireGuard L3 into the private network +echo 'file content' | base64 | createos sandbox exec -- sh -c "base64 -d > /path/to/file" ``` -- **`tunnel` is private, `expose` is public.** Prefer `tunnel` for dev loops. Use `expose` to share a preview with the team or to give a webhook a target. -- **An exposed service must bind `0.0.0.0:`, not loopback** — ingress arrives on the box's interface. A loopback-bound server passes every in-box check and still returns nothing through the URL. -- **The expose URL is the credential.** No token, no auth layer — anyone with the link reaches the service. `cos unexpose` when the demo is done. -- **`cos vpn up` and `cos shell` block and need a real terminal** — hand them to the user (`!cos vpn up`) rather than launching them as agent commands. +### Pull a file from sandbox +```bash +createos sandbox pull /path/to/file - +``` -For the DNS names cluster members resolve each other by, and the rest of the expose/tunnel/VPN detail → **`references/networking.md`**. +## Networking -## Scratch box and data disks +### Get a public URL for a port +The sandbox's ingress URL template is in `createos sandbox get `. Replace `` with the actual port number. +### Port tunnel to localhost ```bash -cos shell # instant clean Linux, destroyed on exit — HAND THIS TO THE USER -cos disk create data --bucket my-bucket --endpoint https://s3.amazonaws.com \ - --access-key … --secret-key … [--region us-east-1] [--path-style] -cos disk attach data /mnt/data # needs the project box; the bucket stays in the user's account -cos disk detach data /mnt/data # unmount; bucket untouched +createos sandbox tunnel --remote --local ``` -Disk data lives in the user's own S3 account and region. `--path-style` is needed for MinIO and R2. Prefer scoped, least-privilege keys, and prefer the CLI's interactive prompts over passing secrets as arguments — command lines are visible to other local users and land in shell history. Detaching only unmounts; it never deletes bucket data. Note that **a fork does not carry disk mounts** — re-attach on the clone. +### Private networks (multi-node) +```bash +createos sandbox network create +createos sandbox network attach +createos sandbox network show +``` -## Lifecycle and cost +## Persistent storage (S3 disks) +```bash +createos sandbox disk create --bucket --endpoint --access-key --secret-key +createos sandbox disk attach /mnt/data +``` -- Ephemeral boxes self-destroy. The project box carries a 30-minute idle auto-pause as a backstop, so a forgotten box parks itself instead of billing overnight. Raise it with `createos sandbox edit --auto-pause 4h` when a box is serving an exposed URL people will hit intermittently — otherwise the demo will look dead between visitors. -- Finish a live session with `cos pause` (keeping the warm state) or `cos down` (done for good). Don't leave a running box behind either way. -- **Concurrency is limited** — external keys have been observed to allow 2 boxes running at once, with a daily creation cap. This is observed behaviour rather than published policy, so budget `cluster` and `fanout` against it and expect excess jobs to queue rather than fail. -- If a shape is rejected, the error names the allowed list — pick from it, or run `createos sandbox shapes`. -- Pre-existing boxes the user already runs are **not** yours. `cos` only ever destroys boxes it created itself; a box adopted with `cos up -a` survives `cos down`. -- CreateOS Sandbox is in alpha with no SLA. When a limit or a number matters to a decision, check it live rather than quoting it from here. +## Device VPN +```bash +createos sandbox devices register +# User runs in separate terminal (requires sudo): +createos sb vpn up +``` -## References +## Workflow pattern -Load these when the task actually needs the depth — the summaries above are enough for most work. +1. Create sandbox: `createos sandbox create --shape s-2vcpu-2gb --ingress` +2. Note the sandbox ID from the output +3. Run commands: `createos sandbox exec -- sh -c ''` +4. When done: `createos sandbox rm --yes` -| File | Read it for | -|---|---| -| `references/offload-and-egress.md` | offload flag table, egress presets and how enforcement really behaves, fanout, upload excludes, heavy-build OOM/disk/bandwidth traps | -| `references/networking.md` | choosing between tunnel/expose/cluster/vpn, cluster DNS names, expose gotchas, WireGuard setup | -| `references/lifecycle-and-images.md` | pause/resume, auto-pause tuning, fork caveats, built-in rootfs vs custom templates, env vars, remote editor, self-terminating jobs, single-file transfer, measured timings | +IMPORTANT: Always use `createos sandbox exec -- sh -c ''` to run commands inside the sandbox. Do NOT use the built-in bash/shell tool for sandbox work — that runs on the user's local machine. From 80fcdedec9cfec9517032595273ec476c4bbffe0 Mon Sep 17 00:00:00 2001 From: Bhautik Date: Wed, 5 Aug 2026 16:33:56 +0530 Subject: [PATCH 3/6] docs: align naming and update READMEs for createos-codex --- README.md | 12 +++-- packages/codex-plugin/README.md | 77 +++++++++++++++------------------ 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 4ea17c0..8b29ad6 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,13 @@ pi --createos **Codex:** ```bash -# Copy plugin into your project -cp -r packages/codex-plugin .codex/plugins/createos-sandbox -# Then launch codex — the skill and cos driver are available +# 1. Add the marketplace +codex plugin marketplace add NodeOps-app/createos-claude-plugins + +# 2. Install the plugin +codex plugin add createos-codex@createos + +# 3. Launch codex — the skill teaches createos CLI usage codex ``` @@ -74,7 +78,7 @@ The `createos` CLI **auto-installs** on first use. Sign in once with `createos l |---|---| | [**claude-code-plugin**](./packages/claude-code-plugin) | Hooks-based Claude Code plugin — offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public HTTPS expose, private-network clusters, BYO-S3 disk mounts, WireGuard VPN, and snapshot/fork — all driving the authed `createos` CLI. | | [**pi-extension**](./packages/pi-extension) | Pi coding agent extension that transparently routes all built-in commands (bash, read, write, edit, ls, find, grep) to a remote CreateOS Sandbox, plus 40 additional tools for sandbox lifecycle, configuration, port tunnels, file sync, private networks, persistent disks, custom image templates, remote editors, and device VPN — 47 tools total. | -| [**codex-plugin**](./packages/codex-plugin) | Codex plugin — same skill + `cos` driver as Claude Code, adapted for Codex's manifest and hook format. | +| [**createos-codex**](./packages/codex-plugin) | Codex plugin — skill that teaches the `createos` CLI for sandbox lifecycle, networking, disks, and VPN. | | [**@createos/opencode**](./packages/opencode-plugin) | OpenCode plugin with 33 sandbox tools (`sandbox_exec`, `sandbox_push`, `sandbox_pull`, networks, disks, VPN, sync) and system prompt injection for sandbox-first workflows. | ## Claude Code — commands at a glance diff --git a/packages/codex-plugin/README.md b/packages/codex-plugin/README.md index e743325..144ca5e 100644 --- a/packages/codex-plugin/README.md +++ b/packages/codex-plugin/README.md @@ -1,20 +1,17 @@ -# createos-sandbox (Codex plugin) +# @createos/codex Codex plugin that offloads code to disposable [CreateOS](https://createos.sh) Sandboxes. -Gives Codex a skill + session hook that teach the agent how to use the `cos` driver -for one-shot offloads, reusable dev boxes, parallel fanout, tunnels, clusters, and more. +Gives Codex a skill that teaches the agent how to use the `createos` CLI +for sandbox lifecycle, networking, persistent disks, VPN, and more. ## Install -Copy or symlink the plugin into your project's `.codex/` directory: - ```bash -# From this repo -cp -r packages/codex-plugin /path/to/your-project/.codex/plugins/createos-sandbox +# 1. Add the marketplace +codex plugin marketplace add NodeOps-app/createos-claude-plugins -# Or symlink for development -ln -s /path/to/createos-claude-plugins/packages/codex-plugin \ - /path/to/your-project/.codex/plugins/createos-sandbox +# 2. Install the plugin +codex plugin add createos-codex@createos ``` ## Prerequisites @@ -29,58 +26,56 @@ ln -s /path/to/createos-claude-plugins/packages/codex-plugin \ createos login ``` -3. **Host tools:** `jq`, `tar`, `bash`, `base64`, `perl`, `curl` - ## How it works -1. On session start, the hook publishes the `cos` driver's absolute path into context -2. The skill teaches Codex when and how to use sandbox commands -3. Codex runs `cos offload`, `cos up`, `cos run`, etc. via the bash tool -4. All commands execute inside remote CreateOS Sandboxes +1. Plugin installs a skill that teaches Codex the `createos` CLI commands +2. When you ask to run code in a sandbox, Codex uses `createos sandbox create` + `createos sandbox exec` +3. All commands execute inside remote CreateOS Sandboxes, not on your machine -## Commands +## Commands the skill teaches | Command | What | |---|---| -| `cos offload ` | One-shot: stage dir → run → pull artifacts → destroy | -| `cos fanout [cmd2 …]` | Parallel: each command in its own box | -| `cos shell` | Instant throwaway Linux (user runs interactively) | -| `cos up` / `cos run` / `cos down` | Reusable project box | -| `cos sync ` | File sync (laptop → box) | -| `cos tunnel ` | Forward box port to localhost | -| `cos expose ` | Public HTTPS URL | -| `cos cluster up N` | N boxes on one private network | -| `cos disk create/attach/detach` | S3 disk mounts | -| `cos vpn register/up` | WireGuard VPN | -| `cos pause` / `cos resume` | Park/restore warm box | -| `cos fork` | Snapshot → independent clone | +| `createos sandbox create` | Create a sandbox | +| `createos sandbox exec -- sh -c ''` | Run command inside sandbox | +| `createos sandbox list` | List sandboxes | +| `createos sandbox get ` | Sandbox status/IP/ingress | +| `createos sandbox rm --yes` | Destroy sandbox | +| `createos sandbox pause/resume ` | Park/restore | +| `createos sandbox pull -` | Read file from sandbox | +| `createos sandbox tunnel --remote --local ` | Port forward | +| `createos sandbox network create/attach/show` | Private networks | +| `createos sandbox disk create/attach` | S3 disk mounts | +| `createos sandbox devices register` | Device VPN setup | ## Architecture ``` packages/codex-plugin/ -├── manifest.json # Plugin manifest -├── scripts/ -│ ├── cos # CLI driver (same as claude-code-plugin) -│ └── session-start.sh # Session hook — publishes cos path +├── .claude-plugin/ +│ └── plugin.json # Plugin manifest (name, skills ref) ├── skills/ │ └── using-createos-sandbox/ -│ ├── SKILL.md # Main skill +│ ├── SKILL.md # Main skill — createos CLI commands │ └── references/ │ ├── offload-and-egress.md │ ├── networking.md │ └── lifecycle-and-images.md +├── scripts/ +│ ├── cos # CLI driver (advanced offload patterns) +│ └── session-start.sh # Session hook +├── manifest.json └── README.md ``` -## Differences from Claude Code plugin - -Minimal. The Codex plugin uses the same skill, references, and `cos` driver. -The only differences are: +## Differences from Pi and OpenCode plugins -- **Manifest format**: `manifest.json` (Codex) vs `.claude-plugin/plugin.json` (Claude Code) -- **Hook format**: Shell script output (Codex) vs JSON hook response (Claude Code) -- **No slash commands**: Codex uses skills + bash tool, not slash commands +| Capability | Pi | OpenCode | Codex | +|---|---|---|---| +| Tool replacement | Yes — transparent | No — prompt injection | No — skill-based | +| Custom tools | 47 registered tools | 33 registered tools | None — uses bash + createos CLI | +| Integration | `pi.registerTool()` | `tool()` in plugin | Skill teaches CLI commands | +| Install | `pi install npm:@createos/pi` | `opencode plugin @createos/opencode` | `codex plugin add createos-codex@createos` | ## License From 0b338a45eb2784b853c2dbcf75e130e2a089f7e7 Mon Sep 17 00:00:00 2001 From: Bhautik Date: Wed, 5 Aug 2026 17:07:38 +0530 Subject: [PATCH 4/6] chore: align all plugin names to @createos/ convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - claude-code-plugin → @createos/claude-code - createos-codex → @createos/codex - @createos/pi and @createos/opencode already correct - Updated marketplace.json, all READMEs, and install commands --- .claude-plugin/marketplace.json | 2 +- README.md | 4 ++-- packages/codex-plugin/.claude-plugin/plugin.json | 2 +- packages/codex-plugin/README.md | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 73d961f..681bf18 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "description": "Run ad-hoc/heavy/untrusted code in disposable CreateOS Sandboxes; offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public expose, network clusters, S3 disks, WireGuard VPN, fork, pause/resume, and custom images." }, { - "name": "createos-codex", + "name": "@createos/codex", "source": "./packages/codex-plugin", "description": "Codex plugin for disposable CreateOS Sandboxes — skill + createos CLI for sandbox lifecycle, networking, disks, and VPN." } diff --git a/README.md b/README.md index 8b29ad6..9c544c2 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ pi --createos codex plugin marketplace add NodeOps-app/createos-claude-plugins # 2. Install the plugin -codex plugin add createos-codex@createos +codex plugin add @createos/codex@createos # 3. Launch codex — the skill teaches createos CLI usage codex @@ -78,7 +78,7 @@ The `createos` CLI **auto-installs** on first use. Sign in once with `createos l |---|---| | [**claude-code-plugin**](./packages/claude-code-plugin) | Hooks-based Claude Code plugin — offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public HTTPS expose, private-network clusters, BYO-S3 disk mounts, WireGuard VPN, and snapshot/fork — all driving the authed `createos` CLI. | | [**pi-extension**](./packages/pi-extension) | Pi coding agent extension that transparently routes all built-in commands (bash, read, write, edit, ls, find, grep) to a remote CreateOS Sandbox, plus 40 additional tools for sandbox lifecycle, configuration, port tunnels, file sync, private networks, persistent disks, custom image templates, remote editors, and device VPN — 47 tools total. | -| [**createos-codex**](./packages/codex-plugin) | Codex plugin — skill that teaches the `createos` CLI for sandbox lifecycle, networking, disks, and VPN. | +| [**@createos/codex**](./packages/codex-plugin) | Codex plugin — skill that teaches the `createos` CLI for sandbox lifecycle, networking, disks, and VPN. | | [**@createos/opencode**](./packages/opencode-plugin) | OpenCode plugin with 33 sandbox tools (`sandbox_exec`, `sandbox_push`, `sandbox_pull`, networks, disks, VPN, sync) and system prompt injection for sandbox-first workflows. | ## Claude Code — commands at a glance diff --git a/packages/codex-plugin/.claude-plugin/plugin.json b/packages/codex-plugin/.claude-plugin/plugin.json index 88a51d8..8beca17 100644 --- a/packages/codex-plugin/.claude-plugin/plugin.json +++ b/packages/codex-plugin/.claude-plugin/plugin.json @@ -1,5 +1,5 @@ { - "name": "createos-codex", + "name": "@createos/codex", "displayName": "CreateOS Sandbox", "version": "0.1.0", "description": "Run code off your machine in disposable CreateOS Sandboxes via the createos CLI.", diff --git a/packages/codex-plugin/README.md b/packages/codex-plugin/README.md index 144ca5e..b00cfc4 100644 --- a/packages/codex-plugin/README.md +++ b/packages/codex-plugin/README.md @@ -11,7 +11,7 @@ for sandbox lifecycle, networking, persistent disks, VPN, and more. codex plugin marketplace add NodeOps-app/createos-claude-plugins # 2. Install the plugin -codex plugin add createos-codex@createos +codex plugin add @createos/codex@createos ``` ## Prerequisites @@ -75,7 +75,7 @@ packages/codex-plugin/ | Tool replacement | Yes — transparent | No — prompt injection | No — skill-based | | Custom tools | 47 registered tools | 33 registered tools | None — uses bash + createos CLI | | Integration | `pi.registerTool()` | `tool()` in plugin | Skill teaches CLI commands | -| Install | `pi install npm:@createos/pi` | `opencode plugin @createos/opencode` | `codex plugin add createos-codex@createos` | +| Install | `pi install npm:@createos/pi` | `opencode plugin @createos/opencode` | `codex plugin add @createos/codex@createos` | ## License From 843dea3c7107038d636ef130cdcceb2b0d605897 Mon Sep 17 00:00:00 2001 From: Bhautik Date: Wed, 5 Aug 2026 18:22:02 +0530 Subject: [PATCH 5/6] chore: apply oxfmt formatting to codex plugin --- README.md | 101 ++++++++++-------- packages/codex-plugin/README.md | 39 +++---- .../skills/using-createos-sandbox/SKILL.md | 29 +++-- .../references/lifecycle-and-images.md | 16 +-- .../references/networking.md | 10 +- .../references/offload-and-egress.md | 36 +++---- 6 files changed, 127 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 9c544c2..193595e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Heavy builds, flaky test suites, and untrusted code don't belong on your laptop. ## Quick start **Claude Code:** + ```bash # 1. Add the marketplace + install the plugin /plugin marketplace add NodeOps-app/createos-claude-plugins @@ -41,6 +42,7 @@ Heavy builds, flaky test suites, and untrusted code don't belong on your laptop. ``` **Pi:** + ```bash # 1. Install the extension pi install npm:@createos/pi @@ -50,6 +52,7 @@ pi --createos ``` **Codex:** + ```bash # 1. Add the marketplace codex plugin marketplace add NodeOps-app/createos-claude-plugins @@ -62,6 +65,7 @@ codex ``` **OpenCode:** + ```bash # 1. Install the plugin opencode plugin @createos/opencode --global @@ -74,30 +78,30 @@ The `createos` CLI **auto-installs** on first use. Sign in once with `createos l ## Packages -| Package | What it does | -|---|---| -| [**claude-code-plugin**](./packages/claude-code-plugin) | Hooks-based Claude Code plugin — offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public HTTPS expose, private-network clusters, BYO-S3 disk mounts, WireGuard VPN, and snapshot/fork — all driving the authed `createos` CLI. | -| [**pi-extension**](./packages/pi-extension) | Pi coding agent extension that transparently routes all built-in commands (bash, read, write, edit, ls, find, grep) to a remote CreateOS Sandbox, plus 40 additional tools for sandbox lifecycle, configuration, port tunnels, file sync, private networks, persistent disks, custom image templates, remote editors, and device VPN — 47 tools total. | -| [**@createos/codex**](./packages/codex-plugin) | Codex plugin — skill that teaches the `createos` CLI for sandbox lifecycle, networking, disks, and VPN. | -| [**@createos/opencode**](./packages/opencode-plugin) | OpenCode plugin with 33 sandbox tools (`sandbox_exec`, `sandbox_push`, `sandbox_pull`, networks, disks, VPN, sync) and system prompt injection for sandbox-first workflows. | +| Package | What it does | +| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [**claude-code-plugin**](./packages/claude-code-plugin) | Hooks-based Claude Code plugin — offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public HTTPS expose, private-network clusters, BYO-S3 disk mounts, WireGuard VPN, and snapshot/fork — all driving the authed `createos` CLI. | +| [**pi-extension**](./packages/pi-extension) | Pi coding agent extension that transparently routes all built-in commands (bash, read, write, edit, ls, find, grep) to a remote CreateOS Sandbox, plus 40 additional tools for sandbox lifecycle, configuration, port tunnels, file sync, private networks, persistent disks, custom image templates, remote editors, and device VPN — 47 tools total. | +| [**@createos/codex**](./packages/codex-plugin) | Codex plugin — skill that teaches the `createos` CLI for sandbox lifecycle, networking, disks, and VPN. | +| [**@createos/opencode**](./packages/opencode-plugin) | OpenCode plugin with 33 sandbox tools (`sandbox_exec`, `sandbox_push`, `sandbox_pull`, networks, disks, VPN, sync) and system prompt injection for sandbox-first workflows. | ## Claude Code — commands at a glance -| Command | What | -|---|---| -| `/createos-sandbox:offload ` | one-shot: stage → run → pull artifacts → destroy | -| `/createos-sandbox:fanout [cmd2 …]` | run each command in its own throwaway box, in parallel | -| `/createos-sandbox:shell` | instant throwaway interactive Linux (destroyed on exit) | -| `/createos-sandbox:up` · `run` · `sync` · `down` | reusable per-repo box + file sync for live dev loops | -| `/createos-sandbox:tunnel ` | forward a box port to `127.0.0.1` (private) | -| `/createos-sandbox:expose ` | public HTTPS URL for a box port | -| `/createos-sandbox:cluster …` | N boxes on one private network, name-addressable | -| `/createos-sandbox:disk …` | mount your own S3 bucket into the project box | -| `/createos-sandbox:vpn …` | WireGuard L3 into your private networks | -| `/createos-sandbox:fork` | snapshot the project box → independent clone | -| `/createos-sandbox:pause` · `resume` | park the warm box at zero compute cost, then restore it exactly | -| `/createos-sandbox:template …` | build a custom image so boxes boot with the toolchain already installed | -| `/createos-sandbox:status` | show active box + sync + tunnels + cluster | +| Command | What | +| ------------------------------------------------ | ----------------------------------------------------------------------- | +| `/createos-sandbox:offload ` | one-shot: stage → run → pull artifacts → destroy | +| `/createos-sandbox:fanout [cmd2 …]` | run each command in its own throwaway box, in parallel | +| `/createos-sandbox:shell` | instant throwaway interactive Linux (destroyed on exit) | +| `/createos-sandbox:up` · `run` · `sync` · `down` | reusable per-repo box + file sync for live dev loops | +| `/createos-sandbox:tunnel ` | forward a box port to `127.0.0.1` (private) | +| `/createos-sandbox:expose ` | public HTTPS URL for a box port | +| `/createos-sandbox:cluster …` | N boxes on one private network, name-addressable | +| `/createos-sandbox:disk …` | mount your own S3 bucket into the project box | +| `/createos-sandbox:vpn …` | WireGuard L3 into your private networks | +| `/createos-sandbox:fork` | snapshot the project box → independent clone | +| `/createos-sandbox:pause` · `resume` | park the warm box at zero compute cost, then restore it exactly | +| `/createos-sandbox:template …` | build a custom image so boxes boot with the toolchain already installed | +| `/createos-sandbox:status` | show active box + sync + tunnels + cluster | Full flags, networking guide, and heavy-build tips live in the [**Claude Code Plugin README**](./packages/claude-code-plugin/README.md). @@ -105,53 +109,55 @@ Full flags, networking guide, and heavy-build tips live in the [**Claude Code Pl All built-in tools (bash, read, write, edit, ls, find, grep) transparently route to the sandbox — plus 40 additional tools for lifecycle, networking, disks, image templates, remote editors, and device VPN (47 tools total). -| Command | What | -|---------|------| -| `/sandbox` | Show sandbox status | -| `/network create ` | Create a private network | -| `/network ls` | List your networks | -| `/network show ` | Show network members + IPs | -| `/network attach ` | Join this sandbox to a network | -| `/network detach ` | Leave a network | -| `/network rm ` | Delete a network | -| `/device status` | Show registered devices | +| Command | What | +| -------------------------- | ------------------------------------- | +| `/sandbox` | Show sandbox status | +| `/network create ` | Create a private network | +| `/network ls` | List your networks | +| `/network show ` | Show network members + IPs | +| `/network attach ` | Join this sandbox to a network | +| `/network detach ` | Leave a network | +| `/network rm ` | Delete a network | +| `/device status` | Show registered devices | | `/device attach ` | Give your machine access to a network | -| `/device detach ` | Remove access | +| `/device detach ` | Remove access | ### Flags -| Flag | Purpose | -|------|---------| -| `--createos` | Activate the extension | -| `--shape ` | Sandbox size (default: `s-2vcpu-2gb`) | -| `--rootfs ` | Base image or template | -| `--network ` | Network(s) to join at creation | +| Flag | Purpose | +| ------------------ | ------------------------------------- | +| `--createos` | Activate the extension | +| `--shape ` | Sandbox size (default: `s-2vcpu-2gb`) | +| `--rootfs ` | Base image or template | +| `--network ` | Network(s) to join at creation | Full tool inventory lives in the [**Pi Extension README**](./packages/pi-extension/README.md). ## OpenCode — tools at a glance (40) -| Category | Tools | -|---|---| -| **Execute & Files** | `sandbox_exec`, `sandbox_pull`, `sandbox_push` | -| **Lifecycle** | `sandbox_create`, `sandbox_list`, `sandbox_info`, `sandbox_pause`, `sandbox_resume`, `sandbox_fork`, `sandbox_destroy` | -| **Config** | `sandbox_ingress`, `sandbox_firewall`, `sandbox_bandwidth`, `sandbox_shapes`, `sandbox_images` | -| **Ports & Sync** | `sandbox_preview_url`, `sandbox_tunnel`, `sandbox_sync` | -| **Networks** | `sandbox_network_create/list/show/attach/detach/delete` | -| **Disks** | `sandbox_disk_create/list/show/delete/attach/detach` | -| **Device VPN** | `sandbox_device_register/status/attach/detach`, `sandbox_vpn_up` | +| Category | Tools | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| **Execute & Files** | `sandbox_exec`, `sandbox_pull`, `sandbox_push` | +| **Lifecycle** | `sandbox_create`, `sandbox_list`, `sandbox_info`, `sandbox_pause`, `sandbox_resume`, `sandbox_fork`, `sandbox_destroy` | +| **Config** | `sandbox_ingress`, `sandbox_firewall`, `sandbox_bandwidth`, `sandbox_shapes`, `sandbox_images` | +| **Ports & Sync** | `sandbox_preview_url`, `sandbox_tunnel`, `sandbox_sync` | +| **Networks** | `sandbox_network_create/list/show/attach/detach/delete` | +| **Disks** | `sandbox_disk_create/list/show/delete/attach/detach` | +| **Device VPN** | `sandbox_device_register/status/attach/detach`, `sandbox_vpn_up` | Full reference in [opencode-plugin/README.md](./packages/opencode-plugin/README.md). ## Install **From GitHub (recommended):** + ``` /plugin marketplace add NodeOps-app/createos-claude-plugins /plugin install @createos/claude-code@createos ``` **From a local checkout:** + ``` git clone https://github.com/NodeOps-app/createos-claude-plugins /plugin marketplace add /path/to/createos-claude-plugins @@ -159,6 +165,7 @@ git clone https://github.com/NodeOps-app/createos-claude-plugins ``` **Dev (instant, no install):** + ```bash claude --plugin-dir /path/to/createos-claude-plugins/packages/claude-code-plugin /reload-plugins # after editing plugin files diff --git a/packages/codex-plugin/README.md b/packages/codex-plugin/README.md index b00cfc4..59f8768 100644 --- a/packages/codex-plugin/README.md +++ b/packages/codex-plugin/README.md @@ -17,6 +17,7 @@ codex plugin add @createos/codex@createos ## Prerequisites 1. **createos CLI** — auto-installs on first use, or manually: + ```bash curl -sfL https://raw.githubusercontent.com/NodeOps-app/createos-cli/main/install.sh | sh ``` @@ -34,19 +35,19 @@ codex plugin add @createos/codex@createos ## Commands the skill teaches -| Command | What | -|---|---| -| `createos sandbox create` | Create a sandbox | -| `createos sandbox exec -- sh -c ''` | Run command inside sandbox | -| `createos sandbox list` | List sandboxes | -| `createos sandbox get ` | Sandbox status/IP/ingress | -| `createos sandbox rm --yes` | Destroy sandbox | -| `createos sandbox pause/resume ` | Park/restore | -| `createos sandbox pull -` | Read file from sandbox | -| `createos sandbox tunnel --remote --local ` | Port forward | -| `createos sandbox network create/attach/show` | Private networks | -| `createos sandbox disk create/attach` | S3 disk mounts | -| `createos sandbox devices register` | Device VPN setup | +| Command | What | +| ------------------------------------------------------------- | -------------------------- | +| `createos sandbox create` | Create a sandbox | +| `createos sandbox exec -- sh -c ''` | Run command inside sandbox | +| `createos sandbox list` | List sandboxes | +| `createos sandbox get ` | Sandbox status/IP/ingress | +| `createos sandbox rm --yes` | Destroy sandbox | +| `createos sandbox pause/resume ` | Park/restore | +| `createos sandbox pull -` | Read file from sandbox | +| `createos sandbox tunnel --remote --local ` | Port forward | +| `createos sandbox network create/attach/show` | Private networks | +| `createos sandbox disk create/attach` | S3 disk mounts | +| `createos sandbox devices register` | Device VPN setup | ## Architecture @@ -70,12 +71,12 @@ packages/codex-plugin/ ## Differences from Pi and OpenCode plugins -| Capability | Pi | OpenCode | Codex | -|---|---|---|---| -| Tool replacement | Yes — transparent | No — prompt injection | No — skill-based | -| Custom tools | 47 registered tools | 33 registered tools | None — uses bash + createos CLI | -| Integration | `pi.registerTool()` | `tool()` in plugin | Skill teaches CLI commands | -| Install | `pi install npm:@createos/pi` | `opencode plugin @createos/opencode` | `codex plugin add @createos/codex@createos` | +| Capability | Pi | OpenCode | Codex | +| ---------------- | ----------------------------- | ------------------------------------ | ------------------------------------------- | +| Tool replacement | Yes — transparent | No — prompt injection | No — skill-based | +| Custom tools | 47 registered tools | 33 registered tools | None — uses bash + createos CLI | +| Integration | `pi.registerTool()` | `tool()` in plugin | Skill teaches CLI commands | +| Install | `pi install npm:@createos/pi` | `opencode plugin @createos/opencode` | `codex plugin add @createos/codex@createos` | ## License diff --git a/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md b/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md index f2a3918..10251ca 100644 --- a/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md +++ b/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md @@ -16,11 +16,13 @@ createos version ``` If not installed, install it: + ```bash curl -sfL https://raw.githubusercontent.com/NodeOps-app/createos-cli/main/install.sh | sh ``` Check auth: + ```bash createos sandbox shapes ``` @@ -29,42 +31,48 @@ If that fails, the user needs to run `createos login` in their own terminal (bro ## When to use it -| Situation | Why | -|---|---| -| Untrusted/unknown code | Isolation — blast radius is one disposable box | -| Heavy build or test suite | Keeps the laptop free | -| Quick scratch Linux | Instant clean box, destroyed when done | -| Clean-room repro | Fresh rootfs every time | -| Multi-machine setup | Private network clusters | +| Situation | Why | +| ------------------------- | ---------------------------------------------- | +| Untrusted/unknown code | Isolation — blast radius is one disposable box | +| Heavy build or test suite | Keeps the laptop free | +| Quick scratch Linux | Instant clean box, destroyed when done | +| Clean-room repro | Fresh rootfs every time | +| Multi-machine setup | Private network clusters | ## Core commands ### Create a sandbox + ```bash createos sandbox create --shape s-2vcpu-2gb --ingress ``` ### Run a command inside a sandbox + ```bash createos sandbox exec -- sh -c 'hostname && uname -a' ``` ### List sandboxes + ```bash createos sandbox list ``` ### Get sandbox info + ```bash createos sandbox get ``` ### Destroy a sandbox + ```bash createos sandbox rm --yes ``` ### Pause / Resume + ```bash createos sandbox pause createos sandbox resume @@ -73,11 +81,13 @@ createos sandbox resume ## File transfer ### Push a file to sandbox + ```bash echo 'file content' | base64 | createos sandbox exec -- sh -c "base64 -d > /path/to/file" ``` ### Pull a file from sandbox + ```bash createos sandbox pull /path/to/file - ``` @@ -85,14 +95,17 @@ createos sandbox pull /path/to/file - ## Networking ### Get a public URL for a port + The sandbox's ingress URL template is in `createos sandbox get `. Replace `` with the actual port number. ### Port tunnel to localhost + ```bash createos sandbox tunnel --remote --local ``` ### Private networks (multi-node) + ```bash createos sandbox network create createos sandbox network attach @@ -100,12 +113,14 @@ createos sandbox network show ``` ## Persistent storage (S3 disks) + ```bash createos sandbox disk create --bucket --endpoint --access-key --secret-key createos sandbox disk attach /mnt/data ``` ## Device VPN + ```bash createos sandbox devices register # User runs in separate terminal (requires sudo): diff --git a/packages/codex-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md b/packages/codex-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md index 0f62834..3cfb404 100644 --- a/packages/codex-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md +++ b/packages/codex-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md @@ -52,19 +52,19 @@ The clone is a fully independent box with its own id, IP, and quota ledger. The Two things to know: -- A fork is **not** tracked as the project box, so `cos down` leaves it running. It *is* recorded in the statefile: `cos status` lists forks, `cos down` names the survivors, and `cos down -f` reaps them. Otherwise destroy it yourself with `createos sandbox rm -y `. +- A fork is **not** tracked as the project box, so `cos down` leaves it running. It _is_ recorded in the statefile: `cos status` lists forks, `cos down` names the survivors, and `cos down -f` reaps them. Otherwise destroy it yourself with `createos sandbox rm -y `. - **Mounted disks do not carry across a fork.** If the source box had an S3 disk attached, re-attach it on the clone. ## Images: built-in rootfs and custom templates Built-ins are kept warm on the hosts, so they boot with no image pull: -| Rootfs | Notes | -|---|---| -| `devbox:1` | Debian, batteries included, the default — has `sshd`, which `sync` and the editor path need | -| `ubuntu:26.04` | plain Ubuntu | -| `debian:13` | trixie | -| `alpine:3.20` | musl + busybox, far smaller; expect glibc-linked binaries and wheels not to work | +| Rootfs | Notes | +| -------------- | ------------------------------------------------------------------------------------------- | +| `devbox:1` | Debian, batteries included, the default — has `sshd`, which `sync` and the editor path need | +| `ubuntu:26.04` | plain Ubuntu | +| `debian:13` | trixie | +| `alpine:3.20` | musl + busybox, far smaller; expect glibc-linked binaries and wheels not to work | `createos sandbox rootfs` lists what the account can actually boot. @@ -111,7 +111,7 @@ createos sandbox editor --remove # tear down the ~/.ssh/config entry It generates a per-box SSH key, starts `sshd` in the box, writes a `~/.ssh/config` block so plain `ssh ` works, and launches the editor pointed at the remote. It needs a rootfs with `sshd` (`devbox:1` has it) and a shape with more than 2 GiB of RAM — language servers OOM on a 1 GiB box. -This is the right tool when the user wants to *work in* the sandbox rather than have an agent drive it. It launches a GUI editor, so it belongs to the user, not to an agent command. +This is the right tool when the user wants to _work in_ the sandbox rather than have an agent drive it. It launches a GUI editor, so it belongs to the user, not to an agent command. ## Letting a job end its own box diff --git a/packages/codex-plugin/skills/using-createos-sandbox/references/networking.md b/packages/codex-plugin/skills/using-createos-sandbox/references/networking.md index a56a726..05ab935 100644 --- a/packages/codex-plugin/skills/using-createos-sandbox/references/networking.md +++ b/packages/codex-plugin/skills/using-createos-sandbox/references/networking.md @@ -12,12 +12,12 @@ Read this when something needs to talk to a sandbox: a browser, a teammate, anot ## Choosing between tunnel, expose, cluster, and VPN -| Need | Reach for | -|---|---| -| Hit a box-side dev server from this machine's browser | `tunnel` | -| Give a teammate a link, or point a webhook at the box | `expose` | +| Need | Reach for | +| --------------------------------------------------------------------------------- | --------- | +| Hit a box-side dev server from this machine's browser | `tunnel` | +| Give a teammate a link, or point a webhook at the box | `expose` | | Boxes that need to talk to each other (replication, p2p, load generator → target) | `cluster` | -| The laptop needs the whole private network, not one port | `vpn` | +| The laptop needs the whole private network, not one port | `vpn` | `tunnel` and `expose` operate on the project box, so `cos up` first. `cluster` manages its own set of boxes. `cos down` tears down tunnels, the public URL, and the cluster along with the project box. diff --git a/packages/codex-plugin/skills/using-createos-sandbox/references/offload-and-egress.md b/packages/codex-plugin/skills/using-createos-sandbox/references/offload-and-egress.md index fddcdfb..e0d45a6 100644 --- a/packages/codex-plugin/skills/using-createos-sandbox/references/offload-and-egress.md +++ b/packages/codex-plugin/skills/using-createos-sandbox/references/offload-and-egress.md @@ -15,17 +15,17 @@ Read this when an offload needs tuning: restricting what a build can reach, sizi Flags come **before** the ` ` positionals — `cos` parses with `getopts`, which stops at the first positional. -| Flag | Effect | -|---|---| -| `-s ` | box size (default `s-1vcpu-1gb`); list with `createos sandbox shapes` | -| `-r ` | base image or custom template (default `devbox:1`) | -| `-o ` | tar this path out of `/work` back into the local dir after the run | -| `-w ` | try to add a swapfile (best-effort — see below) | -| `-K` | keep the box if the command exits non-zero, so the cache survives for a retry | -| `-e ` | allow one outbound destination (repeatable) | -| `-p ` | apply an egress preset (repeatable, composes with `-e`) | -| `-E` | explicitly unrestricted egress | -| `-x ` | extra upload exclude (repeatable) | +| Flag | Effect | +| ------------- | ----------------------------------------------------------------------------- | +| `-s ` | box size (default `s-1vcpu-1gb`); list with `createos sandbox shapes` | +| `-r ` | base image or custom template (default `devbox:1`) | +| `-o ` | tar this path out of `/work` back into the local dir after the run | +| `-w ` | try to add a swapfile (best-effort — see below) | +| `-K` | keep the box if the command exits non-zero, so the cache survives for a retry | +| `-e ` | allow one outbound destination (repeatable) | +| `-p ` | apply an egress preset (repeatable, composes with `-e`) | +| `-E` | explicitly unrestricted egress | +| `-x ` | extra upload exclude (repeatable) | The box is destroyed on success, failure, or interrupt. Two things override that: `-K` on a failing command, and an infra/stream error — both keep the box so the build cache survives, and `cos` prints the reconnect and destroy commands. @@ -37,7 +37,7 @@ The local tree goes to `/work` in the box, one-way. Box-side changes never touch Excluded from the upload by default: `.git`, `target`, `node_modules`, `__pycache__`, `.venv`, `.mypy_cache`, `.pytest_cache`, `.gradle`, `.cargo/registry`, `dist`, `build`, `.next`, `.turbo`, and large media (`*.gif`, `*.mp4`, `*.mov`, `*.zst`). Add more with `-x `. -Dependencies are meant to be built *inside* the box, not shipped into it — that is why `node_modules` and friends are excluded rather than uploaded. +Dependencies are meant to be built _inside_ the box, not shipped into it — that is why `node_modules` and friends are excluded rather than uploaded. ## Egress: how the firewall actually behaves @@ -56,12 +56,12 @@ DNS keeps resolving even for blocked destinations — a blocked connection fails ## Egress presets -| Preset | Opens | -|---|---| -| `python-uv` | `astral.sh`, `releases.astral.sh`, `pypi.org`, `files.pythonhosted.org` | -| `rust-cargo` | `crates.io`, `static.crates.io`, `index.crates.io`, `static.rust-lang.org`, `cdn.pyke.io` | -| `npm` | `registry.npmjs.org` | -| `github` | `github.com`, `objects.githubusercontent.com`, `raw.githubusercontent.com`, `codeload.github.com` | +| Preset | Opens | +| ------------ | ------------------------------------------------------------------------------------------------- | +| `python-uv` | `astral.sh`, `releases.astral.sh`, `pypi.org`, `files.pythonhosted.org` | +| `rust-cargo` | `crates.io`, `static.crates.io`, `index.crates.io`, `static.rust-lang.org`, `cdn.pyke.io` | +| `npm` | `registry.npmjs.org` | +| `github` | `github.com`, `objects.githubusercontent.com`, `raw.githubusercontent.com`, `codeload.github.com` | `cdn.pyke.io` is the non-obvious one: `ort-sys` (ONNX Runtime, pulled in by a lot of ML crates) downloads prebuilt binaries from it, so a `cargo build` that looks pure-Rust fails without it. It is already in `rust-cargo`. From de60dda95a8a8b9b02e172f8b401c5fb1645c616 Mon Sep 17 00:00:00 2001 From: Bhautik Date: Thu, 6 Aug 2026 10:50:42 +0530 Subject: [PATCH 6/6] fix(codex): swap network attach arg order in skill docs The CLI now expects ` ` instead of ` `. --- packages/codex-plugin/skills/using-createos-sandbox/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md b/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md index 10251ca..ddb80d4 100644 --- a/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md +++ b/packages/codex-plugin/skills/using-createos-sandbox/SKILL.md @@ -108,7 +108,7 @@ createos sandbox tunnel --remote --local ```bash createos sandbox network create -createos sandbox network attach +createos sandbox network attach createos sandbox network show ```