darkwing: row 86 round 1 review packet, approve (#1580)

Review notes and the rerun of S1, S2, S4, S8, S9 and S0 in a detached
worktree of ad0bdc20. Verdict comment 27986, rev 803 (swept in 85fd3286).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-12 17:32:48 -05:00
co-authored by Claude Opus 5.5
parent d5bca87a1e
commit 459c4fc681
9 changed files with 382 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Darkwing's row 86 rerun: S1 S2 S4 S8 S9, then S0. Aborts if another seat is busy.
busy=$(for p in /proc/[0-9]*; do c=$(readlink "$p/cwd" 2>/dev/null) || continue; case $c in */sage-g*|*/rocko-r*|*dewey-scratch*|*filbert-scratch*) echo "BUSY ${p#/proc/} $c";; esac; done)
[ -z "$busy" ] || { echo "$busy"; exit 9; }
pgrep -u "$(id -u)" -fa filbert-scratch && { echo "BUSY filbert-scratch"; exit 9; }
export TMPDIR=$HOME/darkwing-scratch/rv86/tmp
cd "$HOME/darkwing-scratch/rv86/wt/agents/researcher/work/i1-spikes" || exit 9
for s in s1-endpoint-pin s2-uid-mapping s4-hostile-client s8-stdin-capability s9-hardening s0-clean-images; do
t0=$(date -u +%H:%M:%SZ)
timeout 600 ./$s.sh >/dev/null 2>&1; rc=$?
echo "$s rc=$rc $t0-$(date -u +%H:%M:%SZ) $(grep -c '^ok' out/$s.out) ok, $(grep -c '^FAIL' out/$s.out) FAIL"
done
echo done
@@ -0,0 +1,7 @@
s1-endpoint-pin rc=0 22:29:05Z-22:29:06Z 6 ok, 0 FAIL
s2-uid-mapping rc=0 22:29:06Z-22:29:17Z 23 ok, 0 FAIL
s4-hostile-client rc=0 22:29:17Z-22:29:24Z 21 ok, 0 FAIL
s8-stdin-capability rc=0 22:29:24Z-22:29:42Z 18 ok, 0 FAIL
s9-hardening rc=0 22:29:42Z-22:29:47Z 32 ok, 0 FAIL
s0-clean-images rc=0 22:29:47Z-22:29:47Z 7 ok, 0 FAIL
done
@@ -0,0 +1,26 @@
# s0-clean-images 2026-10-12T22:29:47Z
endpoint unix:///run/user/1000/docker.sock: name=rootless
scratch /home/jwoltje/darkwing-scratch/rv86/tmp/i1spike-s0-clean-images.rEayPo
## before
node:<none> d6aa754f16b3
alpine:3 294b683cb724
build cache: Build Cache 0B
## remove
ok no container (of anyone) is using the pinned node image
Untagged: node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
Deleted: sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
Total: 0B
## after
network: bridge bridge
network: host host
network: none null
image: alpine:3 294b683cb724
Build Cache 0B
ok the pinned node image is gone
ok alpine:3 (not ours) is still there
ok no image carries a mosaic.i1spike label
ok no container is left
ok only the default networks are left (bridge host none)
ok no volume was ever created
cleanup: no container, network, image or scratch directory labelled s0-clean-images is left
result: pass
@@ -0,0 +1,39 @@
# s1-endpoint-pin 2026-10-12T22:29:05Z
endpoint unix:///run/user/1000/docker.sock: name=rootless
scratch /home/jwoltje/darkwing-scratch/rv86/tmp/i1spike-s1-endpoint-pin.YNI9xJ
## rootless endpoint, the field the launcher parses
$ docker --host unix:///run/user/1000/docker.sock info --format '{{json .SecurityOptions}}'
["name=seccomp,profile=builtin","name=rootless","name=cgroupns"]
ok rootless endpoint lists name=rootless
## one-per-line form, exact-match test
name=seccomp,profile=builtin
name=rootless
name=cgroupns
ok exact line name=rootless present
## plain-text form (what a human sees)
41: Security Options:
42- seccomp
43- Profile: builtin
44- rootless
45- cgroupns
## the Rootless field of the info struct
{{.Rootless}} ->
template: :1:2: executing "" at <.Rootless>: can't evaluate field Rootless in type system.dockerInfo
## system (rootful) endpoint, info only
["name=seccomp,profile=builtin","name=cgroupns"]
ok system endpoint does not list name=rootless
## --host wins over DOCKER_HOST
["name=seccomp,profile=builtin","name=rootless","name=cgroupns"]
ok --host overrides a DOCKER_HOST naming the system socket
## missing socket
exit 1
null
failed to connect to the docker API at unix://<scratch>/none.sock; check if the path is correct and if the daemon is running: dial unix <scratch>/none.sock: connect: no such file or directory
ok missing socket: nonzero exit, no rootless text
## a socket path that is not a daemon (a plain unix server answering garbage is not tested; a regular file is)
exit 1
null
Cannot connect to the Docker daemon at unix://<scratch>/notasocket. Is the docker daemon running?
ok regular file as endpoint: nonzero exit
cleanup: no container, network, image or scratch directory labelled s1-endpoint-pin is left
result: pass
@@ -0,0 +1,65 @@
# s2-uid-mapping 2026-10-12T22:29:06Z
endpoint unix:///run/user/1000/docker.sock: name=rootless
scratch /home/jwoltje/darkwing-scratch/rv86/tmp/i1spike-s2-uid-mapping.UtDZ7H
## the mapping
host uid/gid: 1000/1000; /etc/subuid line count for this user: 1
0 1000 1
1 100000 65536
0 1000 1
1 100000 65536
host fixture: -rw------- jwoltje:jwoltje <scratch>/d/file;srw------- jwoltje:jwoltje <scratch>/d/sock;
## 0600 file and 0600 socket owned by the host user
uid 0 (user: 0:0) -> {"uid":0,"gid":0,"groups":[0],"file":{"ok":true,"head":"not-a-secret\n"},"sock":{"ok":true,"reply":"pong host 1"}}
ok uid 0 reads the file
ok uid 0 connects to the socket
uid 1000 (user: 1000:1000) -> {"uid":1000,"gid":1000,"groups":[1000],"file":{"ok":false,"err":"EACCES"},"sock":{"ok":false,"err":"EACCES"}}
ok uid 1000 cannot read the file
ok uid 1000 cannot connect to the socket
uid 2000 (user: 2000:2000) -> {"uid":2000,"gid":2000,"groups":[2000],"file":{"ok":false,"err":"EACCES"},"sock":{"ok":false,"err":"EACCES"}}
ok uid 2000 cannot read the file
ok uid 2000 cannot connect to the socket
uid 0 with the hardening set (--cap-drop ALL, no-new-privileges, --read-only) -> {"uid":0,"gid":0,"groups":[0],"file":{"ok":true,"head":"not-a-secret\n"},"sock":{"ok":true,"reply":"pong host 2"}}
ok uid 0 with cap-drop ALL still reads the host user's 0600 file
ok uid 0 with cap-drop ALL still connects to the host user's 0600 socket
## 0660 and the host group, non-root container uid with the group added
host fixture: -rw-rw---- jwoltje:jwoltje;srw-rw---- jwoltje:jwoltje;
uid 1000, no extra group -> {"uid":1000,"gid":1000,"groups":[1000],"file":{"ok":false,"err":"EACCES"},"sock":{"ok":false,"err":"EACCES"}}
ok uid 1000 without the group is still refused (file)
uid 1000 + --group-add 0 -> {"uid":1000,"gid":1000,"groups":[0,1000],"file":{"ok":true,"head":"not-a-secret\n"},"sock":{"ok":true,"reply":"pong host 3"}}
ok uid 1000 + group 0 reads the file
ok uid 1000 + group 0 connects to the socket
uid 2000:2000 + --group-add 0 -> {"uid":2000,"gid":2000,"groups":[0,2000],"file":{"ok":true,"head":"not-a-secret\n"},"sock":{"ok":true,"reply":"pong host 4"}}
ok uid 2000 + group 0 connects (two roles would share it)
## world-accessible (0666) for comparison
uid 2000, no group -> {"uid":2000,"gid":2000,"groups":[2000],"file":{"ok":true,"head":"not-a-secret\n"},"sock":{"ok":true,"reply":"pong host 5"}}
ok uid 2000 connects when the socket is 0666
## a file chowned to a container uid, seen from the host
host sees: -rw------- 100999:100999
ok the host user can no longer read a file owned by container uid 1000 (mode 0600)
uid 1000 owner -> {"uid":1000,"gid":1000,"groups":[1000],"file":{"ok":true,"head":"not-a-secret\n"},"sock":{"ok":true,"reply":"pong host 6"}}
ok container uid 1000 reads its own file
chowned back, host sees: -rw------- 1000:1000
## the reverse: a socket created by container uid 1000 in a mounted directory
host sees: srw------- 100999:100999 (host user is 1000:1000)
host connect, mode 0600 -> {"uid":1000,"gid":1000,"groups":[3,90,108,955,956,979,982,983,985,989,995,998,1000,3009],"sock":{"ok":false,"err":"EACCES"}}
ok host user cannot connect to a 0600 socket owned by container uid 1000
after the container root chmods it 0660 (group is the container's gid 1000): host sees srw-rw---- 100999:100999
host connect, mode 0660 -> {"uid":1000,"gid":1000,"groups":[3,90,108,955,956,979,982,983,985,989,995,998,1000,3009],"sock":{"ok":false,"err":"EACCES"}}
ok host user is still refused at 0660 (not in the owning group)
host connect, mode 0666 -> {"uid":1000,"gid":1000,"groups":[3,90,108,955,956,979,982,983,985,989,995,998,1000,3009],"sock":{"ok":true,"reply":"pong container 1"}}
ok host user connects at 0666
## container to container: a socket made by one container uid, reached from another container
host sees the socket: srw-rw---- 101000:101999 (made by container 1001:2000, mode 0660)
client 1000:1000 + --group-add 2000 -> {"uid":1000,"gid":1000,"groups":[1000,2000],"sock":{"ok":true,"reply":"pong broker 1"}}
ok a different uid in the socket's group connects (mode 0660, :ro directory mount, cap-drop ALL)
client 1000:1000, no group -> {"uid":1000,"gid":1000,"groups":[1000],"sock":{"ok":false,"err":"EACCES"}}
ok a different uid outside the group is refused
client 0:0, default capabilities -> {"uid":0,"gid":0,"groups":[0],"sock":{"ok":true,"reply":"pong broker 2"}}
(container root is the host user, who owns nothing here; default capabilities include CAP_DAC_OVERRIDE over inodes mapped into the container)
ok container root with default capabilities connects: CAP_DAC_OVERRIDE overrides the mode
client 0:0, --cap-drop ALL -> {"uid":0,"gid":0,"groups":[0],"sock":{"ok":false,"err":"EACCES"}}
ok container root with cap-drop ALL is refused: it is neither owner nor group
client 0:0, --cap-drop ALL, --group-add 2000 -> {"uid":0,"gid":0,"groups":[0,2000],"sock":{"ok":true,"reply":"pong broker 3"}}
ok container root with cap-drop ALL and the group connects
cleanup: no container, network, image or scratch directory labelled s2-uid-mapping is left
result: pass
@@ -0,0 +1,68 @@
# s4-hostile-client 2026-10-12T22:29:17Z
endpoint unix:///run/user/1000/docker.sock: name=rootless
scratch /home/jwoltje/darkwing-scratch/rv86/tmp/i1spike-s4-hostile-client.s3JQIf
## the broker, on a temp dataRoot (/home/jwoltje/darkwing-scratch/rv86/wt/packages/bus/src/index.mjs, node v26.8.1)
ready <scratch>/dr/bus/broker.sock
dataRoot: drwx------ jwoltje bus/: drwx------ jwoltje bus/ holds: broker.sock bus.sqlite bus.sqlite-shm bus.sqlite-wal writer.lock
broker.sock: srw------- jwoltje
ok broker is listening
## the container: root, bus/ mounted read-only at /bus
{"case":"valid capability, verb role.claim","answer":"{\"ok\":true,\"result\":{\"role\":\"cto\",\"run\":\"spike\"}}"}
{"case":"valid capability, verb agents","answer":"{\"ok\":true,\"result\":[{\"seq\":1,\"at\":\"2026-10-12T22:29:17.745Z\",\"business\":\"demo\",\"role\":\"cto\",\"op\":\"claim\",\"holder_run\":\"spike\",\"harness\":\"pi\",\"address\":null,\"by\":\"spike\",\"reason\":null,\"decision\":null}]}"}
{"case":"made-up capability","answer":"{\"ok\":false,\"error\":\"unauthenticated\"}"}
{"case":"human envelope, made-up proof","answer":"{\"ok\":false,\"error\":\"human-required\"}"}
{"case":"human envelope, this container's own pid and start time","answer":"{\"ok\":false,\"error\":\"human-required\"}"}
{"case":"human envelope, pid 1","answer":"{\"ok\":false,\"error\":\"human-required\"}"}
{"case":"both cap and human","answer":"{\"ok\":false,\"error\":\"invalid-envelope\"}"}
{"case":"unknown key (role: human)","answer":"{\"ok\":false,\"error\":\"invalid-envelope\"}"}
{"case":"valid capability, unknown verb sql","answer":"{\"ok\":false,\"error\":\"unknown-verb\"}"}
{"case":"not json","answer":"{\"ok\":false,\"error\":\"invalid-envelope\"}"}
{"case":"request of exactly 65536 bytes (forged cap)","answer":"{\"ok\":false,\"error\":\"unauthenticated\"}"}
{"case":"request of 65537 bytes (forged cap)","answer":"{\"ok\":false,\"error\":\"request-too-large\"}"}
{"case":"70000 bytes, no newline","answer":"{\"ok\":false,\"error\":\"request-too-large\"}"}
{"case":"1 MiB of junk, newline-terminated","answer":"{\"ok\":false,\"error\":\"request-too-large\"}"}
{"case":"connect and send nothing","answer":"{\"ok\":false,\"error\":\"request-timeout\"}"}
{"case":"partial line then close","answer":"{\"ok\":false,\"error\":\"incomplete-request\"}"}
ok hostile client ran to the end
## assertions
ok a real capability works through a :ro mount of bus/ (role.claim)
ok and a read verb after the claim (agents)
ok made-up capability -> unauthenticated
ok human envelope with a made-up proof -> human-required
ok human envelope with the container's own pid -> human-required
ok human envelope with pid 1 -> human-required
ok cap and human together -> invalid-envelope
ok unknown envelope key -> invalid-envelope
ok valid capability, verb sql -> unknown-verb
ok non-JSON -> invalid-envelope
ok 65536-byte request is read in full (forged cap -> unauthenticated)
ok 65537-byte request -> request-too-large
ok 70000 bytes with no newline -> request-too-large
ok 1 MiB -> request-too-large or a reset ({"ok":false,"error":"request-too-large"})
ok silent connection -> request-timeout after the 5 s idle limit
ok partial line then close -> incomplete-request
## what a mounted bus/ also exposes (the reason the socket needs its own directory)
total 164
drwx------ 2 root root 4096 Oct 12 22:29 .
drwxr-xr-x 1 root root 4096 Oct 12 22:29 ..
srw------- 1 root root 0 Oct 12 22:29 broker.sock
-rw------- 1 root root 4096 Oct 12 22:29 bus.sqlite
-rw------- 1 root root 32768 Oct 12 22:29 bus.sqlite-shm
-rw------- 1 root root 115392 Oct 12 22:29 bus.sqlite-wal
-rw------- 1 root root 46 Oct 12 22:29 writer.lock
{"uid":0,"gid":0,"groups":[0],"file":{"ok":true,"head":"SQLite format 3\u0000"}}
ok container root can read bus.sqlite when bus/ is mounted (the two-listener change must split the directory)
as uid 1000: {"uid":1000,"gid":1000,"groups":[1000],"file":{"ok":false,"err":"EACCES"},"sock":{"ok":false,"err":"EACCES"}}
:ro write test: touch: cannot touch '/bus/x': Read-only file system
:ro write test: write attempt exit 1
## what the broker logged
stdout/stderr of the broker process while serving:
ready <scratch>/dr/bus/broker.sock
events table after close:
event {"seq":1,"kind":"session.launched","business":"demo","actor_role":"cto","subject":"spike","body":"{\"harness\":\"pi\",\"address\":null,\"pid\":null,\"startTime\":null}"}
event {"seq":2,"kind":"action.refused","business":"demo","actor_role":"cto","subject":null,"body":"{\"code\":\"unknown-verb\"}"}
events-total 2
ok no event row for a forged capability, a human-required refusal, an oversize or malformed request (the transport refuses before any identity exists)
ok the broker process printed nothing of its own while serving (only the driver's ready and event lines)
cleanup: no container, network, image or scratch directory labelled s4-hostile-client is left
result: pass
@@ -0,0 +1,41 @@
# s8-stdin-capability 2026-10-12T22:29:24Z
endpoint unix:///run/user/1000/docker.sock: name=rootless
scratch /home/jwoltje/darkwing-scratch/rv86/tmp/i1spike-s8-stdin-capability.3Izcvy
made-up capability: 57 bytes, sha256 a3d659831fdb756840377edc343a25ce8a1764d6562979fdc9ea8397ee044d9e (the string itself is never printed)
## 1. one line on stdin
ok entrypoint started and printed ready
entrypoint says: got len=57 sha256=a3d659831fdb756840377edc343a25ce8a1764d6562979fdc9ea8397ee044d9e
entrypoint says: ready pid=1 ppid=0
ok the entrypoint received the exact string (length and sha256 match)
Config.Env: ["READ=1","HANDLE=1","PATH=...","NODE_VERSION=24.21.0","YARN_VERSION=1.22.22"]
Config.Cmd: ["node","/t/stdin-cap.mjs"] Path: docker-entrypoint.sh Args: ["node","/t/stdin-cap.mjs"]
ok capability absent from docker inspect (whole document, incl. Config.Env, Args, Cmd)
ok capability absent from docker top
ok capability absent from docker logs
ok capability absent from /proc/1/environ
ok capability absent from /proc/1/cmdline
ok capability absent from the host process list (ps -eo args)
stdin of pid 1 after the line: pipe:[1955323506]
## 2. signals: what pid 1 receives, and the exit status
stop-plain: [] handle=0 action='stop -t 3' -> running=false exit=137 stopped-after-ms=3128 log=ready pid=1 ppid=0
ok docker stop, no init, no handler: pid 1 ignores SIGTERM, killed by SIGKILL after the grace period (exit 137, >= 3 s)
stop-handler: [] handle=1 action='stop -t 3' -> running=false exit=0 stopped-after-ms=17 log=ready pid=1 ppid=0;signal SIGTERM pid=1
ok docker stop, no init, handler: pid 1 receives SIGTERM as pid=1, exits 0 promptly
stop-init: [--init] handle=0 action='stop -t 3' -> running=false exit=143 stopped-after-ms=18 log=ready pid=7 ppid=1
ok docker stop, --init, no handler: tini forwards SIGTERM, the default action ends node (exit 143, prompt)
stop-init-hand: [--init] handle=1 action='stop -t 3' -> running=false exit=0 stopped-after-ms=28 log=ready pid=7 ppid=1;signal SIGTERM pid=7
ok docker stop, --init, handler: node receives SIGTERM as a child, not pid 1
kill-plain: [] handle=0 action='kill' -> running=false exit=137 stopped-after-ms=22 log=ready pid=1 ppid=0
ok docker kill (SIGKILL), no init: exit 137
kill-handler: [] handle=1 action='kill' -> running=false exit=137 stopped-after-ms=21 log=ready pid=1 ppid=0
ok docker kill (SIGKILL), handler: handler never runs, exit 137
kill-init: [--init] handle=0 action='kill' -> running=false exit=137 stopped-after-ms=21 log=ready pid=7 ppid=1
ok docker kill (SIGKILL), --init: exit 137
killterm-plain: [] handle=0 action='kill -s TERM' -> running=true exit=0 stopped-after-ms=n/a log=ready pid=1 ppid=0
ok docker kill -s TERM, no init, no handler: ignored, container still running
killterm-init: [--init] handle=0 action='kill -s TERM' -> running=false exit=143 stopped-after-ms=20 log=ready pid=7 ppid=1
ok docker kill -s TERM, --init, no handler: ends the container (exit 143)
killterm-hand: [] handle=1 action='kill -s TERM' -> running=false exit=0 stopped-after-ms=23 log=ready pid=1 ppid=0;signal SIGTERM pid=1
ok docker kill -s TERM, no init, handler: pid 1 receives it, exit 0
cleanup: no container, network, image or scratch directory labelled s8-stdin-capability is left
result: pass
@@ -0,0 +1,72 @@
# s9-hardening 2026-10-12T22:29:42Z
endpoint unix:///run/user/1000/docker.sock: name=rootless
scratch /home/jwoltje/darkwing-scratch/rv86/tmp/i1spike-s9-hardening.x4qdx4
## the daemon
cgroup version 2, driver systemd, warnings null
controllers delegated to the user manager: cpuset cpu io memory pids
## control: no hardening flags (the defaults), as container root and as uid 1000
user 0:0 {"uid":0,"CapEff":"00000000a80425fb","CapBnd":"00000000a80425fb","NoNewPrivs":"0","Seccomp":"2","pidsMax":"76510","memoryMax":"max","swapMax":"max","cpuMax":"max 100000","writeRoot":"ok","writeTmp":"ok"}
ok control (0:0): default CapEff is non-zero (00000000a80425fb)
ok control (0:0): NoNewPrivs 0 without the flag
ok control (0:0): default seccomp filter active (Seccomp: 2)
ok control (0:0): pids.max is the cgroup default, not 32 (76510)
ok control (0:0): no memory limit (max)
ok control (0:0): root filesystem is writable
user 1000 {"uid":1000,"CapEff":"0000000000000000","CapBnd":"00000000a80425fb","NoNewPrivs":"0","Seccomp":"2","pidsMax":"76510","memoryMax":"max","swapMax":"max","cpuMax":"max 100000","writeRoot":"EACCES","writeTmp":"ok"}
ok control (1000): a non-root uid already has CapEff 0 and cannot write / (EACCES, a permission, not a mount)
## each flag alone: does the daemon accept it quietly?
--cap-drop ALL (as 0:0) -> exit 0; 0 warning line(s)
ok cap-drop ALL: CapEff 0 even for container root
--security-opt no-new-privileges -> exit 0; 0 warning line(s)
ok no-new-privileges: NoNewPrivs 1
--read-only -> exit 0; 0 warning line(s)
ok read-only: writes to / fail with EROFS, /tmp too without tmpfs
--read-only --tmpfs /tmp -> exit 0; 0 warning line(s)
ok read-only + tmpfs: /tmp writable, / not
--pids-limit 32 -> exit 0; 0 warning line(s)
ok pids-limit: pids.max 32
--memory 128m -> exit 0; 0 warning line(s)
ok memory: memory.max 134217728
--memory 128m --memory-swap 128m -> exit 0; 0 warning line(s)
ok memory-swap equal to memory: memory.swap.max 0
--cpus 0.5 -> exit 0; 0 warning line(s)
ok cpus: cpu.max '50000 100000'
## all together, read back from docker inspect and from inside
HostConfig.CapDrop ["ALL"]
HostConfig.CapAdd null
HostConfig.SecurityOpt ["no-new-privileges"]
HostConfig.ReadonlyRootfs true
HostConfig.Tmpfs {"/tmp":"rw,noexec,nosuid,size=16m"}
HostConfig.PidsLimit 32
HostConfig.Memory 134217728
HostConfig.MemorySwap 134217728
HostConfig.NanoCpus 500000000
HostConfig.Privileged false
HostConfig.CgroupnsMode "private"
ok inspect: CapDrop is [ALL]
ok inspect: SecurityOpt carries no-new-privileges
ok inspect: ReadonlyRootfs true
ok inspect: PidsLimit 32
ok inspect: Memory 134217728
ok inspect: NanoCpus 500000000
ok inspect: not privileged
inside: {"uid":1000,"CapEff":"0000000000000000","CapBnd":"0000000000000000","NoNewPrivs":"1","Seccomp":"2","pidsMax":"32","memoryMax":"134217728","swapMax":"0","cpuMax":"50000 100000","writeRoot":"EROFS","writeTmp":"ok","fork":{"attempted":200,"started":24,"refused":176}}
ok inside: CapEff 0, CapBnd 0
ok inside: NoNewPrivs 1
ok inside: Seccomp 2 (default profile still on)
ok inside: pids.max 32
ok inside: memory.max 134217728, swap 0
ok inside: cpu.max 50000 100000
ok inside: / read-only, /tmp tmpfs writable
fork burst: 200 attempted, 24 started, 176 refused
ok the pids limit holds: some spawns refused, started <= 32
/tmp mount inside: tmpfs /tmp tmpfs rw,nosuid,nodev,noexec,relatime,size=16384k,uid=1000,gid=1000,inode64,huge=advise 0 0
## memory limit enforced
exit 137; OOMKilled=true; output tail: {"uid":1000,"CapEff":"0000000000000000","CapBnd":"0000000000000000","NoNewPrivs":"1","Seccomp":"2","pidsMax":"32","memor
ok allocating 400 MiB under a 128 MiB limit is OOM-killed (exit 137)
## things the flags do not stop (recorded, not asserted as hardening)
container root with the same flags (user 0:0) maps to host uid 1000; CapEff:
0000000000000000
ok container root with cap-drop ALL has CapEff 0
cleanup: no container, network, image or scratch directory labelled s9-hardening is left
result: pass
@@ -0,0 +1,51 @@
## Row 86 round 1 review (Darkwing): approve
**Verdict: approve**, commit `ad0bdc20` of `agents/researcher/work/i1-spikes/`, candidate manifest `0381c233794f61aa98765b8b7c69bc9dd25b2e31702ab7beca556da73c4a586e`. The packet does what the brief's "I1 spikes" section asks, inside its rules, and I found nothing that contradicts decisions 105 to 108. Nothing blocks. The notes below are for Sage's gate rerun and the design row.
### What I checked
- **Tree.** `sha256sum -c MANIFEST.sha256` from the packet directory gives 32 of 32 OK, and the manifest's own sha256 is the candidate. `git log ad0bdc20..4835a8e2` on the packet and on the brief is empty.
- **Read.** All of `REPORT.md`, `lib.sh`, `s0` to `s9` and the nine files under `tools/`, against the brief's rules:
- every `docker` call goes through `D()`, which names `unix:///run/user/1000/docker.sock`, and `spike_begin` refuses with exit 2 unless the info lists `name=rootless`. The one call to the system socket is S1's `info`, which the brief asks for;
- the node image is pinned by digest. `Dockerfile.clis` installs `@anthropic-ai/[email protected]` and `@earendil-works/[email protected]` inside the image. `package.json` pins pi at 0.85.1, and `~/.local/bin/claude --version` prints 2.1.296;
- every bind source is under the spike's `mktemp -d` scratch directory. No named volume, no `--privileged`, no engine socket in a container, no daemon change;
- no credential. S4's one capability is a throwaway the temp broker issues for a made-up business, on a temp dataRoot under the scratch directory;
- every container, network and image carries `mosaic.i1spike=<spike>`, and `spike_end` removes them and the scratch directory, then turns any leftover into exit 2.
- **Citations.** `packages/bus/src/server.mjs:8-11` and `43-56`, `packages/bus/src/runtime.mjs:66-70`, `packages/seat/src/session.mjs:63` (`ANTHROPIC_BASE_URL` is in `ENV_ALLOW`) and the `close()` path in `packages/cli/src/launcher.mjs:393-411` all say what the report quotes.
- **Decisions.** I read 105 to 108 in `docs/plans/2026-09-26_lead-decisions.md`. I agree there's no contradiction. The report's four open items are the right ones for the design row, and the first one, how a containerized broker authenticates a human once the pid proof can't run, is the hard one.
- **Committed outputs.** All ten `out/*.out` files end in `result: pass` with no `FAIL` line.
### My rerun
In a detached worktree of `ad0bdc20` under `~/darkwing-scratch/rv86/`, with `TMPDIR` there, after a busy check that aborts. I ran the spikes that need no build and no provider traffic, then S0 to clean up:
| Spike | Exit | Assertions | UTC |
|---|---|---|---|
| S1 endpoint pin | 0 | 6 ok | 22:29:05Z to 22:29:06Z |
| S2 uid mapping | 0 | 23 ok | 22:29:06Z to 22:29:17Z |
| S4 hostile client | 0 | 21 ok | 22:29:17Z to 22:29:24Z |
| S8 stdin capability | 0 | 18 ok | 22:29:24Z to 22:29:42Z |
| S9 hardening | 0 | 32 ok | 22:29:42Z to 22:29:47Z |
| S0 clean images | 0 | 7 ok | 22:29:47Z |
Each count matches the committed output, with no `FAIL` line. S2 pulled the node image by digest first; S1 needs no image. The script and its outputs are in `r1/` here.
I left S3, S5, S6 and S7 to Sage's gate. S6 and S7 build the CLI image and reach the providers, and S5 reaches the network. Afterwards `ps -a` showed no rows, `network ls` showed `bridge host none`, and `image ls -a` showed `alpine:3`, the same end state the report gives.
### Notes (non-blocking)
N1. **A rerun in place rewrites tracked files.** `spike_begin` tees into `$HERE/out/$SPIKE.out`, and "How to run" says to `cd` into the packet and run there. In the canonical checkout that leaves the ten committed outputs modified. For the gate, run in a detached worktree of `ad0bdc20` (`REPO` is four levels up from the packet, so a worktree keeps S4's broker import working), then compare `result:` lines. That's what I did, and the worktree came out with those six files modified, as expected.
N2. **S0 checks the whole daemon.** It asserts no container of anyone, only `bridge host none`, no volume, and it runs `builder prune -f`, which empties the whole build cache. That's true and harmless today, since the daemon held only `alpine:3` before the spikes. Once I2 or anything else uses the rootless daemon, S0 fails, or deletes someone else's build cache. Scope its checks to the label, or retire S0, before that.
N3. **One stale line.** `REPORT.md:38` says "Nothing was committed or posted". The packet is committed at `ad0bdc20`.
N4. **S6 sends key-shaped strings to the real providers.** The brief says "with no credential". The `FAKE` and `OAT` values aren't credentials, and the report explains why a key-shaped value is needed: with none, neither CLI makes a model request. I accept it. I'm naming it because it's traffic to api.anthropic.com and api.openai.com with a key-shaped header, which the brief didn't spell out.
N5. **The spike tools aren't production patterns.** `connect-proxy.mjs` reads only the first chunk (`client.once("data")`), so a CONNECT line split across two reads is refused as malformed, and any bytes after the header in that chunk are dropped. `sockserver.mjs` sets the socket's mode after `listen`, so for a moment it has the umask's mode. Neither matters to what the spikes measure. The design row shouldn't copy either. For the broker, choice 3's own directory per socket is the real gate, not the socket's mode.
N6. **Tooling gap.** `scripts/mosaic queue review verify-commit 86 HEAD` reports every file "missing", because the manifest's paths are relative to the packet directory and the tool resolves them from the repository root. Check this packet with `sha256sum -c` from the packet directory. The tool could learn to try the manifest's own directory.
N7. **Dead code.** `RUNFLAGS()` in `lib.sh:51` has no caller, and its comment mentions `--rm`, which it doesn't print.
Approve for commit `ad0bdc20` and the candidate above.