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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,65 @@ a3s-box unseal dev --context app/key

TEE features include SNP report parsing/verification, RA-TLS certificate extensions, AES-256-GCM sealing with HKDF-SHA256, and RA-TLS secret injection. Treat simulation as a developer workflow only; it does not prove hardware isolation. TDX is not productized.

## Coding-agent skill

`integrations/skills/a3s-box/SKILL.md` is an [Agent Skills](https://agentskills.io)
`SKILL.md` that teaches an AI coding agent to drive this CLI — the `--` separator,
the box lifecycle, snapshots, the warm pool, the networking footguns, and an
errors→fix table for recovery. It is **one file** in the cross-tool Agent Skills
format, so the same skill works in Claude Code, OpenAI Codex, Gemini CLI, Cursor,
Sourcegraph Amp, OpenCode, Zed, and a3s-code — no per-agent variant.

### Install

The installer symlinks the single `SKILL.md` into each agent's skills directory
(one source of truth):

```bash
cd integrations/skills

./install.sh all # this project: .agents + .claude + .codex + .a3s
./install.sh --home agents claude # user-wide: ~/.agents + ~/.claude
./install.sh --dir ./agent/skills # any SKILL.md-format skills dir
./install.sh --copy all # copy instead of symlink
```

Two roots reach almost every skill-capable agent; install the targets you use:

| Target | Skills root | Reached by |
|--------|-------------|------------|
| `agents` | `.agents/skills/` | OpenAI Codex · Gemini CLI · Amp · Cursor · OpenCode · Zed |
| `claude` | `.claude/skills/` | Claude Code · Claude Agent SDK · Cline · Cursor/OpenCode (compat) |
| `codex` | `.codex/skills/` | Codex (project-specific path) |
| `a3s-code` | `.a3s/skills/` | a3s-code |
| `all` | all of the above | |

Manual equivalent (no installer):
`ln -s "$(pwd)/a3s-box/SKILL.md" <root>/a3s-box/SKILL.md`.

### Use

Reload the agent so it rescans its skills directory. The skill then:

- **surfaces as the `/a3s-box` slash command** (the directory name is the command), and
- **is auto-invoked** when you ask the agent to run, build, exec into, snapshot, or
sandbox something with a3s-box — its `description` is matched against your request.

The agent reads the skill body and drives `a3s-box` for you — e.g. *"build this repo
and run it in a sandbox"* → `a3s-box build` → `a3s-box run -d` → verify it's up. The
skill restricts itself to `Bash(a3s-box*)`, so it can only invoke this CLI.

### Agents without a skill mechanism

GitHub Copilot, Windsurf/Devin, Continue.dev, Aider, and Jules/Factory have only
always-on instruction files (no on-demand skills). To make one aware of a3s-box, add
a one-line pointer to `integrations/skills/a3s-box/SKILL.md` in that tool's
instructions file, or in a repo-root **`AGENTS.md`** (which most of them read).

See [`integrations/skills/README.md`](integrations/skills/README.md) for the full
agent matrix, the no-skill-agent details, and why this is a skill rather than a
Claude Code plugin.

## Kubernetes CRI

The CRI server is reachable by standard gRPC clients — `crictl`, the kubelet, and `critest` — over its Unix domain socket, and runs the core pod + container lifecycle and `exec` end to end. It is Linux-only and not yet fully `critest`-conformant.
Expand Down
57 changes: 57 additions & 0 deletions integrations/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# a3s-box agent skill

One `SKILL.md` that teaches an AI coding agent to drive the `a3s-box` CLI. It
uses the cross-tool **Agent Skills** format (`<name>/SKILL.md`), so the *same
file* works in every agent that supports skills — there is no per-agent variant.

## Install

```sh
./install.sh all # .agents + .claude + .codex + .a3s, this repo
./install.sh --home agents claude # user-wide (~/.agents, ~/.claude)
./install.sh --dir ./agent/skills # any SKILL.md-format skills dir
./install.sh --copy all # copy instead of symlink
```

The installer symlinks the one `SKILL.md` into each skills root (single source
of truth). Manual equivalent: `ln -s "$(pwd)/a3s-box/SKILL.md" <root>/a3s-box/SKILL.md`.

## Which agents this reaches

Two skills roots cover almost every skill-capable coding agent (2026):

| Skills root | Reached by |
|-------------|-----------|
| `.agents/skills/` | OpenAI Codex · Gemini CLI · Sourcegraph Amp · Cursor · OpenCode · Zed |
| `.claude/skills/` | Claude Code · Claude Agent SDK · Cline · Cursor (compat) · OpenCode (compat) · the a3s CLI menus |
| `.codex/skills/` | Codex (project-specific path) |
| `.a3s/skills/` | a3s-code |

`--home` writes the `~/...` equivalents (`~/.agents/skills`, `~/.claude/skills`,
…). Reload the agent to pick up the skill. The skill directory name (`a3s-box`)
becomes the `/a3s-box` slash command.

## Agents without a skill mechanism

Some agents have no on-demand skills, only always-on instruction files:
**GitHub Copilot** (`.github/copilot-instructions.md`), **Windsurf / Devin**
(`.devin/rules/`), **Continue.dev** (`.continue/rules/`), **Aider**
(`CONVENTIONS.md`), **Jules / Factory** (`AGENTS.md`).

We deliberately do *not* ship a copy in each bespoke rules format. If you want
one of these to know about a3s-box, add a one-line pointer to `a3s-box/SKILL.md`
in that tool's instructions file. Most of them also read the cross-tool
**`AGENTS.md`** at the repo root, so a single `AGENTS.md` line reaches the
majority. (Claude Code is the exception — it reads `CLAUDE.md`, not `AGENTS.md`.)

`claude.ai` and the Claude API do not read the filesystem — upload the `a3s-box/`
folder as a skill ZIP / via the Skills API.

## Why a skill, not a plugin

A Claude Code *plugin* would only help Claude Code. A shared `SKILL.md` is the
single format the whole ecosystem discovers, so one file covers everything.

Note: the a3s-code loader caps skill bodies at 10 KiB and is fail-secure on
`allowed-tools` (omitting it denies all tool use), so the `SKILL.md` is kept
tight and declares `Bash(a3s-box*)`.
121 changes: 121 additions & 0 deletions integrations/skills/a3s-box/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
name: a3s-box
description: Drive the a3s-box microVM sandbox CLI — a Docker-like runtime that runs OCI/container images in hardware-isolated microVMs (libkrun). Use when the user wants to run, build, exec into, snapshot, or tear down containers with a3s-box, sandbox untrusted or agent-generated code, spin up throwaway isolated environments, or mentions a3s-box / microVM / libkrun / "run this safely in a sandbox". Teaches the box lifecycle, the `--` argv separator, the networking model, and error recovery. Not for plain Docker/Podman (different CLI, same verbs).
allowed-tools: Bash(a3s-box*), Bash(curl*), Read(*), Grep(*)
---

# a3s-box — drive the microVM sandbox

`a3s-box` runs OCI images inside hardware-isolated microVMs with a Docker-like
CLI (verbs mirror Docker). **Preflight once:** `a3s-box info` (confirms
virtualization + home dir).

## Mental model (these cause silent failures)

- **Lifecycle:** `created → running → paused → stopped → dead`.
- `run` = create **and** start. `create` stops at `created` → `start` it.
`snapshot restore` → `created` → `start` it.
- A box dies when its **PID 1 exits** — it needs a foreground process.
`run -d alpine` runs `sh`, which exits at once → `dead`. Use a long-lived
command: `run -d nginx`, or `run -d alpine -- sleep 3600`.
- `exec`/`shell`/`cp`/`top`/`attach` need a **running** box. `start` accepts
only `created|stopped|dead`. `rm` removes `created`/`stopped`/`dead` without
`-f`; `-f` is only for a `running`/`paused` box.
- **The `--` rule:** the in-box command goes **after `--`**.
`a3s-box exec <box> -- <cmd>` (required); `a3s-box run <image> [-- <cmd>]`
(optional override). Missing it → `error: unexpected argument '…' found`.
- **`exec` has a 5-second default timeout** — long builds/installs/tests get
killed mid-run (and look like a failure). Use `--timeout 300` (or `--timeout 0`
to disable): `a3s-box exec --timeout 300 web -- <cmd>`.
- **Detached boxes need the monitor.** `run -d` prints the id and exits; its
health/restart task dies with it. For `--restart` policies and health checks to
fire, run `a3s-box monitor` in the background first.
- **No in-guest localhost by default.** A box can't reach its own service on
`127.0.0.1` (so `--health-cmd 'curl localhost'` and in-box curl fail). To make
in-guest localhost work, attach a bridge network (recipe below). To check a
service from your side, curl the **published HOST port** (see Ports).
- **Ports:** publish with `-p HOST:GUEST` (TCP). But `ps`'s PORTS column and
`a3s-box port <box>` render `GUEST -> 0.0.0.0:HOST` — the host port to curl is
the one after `0.0.0.0:`. Read it with `a3s-box port <box>`.
- **Output streams:** `run -d` prints a human `Creating box <name> (<id>)...`
line, the full box id, and (when uncached) image-pull progress — all to
**stdout**; only tracing/WARN/ERROR go to **stderr**. Don't parse the id from
stdout — reference boxes by `--name`. JSON where offered: `inspect`,
`image-inspect`, `snapshot ls --json`, and `ps --format json` (2.6+ only; on
2.0 use `ps --format '{{.ID}} {{.Status}} {{.Names}}'`).

## Run → verify → exec → teardown

```sh
a3s-box info
a3s-box run -d --name web -p 8080:80 app:dev # reference by --name, not stdout
a3s-box ps -a --filter name=web # MUST use -a; expect STATUS=running
a3s-box port web # host port = value after 0.0.0.0:
curl -fsS http://localhost:8080/ # confirm SERVING from the host
a3s-box exec --timeout 60 web -- env # in-box command after --
a3s-box logs web # logs print on stderr
a3s-box stop web && a3s-box rm web
```

A box can boot then die — always verify with `ps -a` (a `dead`/gone box does
**not** appear in plain `ps`; empty output means dead-or-gone, not "name typo").
If STATUS=`dead`, its main process exited: read `a3s-box inspect web`
(`.State.ExitCode`, summary `dead (Exit N)`) and `a3s-box logs web`.

## Errors → fix

| Error (on stderr) | Cause → fix |
|---|---|
| `error: unexpected argument '…' found` (Usage … `-- <CMD>`) | missing `--` → `exec <box> -- <cmd>` |
| `Box X is not running` | stopped/created/dead → `a3s-box start X`; if just run, it died on boot → `inspect`/`logs X` |
| `Box X is not running (status: dead)` | PID 1 exited → `a3s-box inspect X` (`.State.ExitCode`); run a long-lived command |
| `No such box: X` | wrong ref → `a3s-box ps -a` to find name/id |
| `WARN … heartbeat failed, exec will not be available` | guest booted but exec channel never came up → unhealthy; `logs X`, recreate |
| `libkrun call failed status=-17 … krun_add_vsock_port2` / `VM boot failed` | started an already-running/stale box → `a3s-box ps`; if running just `exec`; if wedged `stop X` then `start X` |

## Core commands (non-obvious; full verb list: `a3s-box --help`)

| Goal | Command |
|---|---|
| Run one command, throwaway | `a3s-box run --rm alpine -- echo hi` |
| Create then start | `a3s-box create --name w nginx` → `a3s-box start w` |
| Exec / interactive shell | `a3s-box exec -it web -- /bin/sh` · `a3s-box shell web` |
| List, custom columns | `a3s-box ps -a --format '{{.ID}} {{.Status}} {{.Names}}'` |
| Build image | `a3s-box build -t app:dev .` |
| Copy in / out | `a3s-box cp ./f web:/data/f` · `a3s-box cp web:/data/f ./f` |
| Commit box → image | `a3s-box commit web app:snap` |

Resource/isolation flags on `run`/`create` — `--cpus` (default 2), `--memory`
(default 512m), `-e K=V`, `-v host:guest`, `--read-only`,
`--cap-drop ALL --cap-add NET_BIND_SERVICE`, `--pids-limit`, `--network`,
`--init`, … : `a3s-box run --help`.

## Recipes

**Working in-guest localhost (bridge network)**
```sh
a3s-box network create mynet --subnet 10.89.0.0/24
a3s-box run -d --name api --network mynet -p 8080:80 app:dev
```

**Snapshot → restore** (filesystem snapshot; restored box lands in `created`)
```sh
a3s-box snapshot create web # create from a running/stopped box
a3s-box snapshot ls --json
a3s-box snapshot restore <snap-id> --name restored # name it → no ps scraping
a3s-box ps -a --filter name=restored && a3s-box start restored
```

## Finding exact flags & versions

`a3s-box <command> --help` works for every command, and nested help works too:
`a3s-box snapshot create --help`, `a3s-box network create --help`, etc. Check the
build with `a3s-box version`; newer builds (≥2.4) add `pool run`,
`snapshot prune`, and `monitor --install/--metrics-addr`.

## More

Other areas — `network`/`volume`/`compose`/`pool` (warm pre-boot VMs), registry
`login`/`push`, `events`/`audit`/`df`, and TEE (`--tee`/`--tee-simulate`,
`attest`/`seal`/`unseal`/`inject-secret`, `--sidecar`): `a3s-box --help`, then
`a3s-box <cmd> --help`.
70 changes: 70 additions & 0 deletions integrations/skills/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/bin/sh
# Install the a3s-box agent skill into a coding agent's skills directory.
# One SKILL.md, reused across every agent that speaks the Agent-Skills (SKILL.md)
# format. Symlinks by default (single source of truth); --copy to detach.
#
# Usage:
# ./install.sh [--copy] [--home] <agent>...
# ./install.sh --dir <path> # install into an explicit skills dir
#
# agents: agents claude codex a3s-code all
# agents -> .agents/skills cross-tool standard: Codex, Gemini CLI, Amp,
# Cursor, OpenCode, Zed all read this root
# claude -> .claude/skills Claude Code/SDK, Cline, Cursor & OpenCode compat
# codex -> .codex/skills Codex-specific; the a3s CLI menu also scans it
# a3s-code -> .a3s/skills a3s-code agent dir
# all -> agents + claude + codex + a3s-code
# --home install at user scope ($HOME) instead of the current project
# --copy copy the file instead of symlinking
# --dir P treat P as a skills root and drop a3s-box/SKILL.md inside it
#
# Examples:
# ./install.sh all # wire every root in this repo
# ./install.sh --home agents claude # user-wide cross-tool + Claude Code
# ./install.sh --dir ./my-agent/skills # any SKILL.md-format agent dir
set -eu

SRC="$(CDPATH= cd -- "$(dirname -- "$0")/a3s-box" && pwd)/SKILL.md"
[ -f "$SRC" ] || { echo "error: SKILL.md not found at $SRC" >&2; exit 1; }

COPY=0; SCOPE=project; DIR=""; AGENTS=""
while [ $# -gt 0 ]; do
case "$1" in
--copy) COPY=1 ;;
--home) SCOPE=home ;;
--dir) shift; DIR="${1:?--dir needs a path}" ;;
agents|claude|codex|a3s-code|all) AGENTS="$AGENTS $1" ;;
-h|--help) sed -n '2,23p' "$0"; exit 0 ;;
*) echo "error: unknown arg '$1'" >&2; exit 1 ;;
esac
shift
done

# skills root for a named agent at the chosen scope
root_for() {
base="."; [ "$SCOPE" = home ] && base="$HOME"
case "$1" in
agents) echo "$base/.agents/skills" ;; # cross-tool: Codex/Gemini/Amp/Cursor/OpenCode/Zed
claude) echo "$base/.claude/skills" ;;
codex) echo "$base/.codex/skills" ;;
a3s-code) echo "$base/.a3s/skills" ;; # agent-dir convention; pass --dir for a custom agent
esac
}

place() { # place <skills-root>
dest="$1/a3s-box"
mkdir -p "$dest"
if [ "$COPY" -eq 1 ]; then
cp "$SRC" "$dest/SKILL.md"; echo "copied -> $dest/SKILL.md"
else
ln -sf "$SRC" "$dest/SKILL.md"; echo "linked -> $dest/SKILL.md"
fi
}

[ -n "$DIR" ] && { place "$DIR"; }

case "$AGENTS" in *all*) AGENTS="agents claude codex a3s-code" ;; esac
for a in $AGENTS; do place "$(root_for "$a")"; done

[ -z "$DIR$AGENTS" ] && { echo "nothing to do — pass an agent (agents|claude|codex|a3s-code|all) or --dir" >&2; exit 1; }
echo "done. reload the agent to pick up the skill."
Loading