The lab is designed to test security- and state-sensitive plugins without exposing the host or any real data. It does not claim to be a hard sandbox.
| Control | Setting |
|---|---|
| Networking | network_mode: "none" — no DNS, no outbound, loopback only |
| Privileges | cap_drop: ["ALL"], security_opt: ["no-new-privileges:true"] |
| Filesystem | read_only: true rootfs; scratch via tmpfs (/lab/{home,xdg,work}, /tmp, /run) |
| User | runs as the invoking host uid/gid (non-root) |
| Privileged mode | never used |
| Docker socket | never mounted |
| Host networking | never used |
| Ports | none published; the server binds 127.0.0.1 inside the container |
| Restart | restart: "no" |
| Limits | mem_limit, cpus, pids_limit |
- No mount of any development repository into a container. Plugin sources are
copied into
staging/at build time; only built artifacts reach the image. - Only
./reportsis bind-mounted into the container, and only for output. - A selected manifest file may be mounted read-only over its baked path so edits are never stale.
- All test state (databases, shadow repos, audit logs, fixtures) lives on tmpfs and is discarded with the container.
scripts/stage.mjs excludes node_modules, .git, .github, build output,
coverage, images, logs, .opencode, and every .env*, then aborts if a
credential-like file (.env*, auth.json, credentials*, *.pem, id_rsa*)
is found in the staged tree. .dockerignore additionally excludes .env*, key
material, .ssh, .aws, .config/opencode, .opencode and .lab.
- The lab never reads credentials from an existing OpenCode installation
(
~/.config/opencode,auth.json,~/.local/share/opencode). - The optional online tier reads a dedicated test credential only from
.env(see.env.example). The value is passed as an environment variable to the singlelab-onlinecontainer and is never logged or written to reports. - All functional tests use clearly synthetic, non-functional placeholders.
- Dependency installation runs with
--ignore-scriptsby default. A plugin that needs lifecycle scripts must setinstall.scripts: truein the manifest — an explicit, documented supply-chain decision. - A
source.type: npmplugin is never fetched unlesssource.allowRemoteistrue; the default refuses. - Building a plugin source inevitably runs its build, which may execute arbitrary code. Review untrusted plugins before pointing the lab at them.
Host scripts quote all arguments and pass manifest-derived values through arrays
(mapfile), not unquoted interpolation. The container runs the harness with
fixed arguments. No manifest field is passed to eval except the manifest's
opencode version, which is read into OPENCODE_VERSION/PLUGIN_SDK_VERSION
and constrained by the manifest schema's version pattern.
make resetremoves only this compose project's containers and generated reports (keepingreports/historical/).make cleanadditionally removes the lab image,staging/and generated reports. It never touches other Docker resources, any repository, or the committed historical evidence.
- Rootless Docker reduces risk but is not an absolute boundary against malicious plugin code. A container escape or a resource-exhaustion attack is still possible in principle.
- Outbound redaction is verified on the supported HTTP model-request path using a local mock provider. Provider-native WebSocket traffic is not covered.
- Hook registration is observed indirectly (tool registration + the model path); OpenCode exposes no API to enumerate hooks.
- A plugin’s own persistence is exercised only through the generic write-boundary check, not a plugin-specific storage audit.