diff --git a/.devcontainer/aliases.sh b/.devcontainer/aliases.sh index a6a360d..b26b14e 100644 --- a/.devcontainer/aliases.sh +++ b/.devcontainer/aliases.sh @@ -1,4 +1,5 @@ alias ll="ls -lah" alias gs="git status" alias op="opencode" +alias ocs="opencode-sandbox" alias workspaces="cd /workspaces" diff --git a/AGENTS.md b/AGENTS.md index 85c5e53..cab2c5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,7 +63,7 @@ bash scripts/recovery/validate-recovery.sh # verify workstation is in goo | `DB_PATH` | `/workspaces/opencode-workbench/.saga/.tracker.db` | | `DEEPSEEK_API_KEY` | primary model: `deepseek/deepseek-v4-pro` | | `OPENCODE_API_KEY` | OpenCode Zen + Console Go providers | -| `SIMONPLMAK_CLOUD_PAT` | GitHub MCP (binary) + shadcn MCP + `gh` auth | +| `SIMONMAK_ASCENT_PAT` | GitHub MCP (binary) + shadcn MCP + `gh` auth | | `NPM_TOKEN` | GitHub Packages (`@simonmak-ascent`) — same PAT | | `VERCEL_ACCESS_TOKEN`, `PERPLEXITY_API_KEY`, `BRAVE_API_KEY`, `SENTRY_AUTH_TOKEN`, `MOONSHOT_API_KEY`, `SURREAL_*`, `ALIBABA_CLOUD_*`, `AZURE_*`, `STRIPE_SECRET_KEY` | respective MCP servers/providers | @@ -83,6 +83,7 @@ bash .devcontainer/start-browserless.sh - **Toolchain**: `node` 22, `corepack`/`pnpm`, `uv` + Python 3.11, `rsync`, `gcc-c++` - **CLI**: `gh`, `vercel`, `opencode` (aliased as `op`), `opencode serve` - **Playwright**: chromium (headless shell) + RHEL system libs +- **Sandbox**: `opencode-sandbox` (bubblewrap) — run OpenCode with a read-only system, a writable workspace, and no access to `~/.ssh` or `~/.env.workbench`; installed by the optional `sandbox` component (`scripts/sandbox/opencode-sandbox.sh`) ## OpenCode diff --git a/README.md b/README.md index f1a2ba2..5f9cf88 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This repository is the **single source of truth** for a complete cloud developme - Operational prompts (disaster recovery, backup, security) - Recovery procedures (playbook, checklist, gap analysis) - Security governance (secret management, threat model) +- OpenCode sandbox (bubblewrap) — run the agent with a read-only system and no secret mounts - **`opencode-workbench`** — an MCP server that installs this profile on any Linux box **No important knowledge exists only in human memory.** @@ -181,7 +182,7 @@ All secrets stored in the build box Secrets. Never in repository files. |---|--------|---------| | 1 | `DEEPSEEK_API_KEY` | OpenCode (primary model) | | 2 | `OPENCODE_API_KEY` | OpenCode Zen / Console Go providers | -| 3 | `SIMONPLMAK_CLOUD_PAT` | GitHub MCP, shadcn MCP, `gh` | +| 3 | `SIMONMAK_ASCENT_PAT` | GitHub MCP, shadcn MCP, `gh` | | 4 | `PERPLEXITY_API_KEY` | Perplexity MCP | | 5 | `BRAVE_API_KEY` | Brave Search MCP | | 6 | `BROWSERLESS_TOKEN` | Browserless MCP + container | diff --git a/connector.json b/connector.json index 4602665..878d25d 100644 --- a/connector.json +++ b/connector.json @@ -3,7 +3,7 @@ "name": "opencode-workbench", "title": "OpenCode Workbench", "description": "Clones the OpenCode workbench installation onto Linux machines, locally or over SSH.", - "version": "3.0.0", + "version": "3.1.0", "transport": { "type": "stdio", "command": "npx", diff --git a/docs/CHANGELOG_WORKBENCH.md b/docs/CHANGELOG_WORKBENCH.md index 0e33369..9c10067 100644 --- a/docs/CHANGELOG_WORKBENCH.md +++ b/docs/CHANGELOG_WORKBENCH.md @@ -3,6 +3,16 @@ > All notable changes to the workbench configuration. > Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +## [2026-10-03] — v3.1.0 + +### Added +- **`sandbox` component** — optional [bubblewrap](https://github.com/containers/bubblewrap) + sandbox for the OpenCode CLI. Installs `~/.local/bin/opencode-sandbox` (alias `ocs`), + which runs `opencode` with a read-only system, writable workspace + OpenCode state only, + and no access to `~/.ssh`, cloud credentials or `~/.env.workbench`. Wrapper: + `scripts/sandbox/opencode-sandbox.sh`. +- Documented the sandbox under `docs/security-model.md` (Layer 2b) and `mcp-server/README.md`. + ## [2026-10-03] — v3.0.0 ### Breaking @@ -153,7 +163,7 @@ ### Changed - **Active model**: `kimi-for-coding/k3` → `deepseek/deepseek-v4-pro` -- **devcontainer.json `remoteEnv`**: Added 6 missing env vars (BROWSERLESS_TOKEN, PERPLEXITY_API_KEY, BRAVE_API_KEY, FIGMA_TOKEN, SENTRY_AUTH_TOKEN, SIMONPLMAK_CLOUD_PAT, DEEPSEEK_API_KEY, DB_PATH) +- **devcontainer.json `remoteEnv`**: Added 6 missing env vars (BROWSERLESS_TOKEN, PERPLEXITY_API_KEY, BRAVE_API_KEY, FIGMA_TOKEN, SENTRY_AUTH_TOKEN, SIMONMAK_ASCENT_PAT, DEEPSEEK_API_KEY, DB_PATH) - **Figma MCP**: Fixed env var name from `FIGMA_TOKEN` → `FIGMA_API_KEY` - Runtime sync: `~/.config/opencode/opencode.json` synced from repo - Updated `docs/environment-inventory.md` — reflected model change, added new vars diff --git a/docs/security-model.md b/docs/security-model.md index 6cbfebb..83534ad 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -40,7 +40,7 @@ | Tier | Description | Examples | Storage | |------|-------------|----------|---------| | **Tier 0 — Platform** | GitHub-injected tokens | `GITHUB_TOKEN`, `GH_TOKEN` | Automatic | -| **Tier 1 — Critical** | Required for core function | `DEEPSEEK_API_KEY`, `SIMONPLMAK_CLOUD_PAT` | build box Secrets | +| **Tier 1 — Critical** | Required for core function | `DEEPSEEK_API_KEY`, `SIMONMAK_ASCENT_PAT` | build box Secrets | | **Tier 2 — Operational** | Required for MCP servers | `BRAVE_API_KEY`, `SENTRY_AUTH_TOKEN`, `FIGMA_TOKEN`, `PERPLEXITY_API_KEY`, `BROWSERLESS_TOKEN` | build box Secrets | | **Tier 3 — Optional** | Nice to have, not critical | `OPENROUTER_API_KEY`, `GOOGLE_API_KEY`, `KIMI_API_KEY` | build box Secrets | | **Tier 4 — Non-Secret** | Configuration only | `DATABASE_URL`, `DB_PATH`, `BROWSERLESS_HOST` | `devcontainer.json` | @@ -57,6 +57,23 @@ - Each MCP server receives only its required environment variables - `opencode.json` uses named `{env:VAR}` references (not blanket env passthrough) +### Layer 2b: OpenCode Sandboxing +The optional `sandbox` component installs **bubblewrap** and `~/.local/bin/opencode-sandbox`, +a wrapper that runs the OpenCode CLI (`opencode-sandbox [args...]`, alias `ocs`) inside a +bubblewrap namespace: + +- **Read-only system** — `/usr`, `/etc`, `/bin`, `/lib`, `/opt`, `/run` are mounted `--ro-bind`. +- **Writable, scoped state** — only the workspace and OpenCode's own directories + (`~/.config/opencode`, `~/.local/share/opencode`, `~/.cache/opencode`, + `~/.local/state/opencode`) are writable. +- **Secrets excluded** — `~/.ssh`, `~/.env.workbench`, `~/.aws`, `~/.config/gh` and + other home paths are **not** mounted, so the agent cannot read them. +- **Network allowed by default** (remote model APIs); set `OPENCODE_SANDBOX_NO_NET=1` + for `--unshare-net` isolation. + +Requires kernel support for unprivileged user namespaces; where unavailable the installer +warns and the component fails closed (run `opencode` directly only if you accept that). + ### Layer 3: Documentation Discipline - Secrets documented by name and purpose only - Never record values, prefixes, or lengths in docs diff --git a/mcp-server/README.md b/mcp-server/README.md index 7a986ac..6e06e1c 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -58,7 +58,7 @@ Every tool takes a `target`: - **required** — `git`, `curl`, `node` (>=20), `opencode` - **core** — `pnpm`, `uv`, `gh`, `npm-mcps`, `vendored-mcps`, `research-mcps`, `skills`, `plugins`, `playwright-browsers` -- **optional** — `github-mcp`, `docker`, `docker-containers`, `data-tools`, `scientific`, `db-clients` +- **optional** — `github-mcp`, `docker`, `docker-containers`, `data-tools`, `scientific`, `db-clients`, `sandbox` Add-ons (esg-hub, humanity4ai, saga, surrealdb, google-workspace, google-search, ms-365, stripe, alibaba-cloud-ops, designlang, difflens) are omitted from the rendered @@ -70,6 +70,11 @@ The connector **never reads, transmits, or writes secret values**. It writes an `~/.env.workbench` template (mode `600`) listing the `{env:VAR}` names the rendered config references. Fill them on the target. +The optional `sandbox` component installs [bubblewrap](https://github.com/containers/bubblewrap) +and an `opencode-sandbox` wrapper that runs the OpenCode CLI with a read-only system and +only the workspace + OpenCode's own state directories mounted — `~/.ssh`, cloud +credentials and `~/.env.workbench` stay outside the sandbox. + ## Development ```bash diff --git a/mcp-server/src/components.ts b/mcp-server/src/components.ts index 3256eae..68f4024 100644 --- a/mcp-server/src/components.ts +++ b/mcp-server/src/components.ts @@ -330,6 +330,25 @@ pip3 install --break-system-packages -q numpy scipy pandas matplotlib sympy || w install: `pkg_install postgresql-client || true pipx install pgcli 2>/dev/null || true`, }, + { + id: "sandbox", + title: "OpenCode sandbox (bubblewrap)", + tier: "optional", + description: + "Runs the OpenCode CLI in a bubblewrap sandbox: read-only system, writable workspace, and no access to ~/.ssh, cloud credentials or ~/.env.workbench.", + preview: "install bubblewrap and ~/.local/bin/opencode-sandbox", + detect: `command -v bwrap >/dev/null 2>&1 && [ -x "$WB_HOME/.local/bin/opencode-sandbox" ]`, + install: ` +command -v bwrap >/dev/null 2>&1 || pkg_install bubblewrap || true +command -v bwrap >/dev/null 2>&1 || { warn "bubblewrap unavailable (kernel may block unprivileged user namespaces)"; exit 1; } +if [ -z "\${WB_REPO:-}" ] || [ ! -f "$WB_REPO/scripts/sandbox/opencode-sandbox.sh" ]; then + warn "sandbox script not found under WB_REPO"; exit 1 +fi +mkdir -p "$WB_HOME/.local/bin" +install -m 0755 "$WB_REPO/scripts/sandbox/opencode-sandbox.sh" "$WB_HOME/.local/bin/opencode-sandbox" +ok "opencode-sandbox -> ~/.local/bin/opencode-sandbox" +`, + }, ]; export function componentById(id: string): Component | undefined { @@ -361,6 +380,7 @@ ${CORE_NPM_MCPS.map((p) => ` npm rm -g --silent ${p} >/dev/null 2>&1 || true`). "docker-containers": `command -v docker >/dev/null 2>&1 || { warn "docker missing"; exit 1; } docker rm -f pg-memory browserless >/dev/null 2>&1 || true`, "playwright-browsers": `rm -rf "$WB_HOME/.cache/ms-playwright"`, + sandbox: `rm -f "$WB_HOME/.local/bin/opencode-sandbox"`, }; /** Uninstall script for a component id, or undefined when removal is manual. */ diff --git a/mcp-server/test/component-actions.test.ts b/mcp-server/test/component-actions.test.ts index 797d7c9..d23a041 100644 --- a/mcp-server/test/component-actions.test.ts +++ b/mcp-server/test/component-actions.test.ts @@ -23,6 +23,7 @@ describe("component uninstall coverage", () => { "plugins", "docker-containers", "playwright-browsers", + "sandbox", ]) { expect(typeof uninstallScript(id), `missing uninstall for ${id}`).toBe("string"); } diff --git a/mcp-server/test/server.test.ts b/mcp-server/test/server.test.ts index 7471b2b..0dc7c31 100644 --- a/mcp-server/test/server.test.ts +++ b/mcp-server/test/server.test.ts @@ -57,6 +57,7 @@ describe("opencode-workbench server", () => { optionalMcp: string[]; }; expect(info.components.some((c) => c.id === "opencode" && c.tier === "required")).toBe(true); + expect(info.components.some((c) => c.id === "sandbox" && c.tier === "optional")).toBe(true); expect(info.optionalMcp).toContain("saga"); await client.close(); }); diff --git a/package.json b/package.json index 3590d86..729321e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@simonmak-ascent/opencode-workbench", "mcpName": "io.github.simonmak-ascent/opencode-workbench", - "version": "3.0.0", + "version": "3.1.0", "description": "MCP server and connector that clones the OpenCode workbench configuration onto Linux machines (locally or over SSH).", "license": "MIT", "type": "module", diff --git a/scripts/sandbox/opencode-sandbox.sh b/scripts/sandbox/opencode-sandbox.sh new file mode 100755 index 0000000..166fa55 --- /dev/null +++ b/scripts/sandbox/opencode-sandbox.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# +# opencode-sandbox — run the OpenCode CLI inside a bubblewrap (bwrap) sandbox. +# +# Security goal: the agent process gets a read-only system, a writable copy of +# its workspace and OpenCode's own state directories, and NOTHING ELSE from your +# home. Your SSH keys (~/.ssh), cloud credentials (~/.aws, ~/.config/gcloud), +# GitHub tokens (~/.config/gh) and the secrets file ~/.env.workbench are not +# mounted, so a compromised or misbehaving agent cannot read them. +# +# Usage: +# opencode-sandbox [args...] # same args as `opencode` +# opencode-sandbox run "..." # e.g. `opencode run` in the sandbox +# +# Environment: +# OPENCODE_SANDBOX_CWD=