| name | k8e-sandbox |
| description | Execute a goal inside the K8E sandbox (gVisor/Kata/Firecracker). Use when the user runs /k8e-sandbox <goal>, $k8e-sandbox <goal>, or asks to run/execute/test code safely off the host. |
| argument-hint | <goal> |
| user-invocable | true |
/k8e-sandbox
Treat this as the k8e-sandbox skill command.
Invocation (same skill, different harness prefixes):
- Claude Code:
/k8e-sandbox <goal>
- Codex:
$k8e-sandbox <goal> (or pick from /skills)
- Pi:
/skill:k8e-sandbox <goal> (or /k8e-sandbox when skill commands are enabled)
- dsh (DeepSeek Harness): the model loads this skill via the
skill tool (catalog name k8e-sandbox), or the user names it directly in chat — see dsh execution path below
Goal from invocation arguments:
$ARGUMENTS
If $ARGUMENTS is empty and no goal is otherwise provided, ask the user for a sandbox goal and stop (do not invent work).
dsh (DeepSeek Harness) execution path
In dsh, decide your execution mode by checking the current session's tool
list first — everything else in this skill branches on it.
A. Plugin mounted — k8e_sandbox_session_status IS in your tool list
The dsh-k8e-sandbox plugin replaced the harness's execution seams:
bash (subprocess seam) → runs inside the sandbox pod
read / write / edit / dir listings (fs seam) → sandbox /workspace
k8e_sandbox_* tools → session lifecycle, expose, egress allowlist
Rules:
- Do NOT run
k8e-sandbox-cli here: it is not present inside the
sandbox, and invoking it from the sandboxed bash would recursively dial
the gateway from inside the pod. Use the tools + sandboxed seams instead.
- Prefer plain
bash for commands — it lands in the sandbox. Use
k8e_sandbox_exec when you want structured stdout/stderr/exitCode, and
k8e_sandbox_run_background + k8e_sandbox_poll for long/streaming tasks.
- First action in a session:
k8e_sandbox_session_status — it lazily
creates the session shared by fs/subprocess/exec, and reports
available, sessionId, tenantId.
Tool reference (exact argument shapes — do not guess):
| Tool | Arguments | Returns |
|---|
k8e_sandbox_session_status | {} | available, sessionId, tenantId, error |
k8e_sandbox_session_destroy | {} — releases the pod (idempotent) | destroyed |
k8e_sandbox_exec | {code: string (required), lang?: "bash"|"python"|"node"|"ts", timeout?: number} | stdout, stderr, exitCode, durationMs, truncated |
k8e_sandbox_run_background | {code: string (required), lang?: …} | runId, sessionId, status |
k8e_sandbox_poll | {runId: string (required)} | runId, status, stdout, stderr, exitCode, durationMs |
k8e_sandbox_expose | {port: number (required), host?: string} | url, port |
k8e_sandbox_unexpose | {port: number (required)} | ok, port |
k8e_sandbox_allow_hosts | {hosts: string[] (required)} — full replacement list; [] clears (falls back to cluster defaults) | hosts[] |
Service exposure in dsh: after starting a long-running service with
k8e_sandbox_run_background, call k8e_sandbox_expose {port: 8080} and hand
the returned URL to the user — same gateway-proxied URL the CLI's expose
prints. Teardown with k8e_sandbox_unexpose {port: 8080}.
Session, connection, and mTLS are owned by the plugin: it resolves the
gateway from config → env → ~/.k8e/sandbox/profiles.yaml (KIP-17) and
reuses one persistent gRPC connection. If a tool errors with gateway
unreachable / mTLS / deadline, tell the user to run k8e-sandbox-cli connect
(local) or k8e-sandbox-cli connect --endpoint <host>:50051 --apikey <key>
(remote) outside dsh, then restart the dsh session.
B. Plugin NOT mounted — k8e_sandbox_* tools are NOT in your tool list
The plugin bundle is not installed for this dsh profile. dsh's bash still
runs on the HOST here (no seam replacement), so the CLI-first flow below
works normally: execute everything via k8e-sandbox-cli run ... exactly as
the CLI examples describe. Do not pretend the k8e_sandbox_* tools exist —
calling a nonexistent tool errors.
To enable the full plugin experience, ask the user to install the bundle once
(from a k8e checkout), then restart dsh:
dsh plugin --profile <name> add <k8e>/plugins/deepseek-harness/packages/dsh-k8e-sandbox-bundle
dsh --profile <name> # restart the session
dsh error quick reference
| Symptom | Cause | Fix |
|---|
Tool call fails "not found" / unknown tool k8e_sandbox_* | plugin bundle not mounted | use section B (CLI-first); ask user to install the bundle |
| Tool errors "gateway unreachable" / mTLS / deadline | gateway down or missing credentials | k8e-sandbox-cli connect (or with --endpoint/--apikey) outside dsh, restart dsh |
bash/read error with connection refused | session pod not ready | k8e_sandbox_session_status; wait and retry |
k8e_sandbox_expose returns 503 "no pod IP" | old server: session status.podIP empty | upgrade k8e server (rc7+), or k8e-sandbox-cli connect then retry |
The CLI-first flow below (k8e-sandbox-cli run ...) is for harnesses where the sandbox is not mounted (Claude Code / Codex / Pi / dsh without the plugin).
Binary naming (read this first)
The downloaded file name carries a platform suffix — pick the one for the user's machine:
| Platform | Download name |
|---|
| Linux amd64 | k8e-sandbox-cli-linux-amd64 |
| Linux arm64 | k8e-sandbox-cli-linux-arm64 |
| macOS amd64 | k8e-sandbox-cli-darwin-amd64 |
| macOS arm64 | k8e-sandbox-cli-darwin-arm64 |
| Windows amd64 | k8e-sandbox-cli-windows-amd64.exe |
It is the same binary this skill invokes as k8e-sandbox-cli — just under the platform-suffixed name. To make the plain name work without renaming, create a symlink (do not rename the file):
curl -sLO https://github.com/xiaods/k8e/releases/latest/download/k8e-sandbox-cli-linux-amd64
chmod +x k8e-sandbox-cli-linux-amd64
ln -s k8e-sandbox-cli-linux-amd64 k8e-sandbox-cli
./k8e-sandbox-cli ... connect
(Windows: use mklink k8e-sandbox-cli.exe k8e-sandbox-cli-windows-amd64.exe in cmd.)
From then on, this skill and all examples use the plain name k8e-sandbox-cli — same binary.
If you only see a platform-suffixed name in the user's environment (no symlink yet), use that file directly: ./k8e-sandbox-cli-linux-amd64 status etc. All spellings are interchangeable; never tell the user they are missing a second binary.
Hard rules
- All code and shell execution goes through
k8e-sandbox-cli — never run python3, node, pip, npm, curl, compilers, or tests on the host for this goal.
- Prefer auto session mode:
k8e-sandbox-cli run "..." (creates/reuses session).
- Parse JSON with
jq unless --raw is used.
- If the gateway is unreachable, tell the user to run
k8e-sandbox-cli connect (local) or k8e-sandbox-cli connect --endpoint <host>:50051 --apikey <key> (remote). Multi-cluster: --profile <name> / ~/.k8e/sandbox/profiles.yaml (KIP-17).
Auth & multi-profile (KIP-14 / KIP-17 / #538)
Do not confuse these files:
| Path | Who | What |
|---|
/etc/k8e/config.yaml | k8e server/agent | Daemon flags only |
~/.k8e/sandbox/profiles.yaml | k8e-sandbox-cli | Named gateways / cert dirs |
~/.k8e/sandbox/config.json | k8e-sandbox-cli | Last connect stamp |
mTLS bootstrap: first remote connect/login uses an API key once; CLI stores ca.crt + client.crt + client.key (private key never leaves the machine). Client certs last 90 days and auto-renew when <30 days remain. API keys default to 30-day TTL (k8e sandbox-apikey create name, override with --ttl 90d|never).
Profiles (~/.k8e/sandbox/profiles.yaml, override with K8E_SANDBOX_CONFIG):
version: 1
current_profile: default
profiles:
default:
endpoint: 10.0.0.1:50051
prod:
endpoint: sandbox.prod.example:50051
cert_dir: ~/.k8e/sandbox-prod
device_name: laptop-prod
k8e-sandbox-cli --profile prod connect --apikey k8e-...
k8e-sandbox-cli --profile prod run 'echo hi'
Priority: flags → env (K8E_SANDBOX_ENDPOINT / APIKEY / CERT_DIR / PROFILE) → profile → defaults. Cert dir: K8E_SANDBOX_CERT_DIR → ~/.k8e/sandbox.
Procedure (always)
1. Pre-flight
command -v k8e-sandbox-cli >/dev/null || { echo "k8e-sandbox-cli not on PATH; run connect again"; exit 1; }
k8e-sandbox-cli status
Require "available": true. If not available, stop and instruct the user to connect.
2. Plan
Decompose $ARGUMENTS into sandbox-safe steps (install deps → write files → run code → read outputs).
3. Execute (examples)
k8e-sandbox-cli run 'echo hello'
k8e-sandbox-cli run "print(1+1)" --lang python
k8e-sandbox-cli run 'pip install pandas' --lang bash
k8e-sandbox-cli run 'sleep 30; echo done' --background
k8e-sandbox-cli poll <run-id>
k8e-sandbox-cli run 'echo hi' --tenant my-project
k8e-sandbox-cli subagent <parent-sid>
k8e-sandbox-cli run "python3 -m http.server 8080 --bind 127.0.0.1" --background
k8e-sandbox-cli expose 8080
Useful commands: run, write, read, list, create, get, sessions, destroy, status, log, events, ps, poll, subagent, confirm, approve, snapshot, benchmark, catalog, expose, unexpose, exposed, allow-hosts.
4. Report
Show stdout/stderr and exit codes from the CLI JSON. Do not claim host-side execution.
One-time setup (if not connected)
k8e-sandbox-cli connect
k8e sandbox-apikey create my-agent
k8e-sandbox-cli connect --endpoint <server-ip>:50051 --apikey k8e-...
connect authenticates (mTLS), verifies the gateway, puts k8e-sandbox-cli on PATH when needed (symlink to ~/.local/bin/k8e-sandbox-cli), and installs this skill into Claude / Codex / Pi / dsh discovery paths (--agent dsh or --agent all; dsh reads it from ~/.dsh/skills or ~/.agents/skills).
Command reference
| Command | Purpose |
|---|
k8e-sandbox-cli --profile <name> … | Use named profile from ~/.k8e/sandbox/profiles.yaml |
k8e-sandbox-cli connect | Local/remote auth + install this skill into agent harnesses |
k8e-sandbox-cli connect --skill-only | Re-install this skill only (no gateway dial) |
k8e-sandbox-cli login | Remote mTLS only (no skill install); optional --device-name |
k8e-sandbox-cli status | Gateway + session probe |
k8e-sandbox-cli run <code> | Exec in sandbox (--lang, --timeout, --raw, --session-id, --tenant, --background, --manifest, --git-repo, --allowed-hosts) |
k8e-sandbox-cli create | Manual session (--runtime, --env, --secret, --allowed-hosts, --manifest, --git-repo) |
k8e-sandbox-cli get <sid> | Session introspection (phase, runtime, env keys) |
k8e-sandbox-cli sessions | List sessions |
k8e-sandbox-cli write/read/list | Workspace files; list --since <ts> for changed-file diff |
k8e-sandbox-cli push <sid> <local> [remote] | Stream a local file INTO the sandbox (chunked 4MiB windows — constant memory, binary-safe, any size; --chunk-mb to tune) |
k8e-sandbox-cli pull <sid> <remote> [local] | Stream a sandbox file OUT to a local path (same chunked transfer) |
k8e-sandbox-cli log <sid> | Replay exec transcript (--offset, --limit, --follow) |
Default run output is JSON: stdout, stderr, exit_code, session_id. Use --raw to stream text.
Service exposure (KIP-24)
When the agent builds a long-running service inside the sandbox (web app,
API server), expose it through the k8e API Gateway so the gateway/other hosts
can reach it — no port-forward, no inbound pod exposure:
k8e-sandbox-cli run "python3 -m http.server 8080 --bind 127.0.0.1" --background
k8e-sandbox-cli expose 8080 # -> {"url":"http://<gateway>/k8e/expose/<sid>/8080/",...}
curl http://<gateway>/k8e/expose/<sid>/8080/ # reachable via the gateway (VPC/LB)
k8e-sandbox-cli exposed # list live exposures
k8e-sandbox-cli unexpose 8080 # tear down
The exposed URL routes: Cilium Gateway API (:80/:443) -> embedded e2b HTTP
server -> reverse proxy to http://<podIP>:<port>. The gateway base is
configured server-side (--sandbox-expose-base-url, default
http://<advertise-hostname>). The CNP is re-applied automatically so only
the gateway/e2b-server can reach the exposed port.
Egress allowlist is freely configurable — when the sandbox needs outbound
access to domains (package registries, tunnel endpoints), update it live:
k8e-sandbox-cli allow-hosts --add pypi.org,registry.npmjs.org
k8e-sandbox-cli allow-hosts --remove pypi.org
k8e-sandbox-cli allow-hosts --clear # fall back to cluster defaults
Session modes
| Mode | How | State |
|---|
| Auto (default) | run without session id | ~/.k8e/sandbox/default/state.json |
| Tenant | --tenant my-project | ~/.k8e/sandbox/{tenant}/state.json |
| Manual | create → run --session-id → destroy | none |
Egress
Default allowed hosts (cluster SandboxMatrix.spec.defaultAllowedHosts): pypi.org, files.pythonhosted.org, registry.npmjs.org, github.com, raw.githubusercontent.com.
- At session creation:
create --allowed-hosts a.com,b.com (or run --allowed-hosts for auto-created sessions).
- Live, any time (KIP-24):
allow-hosts --add a.com,b.com / --remove a.com / --clear (fall back to cluster defaults). Applies immediately via CNP re-apply; in dsh use k8e_sandbox_allow_hosts {hosts: [...]}.
Security red lines
--env is for non-sensitive config only (stored on CRD). Use --secret ENV=secret:key for secrets.
- Never pass host secrets into sandbox flags in chat logs if avoidable.
- Never
sudo via sandbox CLI.
- Destructive sandbox actions require
confirm → approve (human in the loop); don't skip it.
Error quick reference
| Exit | Meaning | Action |
|---|
| 2 | TLS / cert / unreachable | Server reinstalled or CA rotated? Re-run connect --reset-certs --apikey <key> (clears the cached CA and re-bootstraps trust); otherwise check profile cert_dir |
| 1 | Command/session error | Read JSON error; recreate session if gone; re-create API key if TTL expired |
| 8 | ResourceExhausted | Wait or free warm pool capacity |
Your role when this skill is active
Do:
- dsh + plugin mounted (section A): run everything through the sandboxed
bash/read/write seams and the k8e_sandbox_* tools; start long-running
services with k8e_sandbox_run_background, hand the user a reachable URL
via k8e_sandbox_expose.
- Everywhere else (CLI-first flow): execute
$ARGUMENTS entirely via
k8e-sandbox-cli; prefer run; use --lang python for Python; use --raw
for long streams; show real CLI output. When the goal builds a long-running
service (web app, API), start it with run --background and hand the user a
reachable URL via expose <port>.
Don't: run the goal on the host; skip pre-flight; invent successful output
without actually running a tool/CLI; call k8e_sandbox_* tools that are not
in your current tool list (plugin not mounted — use section B instead).