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.
| 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/incus-setup.sh— run on the Incus host, never inside the VM. Creates/starts the VM (defaultdevbox), mounts the workspace, pushes and runssetup-vm.sh, then syncs the opencode config. It also pushes the git config (config/gitconfig, orconfig/gitconfig.exampleif 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~/.gitconfigfor both root and theagentuser (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.envsecrets as Incus instance env vars (environment.*, DB-side on the host, never on the VM disk), pushesopencode.json+AGENTS.mdverbatim (opencode resolves{env:...}natively), pushes the sharedplugins/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-rootagentuser (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 fromservices/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 thesecurity-checkCLI (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]
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/signingkeyThe 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:
cp .env.example .envand fill in the tokens (BM, Context7, OpenCode).- 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 forincus exec; the config is pushed verbatim and opencode resolves the{env:...}placeholders at startup. - 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).
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.
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