3 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 0cc5982824 fix(execd): keep the host socket from children, and git where dir says
CI / build (push) Waiting to run
Go / generate (push) Waiting to run
FD 3 arrives without FD_CLOEXEC, which is how it crosses the exec into
execd, and execd never set it again, so every command, pty and git it
ran held the host's channel: it could read requests meant for the
daemon and answer in its name. newDaemon now sets FD_CLOEXEC. The test
harness made its pair with SOCK_CLOEXEC, which hid this; the daemon's
end now arrives inheritable, as under runsc. The workspace descriptor
dup'd for a top-level name closes on exec too.

The git ceiling governs only discovery, so -C, --git-dir, --work-tree
and a GIT_DIR or GIT_CONFIG_GLOBAL in env reached a repository or a
config file outside the workspace. Before its subcommand git now takes
-c and options that move nothing, and refuses the rest, since an
option execd does not know could take a value that hides the next.
GIT_* in env is refused and the global config is /dev/null, so HOME
picks no file. The ceiling test now covers each of these.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 04:05:18 -07:00
zeekayandHanzo Dev 4855f06fd1 fix(execd): confine git, make patch all-or-nothing, answer only what happened
CI / build (push) Failing after 33s
Go / generate (push) Failing after 46s
The git ceiling named the workspace, which git only applies to a proper
ancestor of the directory it starts from, so a request with no dir found,
read and wrote a repository above the workspace. The ceiling is now the
workspace's parent, which refuses from the root and from any directory
under it.

patch staged by action id, so two changes naming one file collided and
the second failed with the first's bytes already in place, and a delete
that could not happen left earlier renames behind. Every change now
stages under a random name, the file that was there is kept on a second
link until every move has landed, and a move that fails is put back. A
diff that names one file twice, gives two names for one file, would
delete a directory, or renames is refused.

A zero-length packet is a packet on SOCK_SEQPACKET, and reading it as end
of file ended the daemon for good. POLLRDHUP tells the two apart.

Also: a listing fills a page to what a frame holds instead of promising
4096 entries no frame can carry; write and patch resolve the parent with
openat2 on the path as written, so one string cannot mean two places; a
refused request is not recorded, so its id is free, while an action that
ran is recorded whatever came of it; a staged name is random, so a
workspace file is never unlinked to make room; children start from a
declared environment instead of the daemon's; write reports the mode on
disk and keeps the mode a file had; mode means permission bits in stat as
well as read and list; a read past the end is refused; an oversize
refusal names the op it refused; the ledger is bounded by bytes as well
as count; the diff path is the reading both header lines agree on; the
build constraint sits above the package doc; and BUILD embeds a library,
which is how the repo builds a main package with tests.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-18 17:49:38 -07:00
zeekayandHanzo Dev 6208c7df95 Add execd, the execution daemon inside the sandbox
CI / build (push) Canceled after 0s
Go / generate (push) Canceled after 0s
A coding agent is two programs: one reasons, one runs cargo test. execd is the
second half, and it knows nothing about the first — no model, no session, no
plan, no credential. Actions arrive as ZAP frames on FD 3, one end of a
SOCK_SEQPACKET pair the host connected before exec, so there is no listen, no
connect and no path in a filesystem.

Every request carries an action id and every reply names it. A repeated id for
a completed action is answered from the record instead of run again, so losing
an answer to git commit does not commit twice; the table is bounded by count
and an id whose result was evicted is refused rather than replayed. Paths
resolve with openat2 RESOLVE_BENEATH, so the kernel refuses "..", an absolute
path and a symlink that leaves the workspace. write and patch stage a
temporary file and rename over the target, and patch verifies every file in a
diff before renaming any of them.

Nothing fsyncs: the host serves the workspace and owns durability, and an
fsync inside the cell can cost seconds for nothing. Tests need no visor — a
socketpair in-process is the same code path as FD 3 under runsc.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-18 13:48:08 -07:00