Files
zeekayandHanzo Dev 6208c7df95
CI / build (push) Canceled after 0s
Go / generate (push) Canceled after 0s
Add execd, the execution daemon inside the sandbox
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

124 lines
3.3 KiB
Go

// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
package main
import (
"bytes"
"encoding/binary"
"fmt"
"golang.org/x/sys/unix"
)
// watchMask is what a watch reports: names appearing, leaving, changing, and
// the watched path itself going away.
const watchMask = unix.IN_CREATE | unix.IN_DELETE | unix.IN_MODIFY |
unix.IN_CLOSE_WRITE | unix.IN_ATTRIB | unix.IN_MOVED_FROM |
unix.IN_MOVED_TO | unix.IN_DELETE_SELF | unix.IN_MOVE_SELF
// watch answers with a handle and then pushes one kindEvent frame per
// inotify event under the same action id. One path, not its subtree: the
// host watches the directories it cares about.
func (d *daemon) watch(r *req) *rep {
name, err := d.root.abs(r.path)
if err != nil {
return fail(r, err)
}
nfy, err := unix.InotifyInit1(unix.IN_CLOEXEC)
if err != nil {
return fail(r, fmt.Errorf("inotify: %w", err))
}
if _, err := unix.InotifyAddWatch(nfy, name, watchMask); err != nil {
unix.Close(nfy)
return fail(r, fmt.Errorf("watch %s: %w", r.path, err))
}
var stop [2]int
if err := unix.Pipe2(stop[:], unix.O_CLOEXEC); err != nil {
unix.Close(nfy)
return fail(r, err)
}
h := &handle{id: r.id, kind: opWatch, nfy: nfy, stop: stop, done: make(chan struct{})}
n := d.add(h)
d.pumps.Add(1)
go d.pumpWatch(h, n)
return &rep{handle: n, path: name}
}
// pumpWatch reads events until the handle is closed. It owns neither
// descriptor's lifetime: shut waits for this loop to return and then closes
// them, so no descriptor is ever closed under a blocked read.
func (d *daemon) pumpWatch(h *handle, n uint64) {
defer d.pumps.Done()
defer close(h.done)
fds := []unix.PollFd{
{Fd: int32(h.nfy), Events: unix.POLLIN},
{Fd: int32(h.stop[0]), Events: unix.POLLIN},
}
buf := make([]byte, 16<<10)
for {
if _, err := unix.Poll(fds, -1); err != nil {
if err == unix.EINTR {
continue
}
return
}
if fds[1].Revents != 0 {
return
}
if fds[0].Revents == 0 {
continue
}
got, err := unix.Read(h.nfy, buf)
if err == unix.EINTR {
continue
}
if err != nil || got <= 0 {
return
}
for _, e := range events(buf[:got]) {
e.op, e.kind, e.id, e.handle = opWatch, kindEvent, h.id, n
d.send(e)
}
}
}
// events cuts an inotify read into one reply per event.
func events(buf []byte) []*rep {
var out []*rep
for len(buf) >= unix.SizeofInotifyEvent {
mask := binary.LittleEndian.Uint32(buf[4:])
size := binary.LittleEndian.Uint32(buf[12:])
end := unix.SizeofInotifyEvent + int(size)
if end > len(buf) {
return out
}
name := ""
if size > 0 {
raw := buf[unix.SizeofInotifyEvent:end]
if i := bytes.IndexByte(raw, 0); i >= 0 {
raw = raw[:i]
}
name = string(raw)
}
out = append(out, &rep{mask: mask, path: name})
buf = buf[end:]
}
return out
}