A minor release on v0.2.1. We ran Sandy end to end against a public third-party MCP server (@cyanheads/libofcongress-mcp-server): first under macOS Seatbelt, then again on Linux in a read-only Docker container. This release fixes everything that test found (write-up). It includes one security fix, and it adds capability (MCP Registry import, Seatbelt detection).
Security
- A Linux host running Docker was detected as a
dockersandbox (fail-open). The container detector matcheddocker|containerdanywhere in/proc/self/mountinfo. A Docker host lists every running container's overlay rootfs there, so on a bare Linux machine with any container running, a config declaringsandbox.runtime: "docker"started unsandboxed and reportedRESULT: OK. Now only the process's own root mount is inspected. Affects v0.2.1 and earlier on Linux hosts running containers; upgrade. Advisory: GHSA-vrfr-983g-7848 (high). (#60)
New
sandy importreads MCP Registryserver.json. Conversion is deterministic, and the result still has to pass the existing manifest schema. The registry format lists no tools, so--tools <server=a,b>is required. When a server offers both a hosted endpoint and a package,--registry-source remote|packageis required too. Headers, URL templates, unsupported registries or transports, and required arguments with no fixed value are refused (exit 3). (#56)macos-sandbox-execis detected. Detection runs a nested no-opsandbox-exec, which the kernel refuses under a restrictive Seatbelt profile. Runtimes that still can't be detected (systemd-nspawn,chroot,windows-appcontainer) now get an error that points atcustom. (#54)import --applycan add the first server to a missing or emptymcp-servers.json. A refused apply now restores both config files byte for byte. (#55)
Fixed
-
Reports are never silently dropped.
run: areport.filethat the configured format can't be written under (e.g..mdwithpdf) is refused with exit2before any MCP call.run/ask: a report that couldn't be written printsreport: NOT WRITTEN — <reason>and exits1. This includes a failed multi-round consolidation inask.- A failed narrate rewrite no longer crashes
ask. - A filename the model proposes that the configured format can't use is replaced by the default name.
-
stdio MCP servers' stderr is drained. Previously it was piped but never read: a non-Node server writing more than ~64 KiB to stderr could block forever, and a server that died at startup reported only
Connection closed. A startup failure now includes the server's own error lines, e.g.(server stderr: npm error enoent … mkdir '/home/node/.npm'). (#60) -
A failed import fetch is audited (
import_fetchwithoutcome: "error"). (#51) -
Markdown footnotes pair up: one reference and one definition per claim. (#53)
-
The
customboundary message is accurate. It says Sandy continues under the operator's boundary and can't verify it, instead of claiming to refuse. (#57) -
Every audit event carries a per-invocation
sessionid, so(session, seq)is unique in a shared JSONL log. (#58)
Notes
- Contract changes: new exit-2/exit-1 cases for reports, a
sessionfield on audit events,declaredRuntimein the capability manifest, and aformatfield on the import result. - Tests: 387/387 (was 349 at v0.2.1). Typecheck and build are green, and CI (core, the Docker/Firejail
boundary × modematrix, and the identity check) passes. - Version bumped
0.2.1 → 0.3.0inpackage.json,package-lock.json,plugin/.claude-plugin/plugin.json, and the two runtime identity strings.