From 2127ea4af31243f7c6a693d122f7248cc8c376eb Mon Sep 17 00:00:00 2001 From: Roy Lin Date: Mon, 29 Jun 2026 08:57:33 +0800 Subject: [PATCH] docs(skills): ship the a3s-box agent skill + cross-tool installer One Agent Skills SKILL.md that teaches a coding agent to drive the a3s-box CLI, reused across Claude Code / Codex / Gemini CLI / Cursor / Amp / OpenCode / Zed / a3s-code (shared SKILL.md format). Adds integrations/skills/ (SKILL.md, install.sh, README) and a complete Install+Use section in the box README. Docs-only. --- README.md | 59 +++++++++++++ integrations/skills/README.md | 57 +++++++++++++ integrations/skills/a3s-box/SKILL.md | 121 +++++++++++++++++++++++++++ integrations/skills/install.sh | 70 ++++++++++++++++ 4 files changed, 307 insertions(+) create mode 100644 integrations/skills/README.md create mode 100644 integrations/skills/a3s-box/SKILL.md create mode 100755 integrations/skills/install.sh diff --git a/README.md b/README.md index 8d5179e6..6cee809d 100644 --- a/README.md +++ b/README.md @@ -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" /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. diff --git a/integrations/skills/README.md b/integrations/skills/README.md new file mode 100644 index 00000000..46efa06b --- /dev/null +++ b/integrations/skills/README.md @@ -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 (`/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" /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*)`. diff --git a/integrations/skills/a3s-box/SKILL.md b/integrations/skills/a3s-box/SKILL.md new file mode 100644 index 00000000..98d73221 --- /dev/null +++ b/integrations/skills/a3s-box/SKILL.md @@ -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 -- ` (required); `a3s-box run [-- ]` + (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 -- `. +- **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 ` 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 `. +- **Output streams:** `run -d` prints a human `Creating box ()...` + 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 … `-- `) | missing `--` → `exec -- ` | +| `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 --name restored # name it → no ps scraping +a3s-box ps -a --filter name=restored && a3s-box start restored +``` + +## Finding exact flags & versions + +`a3s-box --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 --help`. diff --git a/integrations/skills/install.sh b/integrations/skills/install.sh new file mode 100755 index 00000000..71b3ed7a --- /dev/null +++ b/integrations/skills/install.sh @@ -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] ... +# ./install.sh --dir # 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 + 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."