Private /tmp, no display, git over SSH and netlock check fixes

- /tmp: the agent gets its own (<state>/tmp, files older than 7 days are
  removed on start) — the host's /tmp holds sockets that lead out of the
  sandbox (tmux, X11, browsers' singleton sockets, other tools).
  OCSB_TMP=shared keeps the old behaviour. /tmp is mounted before the
  project and state binds so they stay visible if they live under /tmp
- DISPLAY, WAYLAND_DISPLAY, XAUTHORITY are empty in the sandbox and
  ~/.Xauthority is hidden; browsers must run headless
- GIT_SSH_COMMAND uses -F ~/.ssh/config: in the user namespace root-owned
  system ssh configs look owned by nobody and ssh refuses them
- netlock self-test checks that the proxy opens the tunnel, not that a
  remote site answers; failed self-tests now say so instead of exiting
  silently
- doctor: no false error for opencode outside a project
- skill and READMEs: worktrees only for code changes, private /tmp,
  headless browsers, troubleshooting entries
This commit is contained in:
2026-10-30 01:09:53 -07:00
parent 5193dbe573
commit 6dec88641a
5 changed files with 48 additions and 14 deletions
+12 -4
View File
@@ -124,6 +124,7 @@ files are read, so you can use it in values. Relative paths are relative to the
| `OCSB_PLUGINS` | skill-compact-resurrect | plugin files from `plugins/` (or absolute paths) | | `OCSB_PLUGINS` | skill-compact-resurrect | plugin files from `plugins/` (or absolute paths) |
| `OCSB_PANEL` | `1` | "Tokens" panel in the web UI | | `OCSB_PANEL` | `1` | "Tokens" panel in the web UI |
| `OCSB_NOTIFICATIONS` | `1` | desktop notifications through a filtered D-Bus proxy | | `OCSB_NOTIFICATIONS` | `1` | desktop notifications through a filtered D-Bus proxy |
| `OCSB_TMP` | `private` | `private`: the agent's own `/tmp` in the state directory; `shared`: the host's `/tmp` |
| `OCSB_SANDBOX` | `1` | `0` = filter and netlock only, no bubblewrap | | `OCSB_SANDBOX` | `1` | `0` = filter and netlock only, no bubblewrap |
| `OCSB_WORKTREES_DIR` | `../<project>-worktrees` | where worktrees go | | `OCSB_WORKTREES_DIR` | `../<project>-worktrees` | where worktrees go |
| `OCSB_WT_PREFIX` | — | worktree directory = `<prefix><slug>` | | `OCSB_WT_PREFIX` | — | worktree directory = `<prefix><slug>` |
@@ -226,7 +227,11 @@ browser → :4196 (context panel inside OpenCode) → :4197
Inside the sandbox: Inside the sandbox:
- **Writable:** the project, the worktrees directory, the state directory, `OCSB_RW`, - **Writable:** the project, the worktrees directory, the state directory, `OCSB_RW`,
`OCSB_RW_HOME`, `/tmp`. Everything else is read-only. `OCSB_RW_HOME` and a private `/tmp`. Everything else is read-only.
- **Private `/tmp`:** the host's `/tmp` holds sockets that lead out of a sandbox (tmux, X11,
browsers' singleton sockets, other tools), so the agent gets its own, stored in
`<state>/tmp` (files older than 7 days are removed on start). Look there for its screenshots
and logs. Docker mounts of `/tmp` paths would point at the host's `/tmp` — mount project paths.
- **Hidden:** `OCSB_HIDE_HOME`, `OCSB_HIDE`, private keys in `~/.ssh` (`config`, `known_hosts` - **Hidden:** `OCSB_HIDE_HOME`, `OCSB_HIDE`, private keys in `~/.ssh` (`config`, `known_hosts`
and `*.pub` stay visible; the host's ssh-agent signs). and `*.pub` stay visible; the host's ssh-agent signs).
- **Network:** `HTTP(S)_PROXY` point to the filter. `GIT_SSH_COMMAND` sends ssh through it with - **Network:** `HTTP(S)_PROXY` point to the filter. `GIT_SSH_COMMAND` sends ssh through it with
@@ -238,8 +243,9 @@ Inside the sandbox:
- **Session bus:** replaced by `xdg-dbus-proxy` that talks only to - **Session bus:** replaced by `xdg-dbus-proxy` that talks only to
`org.freedesktop.Notifications`; the real bus would let the agent start processes outside `org.freedesktop.Notifications`; the real bus would let the agent start processes outside
the sandbox with `systemd-run --user`. the sandbox with `systemd-run --user`.
- **Browser:** `xdg-open` is disabled for the agent; the UI is opened by `run` from your - **No display:** `DISPLAY`, `WAYLAND_DISPLAY` and `XAUTHORITY` are empty and the X cookie is
normal session. hidden, so GUI programs cannot reach your desktop; run browsers (Playwright MCP, Chrome)
headless. `xdg-open` is disabled; the UI is opened by `run` from your normal session.
- **No silent fallback:** with `OCSB_SANDBOX=1` and no working bubblewrap, `run` refuses to - **No silent fallback:** with `OCSB_SANDBOX=1` and no working bubblewrap, `run` refuses to
start. start.
@@ -260,7 +266,7 @@ place, requests to production — not to hold a determined attacker.
- **Allowed hosts are a channel.** Data can leave through any host the rules allow. - **Allowed hosts are a channel.** Data can leave through any host the rules allow.
- **The project is writable.** Mistakes inside it are real; cc-safety-net blocks the common - **The project is writable.** Mistakes inside it are real; cc-safety-net blocks the common
destructive commands, git keeps history — commit often. destructive commands, git keeps history — commit often.
- `/tmp` is shared with the host. - With `OCSB_TMP=shared` the host's `/tmp` and its sockets are reachable.
## Troubleshooting ## Troubleshooting
@@ -275,6 +281,8 @@ place, requests to production — not to hold a determined attacker.
| `bubblewrap cannot create a sandbox` | unprivileged user namespaces are disabled (`kernel.unprivileged_userns_clone`, AppArmor userns restrictions) | | `bubblewrap cannot create a sandbox` | unprivileged user namespaces are disabled (`kernel.unprivileged_userns_clone`, AppArmor userns restrictions) |
| no "Tokens" panel | open port 4196, not 4197; see `<state>/data/opencode/log/context-panel.log` | | no "Tokens" panel | open port 4196, not 4197; see `<state>/data/opencode/log/context-panel.log` |
| no notifications | install `xdg-dbus-proxy` | | no notifications | install `xdg-dbus-proxy` |
| browser tools fail (`Missing X server`, `Failed to connect to Wayland display`) | no display in the sandbox: run the browser headless (Playwright MCP: `--headless`) |
| can't find a file the agent put in `/tmp` | it is in `~/.local/share/opencode-sandbox/<name>/tmp` |
| `docker build` fails pulling images | the proxy reaches registries through squid: check `egress logs` | | `docker build` fails pulling images | the proxy reaches registries through squid: check `egress logs` |
## Uninstall ## Uninstall
+13 -4
View File
@@ -123,6 +123,7 @@ squid.conf.
| `OCSB_PLUGINS` | skill-compact-resurrect | файлы плагинов из `plugins/` (или абсолютные пути) | | `OCSB_PLUGINS` | skill-compact-resurrect | файлы плагинов из `plugins/` (или абсолютные пути) |
| `OCSB_PANEL` | `1` | панель «Токены» в веб-интерфейсе | | `OCSB_PANEL` | `1` | панель «Токены» в веб-интерфейсе |
| `OCSB_NOTIFICATIONS` | `1` | уведомления через фильтрующий прокси D-Bus | | `OCSB_NOTIFICATIONS` | `1` | уведомления через фильтрующий прокси D-Bus |
| `OCSB_TMP` | `private` | `private` — свой `/tmp` агента в каталоге состояния; `shared` — `/tmp` хоста |
| `OCSB_SANDBOX` | `1` | `0` — только фильтр и netlock, без bubblewrap | | `OCSB_SANDBOX` | `1` | `0` — только фильтр и netlock, без bubblewrap |
| `OCSB_WORKTREES_DIR` | `../<проект>-worktrees` | куда класть worktree | | `OCSB_WORKTREES_DIR` | `../<проект>-worktrees` | куда класть worktree |
| `OCSB_WT_PREFIX` | — | каталог дерева = `<префикс><slug>` | | `OCSB_WT_PREFIX` | — | каталог дерева = `<префикс><slug>` |
@@ -225,8 +226,12 @@ opencode-sandbox run
Внутри песочницы: Внутри песочницы:
- **На запись:** проект, каталог worktree, каталог состояния, `OCSB_RW`, `OCSB_RW_HOME`, - **На запись:** проект, каталог worktree, каталог состояния, `OCSB_RW`, `OCSB_RW_HOME` и
`/tmp`. Всё остальное — только чтение. собственный `/tmp`. Всё остальное — только чтение.
- **Свой `/tmp`:** в `/tmp` хоста лежат сокеты, через которые можно выйти из песочницы
(tmux, X11, сокеты браузеров, других программ), поэтому у агента свой `/tmp` — в
`<состояние>/tmp` (файлы старше 7 дней удаляются при запуске). Скриншоты и логи агента ищите
там. Docker смонтирует путь из `/tmp` из `/tmp` хоста — монтируйте пути проекта.
- **Скрыто:** `OCSB_HIDE_HOME`, `OCSB_HIDE`, приватные ключи в `~/.ssh` (`config`, - **Скрыто:** `OCSB_HIDE_HOME`, `OCSB_HIDE`, приватные ключи в `~/.ssh` (`config`,
`known_hosts` и `*.pub` видны; подписывает ssh-agent хоста). `known_hosts` и `*.pub` видны; подписывает ssh-agent хоста).
- **Сеть:** `HTTP(S)_PROXY` указывают на фильтр. `GIT_SSH_COMMAND` пускает ssh через него - **Сеть:** `HTTP(S)_PROXY` указывают на фильтр. `GIT_SSH_COMMAND` пускает ssh через него
@@ -239,7 +244,9 @@ opencode-sandbox run
- **Сессионная шина:** заменена на `xdg-dbus-proxy`, который пропускает только - **Сессионная шина:** заменена на `xdg-dbus-proxy`, который пропускает только
`org.freedesktop.Notifications`; настоящая шина позволила бы агенту запускать процессы вне `org.freedesktop.Notifications`; настоящая шина позволила бы агенту запускать процессы вне
песочницы через `systemd-run --user`. песочницы через `systemd-run --user`.
- **Браузер:** `xdg-open` агенту отключён; интерфейс открывает `run` из вашей обычной сессии. - **Нет экрана:** `DISPLAY`, `WAYLAND_DISPLAY` и `XAUTHORITY` пустые, cookie X скрыт — графические
программы до вашего рабочего стола не доберутся; браузеры (Playwright MCP, Chrome) — только
headless. `xdg-open` отключён; интерфейс открывает `run` из вашей обычной сессии.
- **Без тихого отката:** при `OCSB_SANDBOX=1` и неработающем bubblewrap `run` не запустится. - **Без тихого отката:** при `OCSB_SANDBOX=1` и неработающем bubblewrap `run` не запустится.
**Netlock** (`netlock setup`, sudo) создаёт группу `opencode-sandbox`, добавляет в неё вас и **Netlock** (`netlock setup`, sudo) создаёт группу `opencode-sandbox`, добавляет в неё вас и
@@ -260,7 +267,7 @@ opencode-sandbox run
правила. правила.
- **Проект доступен на запись.** Ошибки внутри него настоящие; cc-safety-net блокирует - **Проект доступен на запись.** Ошибки внутри него настоящие; cc-safety-net блокирует
типичные разрушительные команды, git хранит историю — коммитьте чаще. типичные разрушительные команды, git хранит историю — коммитьте чаще.
- `/tmp` общий с хостом. - С `OCSB_TMP=shared` доступны `/tmp` хоста и его сокеты.
## Решение проблем ## Решение проблем
@@ -275,6 +282,8 @@ opencode-sandbox run
| `bubblewrap cannot create a sandbox` | отключены непривилегированные user namespaces (`kernel.unprivileged_userns_clone`, ограничения AppArmor) | | `bubblewrap cannot create a sandbox` | отключены непривилегированные user namespaces (`kernel.unprivileged_userns_clone`, ограничения AppArmor) |
| нет панели «Токены» | открывайте порт 4196, а не 4197; журнал — `<состояние>/data/opencode/log/context-panel.log` | | нет панели «Токены» | открывайте порт 4196, а не 4197; журнал — `<состояние>/data/opencode/log/context-panel.log` |
| нет уведомлений | установите `xdg-dbus-proxy` | | нет уведомлений | установите `xdg-dbus-proxy` |
| браузерные инструменты падают (`Missing X server`, `Failed to connect to Wayland display`) | в песочнице нет экрана: запускайте браузер headless (Playwright MCP: `--headless`) |
| не найти файл, который агент положил в `/tmp` | он в `~/.local/share/opencode-sandbox/<имя>/tmp` |
| `docker build` не скачивает образы | реестры доступны через squid: смотрите `egress logs` | | `docker build` не скачивает образы | реестры доступны через squid: смотрите `egress logs` |
## Удаление ## Удаление
+3 -2
View File
@@ -19,7 +19,7 @@ ocsb_info() { echo "→ $*"; }
OCSB_SCALARS=( OCSB_SCALARS=(
OCSB_NAME OCSB_WEB_PORT OCSB_EGRESS_PORT OCSB_NETLOCK_GROUP OCSB_SANDBOX OCSB_PANEL OCSB_NAME OCSB_WEB_PORT OCSB_EGRESS_PORT OCSB_NETLOCK_GROUP OCSB_SANDBOX OCSB_PANEL
OCSB_NOTIFICATIONS OCSB_OPENCODE_BIN OCSB_ENV_FILE OCSB_KUBECONFIG OCSB_PASSWORD_FILE OCSB_NOTIFICATIONS OCSB_TMP OCSB_OPENCODE_BIN OCSB_ENV_FILE OCSB_KUBECONFIG OCSB_PASSWORD_FILE
OCSB_WORKTREES_DIR OCSB_WT_PREFIX OCSB_WT_BASE OCSB_WT_SLUG_REGEX OCSB_DOCKER_GW OCSB_WORKTREES_DIR OCSB_WT_PREFIX OCSB_WT_BASE OCSB_WT_SLUG_REGEX OCSB_DOCKER_GW
OCSB_STATE_DIR OCSB_OPENCODE_CONFIG OCSB_EGRESS_BASE OCSB_STATE_DIR OCSB_OPENCODE_CONFIG OCSB_EGRESS_BASE
) )
@@ -33,6 +33,7 @@ ocsb_defaults() {
OCSB_SANDBOX=1 # bubblewrap; 0 = egress filter and netlock only OCSB_SANDBOX=1 # bubblewrap; 0 = egress filter and netlock only
OCSB_PANEL=1 # "Tokens" panel in the web UI (plugins/context-panel) OCSB_PANEL=1 # "Tokens" panel in the web UI (plugins/context-panel)
OCSB_NOTIFICATIONS=1 # desktop notifications through a filtered D-Bus proxy OCSB_NOTIFICATIONS=1 # desktop notifications through a filtered D-Bus proxy
OCSB_TMP=private # private: own /tmp in the state dir; shared: the host's /tmp
OCSB_OPENCODE_BIN="" # default: `opencode` from PATH OCSB_OPENCODE_BIN="" # default: `opencode` from PATH
OCSB_ENV_FILE="" # KEY=VALUE file with tokens, exported to OpenCode OCSB_ENV_FILE="" # KEY=VALUE file with tokens, exported to OpenCode
OCSB_KUBECONFIG="" # kubeconfig for the agent (~/.kube/config stays hidden) OCSB_KUBECONFIG="" # kubeconfig for the agent (~/.kube/config stays hidden)
@@ -53,7 +54,7 @@ ocsb_defaults() {
OCSB_HIDE_HOME=(.gnupg .aws .azure .config/gcloud .kube/config .docker/config.json .netrc OCSB_HIDE_HOME=(.gnupg .aws .azure .config/gcloud .kube/config .docker/config.json .netrc
.git-credentials .config/gh/hosts.yml .password-store .local/share/keyrings .mozilla .git-credentials .config/gh/hosts.yml .password-store .local/share/keyrings .mozilla
.config/google-chrome .config/chromium .config/BraveSoftware .claude .codex .config/google-chrome .config/chromium .config/BraveSoftware .claude .codex
.config/opencode .local/share/opencode) .config/opencode .local/share/opencode .Xauthority)
OCSB_SKILLS=(opencode-sandbox) # skills mounted for the agent (names of skill directories) OCSB_SKILLS=(opencode-sandbox) # skills mounted for the agent (names of skill directories)
OCSB_SKILL_DIRS=() # extra directories to look for skills in OCSB_SKILL_DIRS=() # extra directories to look for skills in
OCSB_PLUGINS=(skill-compact-resurrect/skill-compact-resurrect.v2.ts) # files in plugins/ OCSB_PLUGINS=(skill-compact-resurrect/skill-compact-resurrect.v2.ts) # files in plugins/
+16 -2
View File
@@ -124,11 +124,23 @@ ocsb_bwrap_args() {
OCSB_BWRAP=( OCSB_BWRAP=(
--die-with-parent --new-session --unshare-pid --unshare-ipc --unshare-uts --die-with-parent --new-session --unshare-pid --unshare-ipc --unshare-uts
--ro-bind / / --dev /dev --proc /proc --ro-bind / / --dev /dev --proc /proc
--bind /tmp /tmp --tmpfs "/run/user/${uid}"
)
# /tmp: the host's holds sockets that lead out of the sandbox (tmux, X11, browsers,
# other agents) — by default the agent gets its own, kept in the state directory.
# Mounted before the project and the state, so they stay visible if they live under /tmp
if [[ ${OCSB_TMP} == shared ]]; then
OCSB_BWRAP+=(--bind /tmp /tmp)
else
mkdir -p "${OCSB_STATE_DIR}/tmp"
chmod 700 "${OCSB_STATE_DIR}/tmp"
find "${OCSB_STATE_DIR}/tmp" -mindepth 1 -mtime +7 -delete 2>/dev/null || true
OCSB_BWRAP+=(--bind "${OCSB_STATE_DIR}/tmp" /tmp)
fi
OCSB_BWRAP+=(
--bind "${OCSB_PROJECT_DIR}" "${OCSB_PROJECT_DIR}" --bind "${OCSB_PROJECT_DIR}" "${OCSB_PROJECT_DIR}"
--bind "${OCSB_WORKTREES_DIR}" "${OCSB_WORKTREES_DIR}" --bind "${OCSB_WORKTREES_DIR}" "${OCSB_WORKTREES_DIR}"
--bind "${OCSB_STATE_DIR}" "${OCSB_STATE_DIR}" --bind "${OCSB_STATE_DIR}" "${OCSB_STATE_DIR}"
--tmpfs "/run/user/${uid}"
) )
for p in "${OCSB_RW[@]}"; do for p in "${OCSB_RW[@]}"; do
p=$(ocsb_abs "${p}") p=$(ocsb_abs "${p}")
@@ -243,6 +255,8 @@ ocsb_run() {
# No permission prompts in the sandbox: permission rules from all configs form one # No permission prompts in the sandbox: permission rules from all configs form one
# list, the last match wins, and OPENCODE_CONFIG_CONTENT is the last document. # list, the last match wins, and OPENCODE_CONFIG_CONTENT is the last document.
# Denies are repeated after "*" so they still win. # Denies are repeated after "*" so they still win.
# No display: GUI programs fail fast; browsers must run headless
env+=("DISPLAY=" "WAYLAND_DISPLAY=" "XAUTHORITY=")
env+=('OPENCODE_CONFIG_CONTENT={"permission":{"bash":{"*":"allow","systemd-run *":"deny","sudo *":"deny"},"external_directory":{"*":"allow"}}}') env+=('OPENCODE_CONFIG_CONTENT={"permission":{"bash":{"*":"allow","systemd-run *":"deny","sudo *":"deny"},"external_directory":{"*":"allow"}}}')
echo " sandbox: writable ${OCSB_PROJECT_DIR}, ${OCSB_WORKTREES_DIR}${OCSB_RW[*]:+, ${OCSB_RW[*]}}; the rest of the system is read-only" echo " sandbox: writable ${OCSB_PROJECT_DIR}, ${OCSB_WORKTREES_DIR}${OCSB_RW[*]:+, ${OCSB_RW[*]}}; the rest of the system is read-only"
fi fi
+4 -2
View File
@@ -14,7 +14,7 @@ sandbox is on.
|---|---| |---|---|
| The project, its worktrees directory, paths from `OCSB_RW` in `.opencode-sandbox/sandbox.conf` | read-write, real disk | | The project, its worktrees directory, paths from `OCSB_RW` in `.opencode-sandbox/sandbox.conf` | read-write, real disk |
| Toolchain caches in `~` (`.cache`, `.npm`, `.asdf`, `.bundle`, `.gem`, …) | read-write | | Toolchain caches in `~` (`.cache`, `.npm`, `.asdf`, `.bundle`, `.gem`, …) | read-write |
| `/tmp` | read-write, shared with the host | | `/tmp` | read-write, **private** to the sandbox (on the host it is `<state>/tmp`; files older than 7 days are cleaned) |
| Everything else | read-only | | Everything else | read-only |
| Private SSH keys, `~/.gnupg`, cloud CLI credentials, `~/.kube/config`, browser profiles, other agents' configs, `OCSB_HIDE` paths | hidden (empty) | | Private SSH keys, `~/.gnupg`, cloud CLI credentials, `~/.kube/config`, browser profiles, other agents' configs, `OCSB_HIDE` paths | hidden (empty) |
@@ -39,6 +39,7 @@ sandbox is on.
## Things you cannot do ## Things you cannot do
- Open a browser (`xdg-open` is disabled): print the link for the user. - Open a browser (`xdg-open` is disabled): print the link for the user.
- Run GUI programs: there is no display. Browsers (Playwright, Chrome) must run headless.
- `sudo`, `systemd-run`: denied. - `sudo`, `systemd-run`: denied.
- Start processes outside the sandbox through the session bus: only desktop notifications - Start processes outside the sandbox through the session bus: only desktop notifications
are reachable. are reachable.
@@ -46,7 +47,8 @@ sandbox is on.
## Docker ## Docker
The Docker socket is available, so Docker can see the whole host. Mount only paths inside The Docker socket is available, so Docker can see the whole host. Mount only paths inside
the project or its worktrees; never mount `/`, `~` or system directories. the project or its worktrees; never mount `/`, `~` or system directories. Do not mount `/tmp`
paths: the Docker daemon sees the host's `/tmp`, not your private one.
## Worktrees ## Worktrees