Status: in progress. Phase 0.5 is complete.
- exec sandbox (
Landlock/seccomp) - fetch/SSRF policy
- Go gateway routing
- plugin-host isolation
Implemented:
go/cmd/anvil-exec-runner: fixed absolute tool path, no shell, no PATH lookup;go/internal/execrunner: Landlock read-only workspace and runtime rules,no_new_privs, and a positive seccomp allowlist;- seccomp rejects namespace/privilege/network/mount/ptrace syscalls outside the
allowlist, returns
ENOSYSforclone3so glibc falls back toclone, and filtersclonenamespace flags, anonymousPROT_EXECmmap, andmprotect(PROT_EXEC); - Landlock grants execute only to the selected tool and its ELF interpreter; runtime directories remain read-only;
- the runner locks its OS thread before applying per-thread Landlock/seccomp state and execs on that same goroutine;
go/internal/execpolicy:rg/fdargv parsers rejecting command-launching and config-loading flags;- gateway
-cgroup-rootcreates a per-session child, cleans it on close, and passesANVIL_CGROUP_PATHtosys.exec; the runner writespids.maxand joins it before exec; - process-group supervision with timeout, cancellation, descendant kill,
combined output cap,
Pdeathsig, andWaitDelay; - per-sandbox cgroup v2
pids.maxwhenANVIL_CGROUP_PATHis supplied, otherwiseRLIMIT_NPROCreserves 64 tasks above the user's current count; - cgroup move happens before Landlock/seccomp and before the final execve;
when cgroup is active the runner raises the soft
RLIMIT_NPROCto the hard limit, leavingmin(pids.max, RLIMIT_NPROC hard)as the effective bound; - extra descriptors are marked close-on-exec before applying the sandbox;
- tests for outside-file/symlink and workspace-write denial, concurrent
symlink-swap races, non-allowlisted exec (including via
ld.so), socket/permission denial, allowlist escape, clone3 fallback, tool startup, cgroup file writes and stale-directory cleanup, RLIMIT_NPROC verification, timeout descendant kill, output cap, fd cleanup, and exit-code capture.
Open:
- gateway must resolve the tool fd/policy, create
ANVIL_CGROUP_PATHbefore launch, remove it after wait, and callCleanupStaleCgroupsfor stale empty directories at startup; - gateway needs write access to
/sys/fs/cgroup/anvil-exec/via systemdDelegate=pidsor a pre-created/chowned root; it must not run as root just for cgroup management; - CI needs an explicit Landlock capability probe and skip path;
- gateway control-socket registration and inherited-fd token delivery remain.
Implemented:
go/internal/fetch: exact host allowlist, DNS pinning, A/AAAA validation, private/loopback/link-local/CGNAT/documentation/transition-range rejection, HTTPS-only production policy, disabled redirects, timeout, and body cap;sys.fetchrouting in the gateway withsys.fetchpermission checks;- allowlisted request headers (
Accept,Authorization,Content-Type,User-Agent), with sensitive header names audited without values; - TLS
ServerNameset from the original host withInsecureSkipVerifyexplicitly false, while the TCP dial uses the pinned IP; httptestcoverage for pinning/host preservation, mixed DNS answers, blocked addresses, host denial, redirect denial, headers, and body truncation.
Open:
- add deployment documentation for the allowlist; the gateway currently requires
explicit
-fetch-allowhosts and has no wildcard support.
Implemented:
go/cmd/anvil-gatewayandgo/internal/gateway: per-session Unix socket with mode0600, 4-byte length-prefixed JSON-RPC,SO_PEERCREDUID check,session.hellotoken-hash authentication, andsys.execrouting throughanvil-exec-runner;- the bootstrap token hash is supplied with
-token-sha256; the raw token is only sent by the client insession.hello; crates/anvil-ipc/examples/gateway_handshake.rsperforms a real Rust-to-Go handshake;- Go tests cover handshake rejection and the
sys.execrunner route.
Security boundaries:
-token-sha256is a verifier, not a token. The gateway hashes the 32-byte token fromsession.helloand compares it; it does not accept the hash as a credential. The token is consumed by the first successful handshake, so a second connection is rejected. v1 has no HMAC/nonce challenge; this is safe only because the token is 32 random bytes, is sent only over the session UDS, and is one-shot.SO_PEERCREDsupplies the same-UID check; the one-shot token supplies plugin identity. Both are required.-allow-execis per gateway instance, and the Phase 0.6 deployment uses one gateway per plugin session. Per-session permission negotiation is a follow-up.- A gateway is a per-session process, not a daemon.
Closeremoves the socket; a crashed gateway leaves a stale socket and no authority. - Closing the client connection cancels in-flight
sys.execwork and kills the runner process group. A handshake is single-use per connection.
Open:
- replace the bootstrap
-token-sha256flag with editor control-socket registration and inherited-fd token delivery; client must pin the gateway pid/start-time so a stale-socket replacement cannot impersonate it; - add fetch/SSRF and gateway audit logging;
- provide deployment-specific delegated cgroup root provisioning/systemd unit;
- add real-runner gateway integration coverage to CI;
- continue
anvil-tuiTUI-2+ (Elm Msg/Cmd runtime, selections, prefix commands); - benchmark complete sandboxed exec and TLS fetch with transport reuse.
Not started. Split into independently verifiable slices:
- 0.7a runtime: add Luau (
mlua) or QuickJS (rquickjs), load a script, execute a function, and return a value; no gateway dependency. - 0.7b control registration: editor-to-gateway control socket, gateway pid/start-time pinning, and inherited-fd token delivery replacing the bootstrap hash argument.
- 0.7c plugin-host process: read the token fd, connect to the gateway,
session.hello, and receive permissions. - 0.7d hot reload supervisor: SIGTERM the old host, start a new host, and repeat the handshake. Verify no process, fd, or socket leak.
- exec runner denies outside reads, network, ptrace, and privilege changes;
- SSRF denies loopback/private/link-local/CGNAT and DNS rebinding;
- gateway logs every denied request with plugin id and trace id;
- plugin host survives SIGKILL, is denied outside workspace, and re-handshakes after reload;
- full-kernel tests run locally/nightly; PR CI skips only after an explicit capability probe.