Skip to content

Repository files navigation

sandbox-opencode

Sandbox workspace for the opencode agent. It lives on the Incus host at $HOME/workspace and is mounted into the devbox VM at /workspace (see SETUP.md). Purpose: run a code agent inside a hardware-isolated KVM VM so it only reaches this shared folder — never the host machine or the LAN.

Layout

Path Purpose
SETUP.md Host-side Incus setup (VM, mount, firewall) — targets the host
INSTRUCTIONS.md User scratchpad: drop long/detailed instructions here for the agent; read at session start
AGENTS.md Agent guidance (French chat, scripts execution hosts, global policies)
config/gitconfig.example Template for the VM's ~/.gitconfig; copy to config/gitconfig (gitignored) and fill in your identity
scripts/ VM lifecycle scripts (see below)
services/phoenix/ Host-side Phoenix service (Docker Compose) collecting OpenCode traces; see its README
profiles/<name>/ opencode profiles: opencode.json (with {env:...} placeholders), AGENTS.md, and a skills file listing the skills to deploy
skills/<name>/ Skills catalogue, versioned; each profile picks the ones it needs
plugins/guardrails/ Shared opencode plugin (agent guardrails) pushed into the VM's global plugin dir
tests/ Host-run unit tests for the guardrails rule engine (node --test 'tests/**/*.test.ts')

Scripts — mind the execution host

  • scripts/incus-setup.sh — run on the Incus host, never inside the VM. Creates/starts the VM (default devbox), mounts the workspace, pushes and runs setup-vm.sh, then syncs the opencode config. It also pushes the git config (config/gitconfig, or config/gitconfig.example if absent) into the VM. Usage: bash scripts/incus-setup.sh [instance]
  • scripts/setup-vm.sh — runs inside the VM (pushed by incus-setup.sh). Installs mise + node@24 + opencode via npm, and deploys the pushed git config as ~/.gitconfig for both root and the agent user (falls back to /workspace/config/gitconfig* when run standalone). Safe to re-run, no sudo.
  • scripts/sync-opencode.sh — run on the Incus host. Deploys a profile into the VM at /home/agent/.config/opencode/ (dedicated non-root user): injects the .env secrets as Incus instance env vars (environment.*, DB-side on the host, never on the VM disk), pushes opencode.json + AGENTS.md verbatim (opencode resolves {env:...} natively), pushes the shared plugins/guardrails/ plugin, and installs the profile's skills. Usage: bash scripts/sync-opencode.sh [profile] [instance]
  • scripts/vm-root.sh — run on the Incus host. Opens an interactive root zsh in the VM (admin work only). Usage: bash scripts/vm-root.sh [instance]
  • scripts/vm-opencode.sh — run on the Incus host. Runs opencode in the VM as the non-root agent user (uid 1000, cwd /workspace); extra args go to opencode. Usage: bash scripts/vm-opencode.sh [opencode args...]
  • scripts/sync-tracing.sh — run on the Incus host. Installs/refreshes the Arize OpenCode tracing harness inside the VM (Python reconciler + plugin shim) so sessions export to the Phoenix service from services/phoenix/. Non-secret settings come from .env (injected as Incus env vars); re-runnable after a VM rebuild. Usage: bash scripts/sync-tracing.sh [instance]
  • scripts/sync-security-check.sh — run on the Incus host. Deploys the security-check CLI (sec) into the VM from $HOME/workspace-ia/security-check: installs the wrapper + sec, points reports at $HOME/workspace-ia/.security-reports, grants uid 1000 access to the Docker socket, and pulls the published image. Usage: bash scripts/sync-security-check.sh [instance]

Setup

Follow SETUP.md to install and configure Incus on the host, then run the scripted setup (bash scripts/incus-setup.sh) or the manual steps.

Before that, create the VM's git identity — the real file is gitignored:

cp config/gitconfig.example config/gitconfig   # then edit name/email/signingkey

OpenCode config in the VM

The VM's opencode config comes from a profile in profiles/<name>/ (opencode.json + AGENTS.md + a skills list). Secrets are kept out of git and out of the VM disk:

  1. cp .env.example .env and fill in the tokens (BM, Context7, OpenCode).
  2. Run bash scripts/sync-opencode.sh (host) — secrets are injected as Incus instance env vars (environment.*), stored in the Incus DB on the host, applied at VM boot and for incus exec; the config is pushed verbatim and opencode resolves the {env:...} placeholders at startup.
  3. Restart opencode inside the VM — config is loaded at startup, not hot-reloaded.

incus-setup.sh calls the sync automatically after installing the VM tooling. Skills listed in the profile are copied from skills/<name>/ into the VM's global skills dir; catalogue skills dropped from the profile list are pruned.

Excluded from the VM config by design: the local codebase-memory-mcp MCP, custom agents, the ponytail plugin, and auth.json (VM model auth must be done in-VM via opencode auth login — unless OPENCODE_KEY is set, which configures the opencode/opencode-go providers via env vars).

Security check in the VM

The VM ships the security-check CLI (alias sec): Gitleaks, OpenGrep, Trivy, Checkov and OSV-Scanner run from the published ghcr.io/erikaouizerate/security-check:latest image, with the target mounted read-only.

sec [secrets|sast|iac|sca|all] [target]

Reports (SARIF) land outside the audited project, in /workspace/.security-reports/<project>/ — i.e. $HOME/workspace-ia/.security-reports/<project>/ on the host. Run it as the agent user (the vm-agent.sh default); as root the reports would be root-owned on the host share. secrets needs a Git repository.

The tool source lives in its own project (~/workspace-ia/security-check); scripts/sync-security-check.sh synchronizes it into the VM.

Troubleshooting — VM has no IPv4 egress (fixed 2026-09-10)

Symptom: everything in the VM times out over IPv4 (github, MCP servers), IPv6 partly works, and sync-opencode.sh hangs on ==> Verifying in VM because opencode re-fetches its git plugin at every run.

Cause: Docker sets iptables -P FORWARD DROP on the host. Forwarded traffic incusbr0 → wlo1 matches none of the chain's jumps (DOCKER-USER, DOCKER-FORWARD, ts-forward) and falls through to the DROP. IPv4 only, hence the misleading "only IPv6 works". Not a firewalld or Incus problem — Incus already masquerades v4 and v6 in pstrt.incusbr0.

Fix on the host (official Docker ≥ 28 knob; on Docker ≥ 28 unpublished container ports stay protected regardless of the FORWARD policy):

echo '{ "ip-forward-no-drop": true }' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
sudo iptables -P FORWARD ACCEPT   # clear the DROP left by the previous start
incus exec devbox -- curl -4 -sI --max-time 8 https://github.com -o /dev/null -w '%{http_code}\n'  # 200

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages