Flows through the code
dev branch. What happens, module by module and syscall by syscall, when a command is confined and when the acceptance test runs; then the flow the launcher will add, which is designed and not built. Line numbers are from the files on that date and will drift; the function names will not.1. sharness-ai run --policy p.json --expect-hash sha256:M -- cmd
cli.main parse: --policy, --expect-hash, --verbose, argv after "--"
│
▼
policy.load(path) JSON file → dict; refuses non-objects and non-JSON
policy.check(policy) the v0 shape: pbp=1, id, files.read/write/exec (absolute paths),
network "none" | {connect_tcp_ports}, process.no_new_privs: true
policy.policy_hash(policy) canonical bytes (sorted keys, no whitespace, UTF-8, integers only)
→ "sha256:" + hex ← E02
│
├── expect_hash given and ≠ hash → print "refused … Nothing ran.", exit 125
├── check found problems → exit 125
├── landlock.abi() == 0 → "Landlock is not available", exit 126
│
▼
run.run: os.fork()
│
├─ parent: waitpid(child) → the child's exit code; removes the child's cgroup
│
└─ child: run.apply(policy)
1. network "none" → run._unshare_net(): unshare(CLONE_NEWNET)
EPERM? → unshare(CLONE_NEWUSER | CLONE_NEWNET)
still refused? → rely on Landlock TCP rules (UDP uncovered)
1b. caps (process.max_memory_mb, max_pids): limits.cgroup() makes a memory and pids cgroup
for this pid where /sys/fs/cgroup is writable; limits.rlimits() sets RLIMIT_AS, RLIMIT_NPROC
2. landlock.Ruleset(): landlock_create_ruleset(2) with every fs right the ABI knows,
+ bind/connect TCP (ABI 4+), + signal/abstract-socket scoping (ABI 6+)
3. for each path: landlock_add_rule(2) PATH_BENEATH with O_PATH fd and the rights:
read → READ_FILE | READ_DIR
exec → EXECUTE | READ_FILE | READ_DIR
write → every right of the ABI
a missing path is skipped and reported; a plain file gets file rights only
for each port: landlock_add_rule(2) NET_PORT with CONNECT_TCP
4. ruleset.restrict_self(): prctl(PR_SET_NO_NEW_PRIVS, 1); landlock_restrict_self(2)
→ final for this process and every descendant, for ever
5. environ: SHARNESS_AI_POLICY_HASH, SHARNESS_AI_POLICY_ID
6. os.execvp(cmd) the command runs under the policy; anything it starts inherits it
(any failure before exec → stderr, _exit(126))
Why fork then apply, and not apply then fork: restrict_self is irreversible for the calling process, so it is only ever called in a child. The shell you typed run into is never restricted. That is also why the result is a Boundary for the child and a Setting for the caller.
2. sharness-ai hello
hello.hello()
│
▼
hello.hello_policy() a deny-all for *this* interpreter: read and exec the system prefixes,
sys.base_prefix and the interpreter's directory; read the workspace;
write nothing; network "none"
policy.policy_hash() printed, so the person can keep it
│
▼
write the policy to a temporary file
for each probe (read, write-tmp, write-cwd, net, memory):
subprocess: python -m sharness_ai run --policy <tmp> -- python -I -c <probe> ← the flow above, end to end
hello._classify(): "allowed" if the probe printed ok; "refused (…)" on PermissionError / OSError
one more: run … --expect-hash sha256:000… → must be refused with "Nothing ran" (exit 125)
│
▼
compare against EXPECT; print the table, "N/5 held", and the caveat
delete the temporary policy; exit 0 if every expectation held
hello deliberately goes through the CLI (python -m sharness_ai run) rather than calling run.run in-process, so the acceptance test exercises the same path a person types.
3. sharness-ai probe
probe.probe() reads /proc/self/status (uid, capability sets, NoNewPrivs, Seccomp), calls landlock_create_ruleset(2) with the version flag to get the ABI, tries memfd_secret(2) (syscall 447) and closes the fd, lists /proc/self/ns, looks at /sys/fs/cgroup, reads the relevant keys from /proc/config.gz, checks which tools are on the path, and lists the names of proxy variables that are set. probe.render() prints it as text; --json prints the dict. Nothing is written.
4. The policy hash, in two implementations
Python (policy.canonical) Browser (explain/mockups/policy-hash.html)
refuse floats refuse non-integers
json.dumps(obj, sort_keys=True, keys sorted by code point, recursively;
separators=(",", ":"), JSON.stringify for strings, integers, booleans, null
ensure_ascii=False) joined with "," and ":"
.encode("utf-8") TextEncoder (UTF-8)
hashlib.sha256 crypto.subtle.digest("SHA-256")
"sha256:" + hexdigest "sha256:" + hex
The test suite pins one vector; the deny-all example's hash appears on this site and in the tests, so a change to either implementation fails a build.
5. What the launcher adds (designed)
sharness-ai launch --manifest m.json --expect-hash sha256:M -- claude … (not built)
1. canonicalise the manifest, hash it, refuse unless it equals M (or carries the root key's signature)
2. for each component: useradd an ephemeral uid ~49 ms, measured
3. make a P-256 session CA; issue one certificate per role (SAN URI spiffe://<session>/<role>);
destroy the CA key; put the CA certificate's hash in the manifest 24 ms, measured
4. start sharness.kms under its policy; it makes the session identity, prints k1 only; freeze it
5. for the orchestrator: apply its Landlock domain (execute allowlist, state folder, two ports),
drop to its uid, no_new_privs, then exec it E06
6. per command: start the other components under their policies (new namespaces ~4 ms), run, tear down
7. at session end: thaw and kill the KMS; delete users, firewall rules, sockets
Steps 2, 3, 4 and 6 reuse what run.apply already does per process. The new pieces are the session CA (a few dozen lines with cryptography) and the orchestrator's domain, which is the process-tree proof of concept, E06.
Sources: the modules cli.py, policy.py, landlock.py, run.py, hello.py and probe.py on the CLI's dev branch, 25 Oct 2026; Components: sharness.launcher; Enforcing the process tree.