F3b: close the review's HIGH + two MEDIUMs, with a door inventory
The reviewer found a second containment escape in this component, one phase after F3a's: recovery renamed through untrusted manifest paths with no containment check, while the forward door refuses the identical ../ escape. The leader required a DOOR INVENTORY, not a point fix -- every coordinator path that writes/renames/deletes a path NOT from a freshly-validated request, with each one's containment status. The table is in the task note; every escaping door was in recovery (trusting manifest bytes) plus the sweep's depth gap. All forward doors already resolve through require-contained-path.
HIGH (reproduced by the author before fixing -- outside victim changed): recover-complete! now runs every manifest target/temp/backup through a real-path containment check FIRST, before proving anything or touching a file; a path escaping the root is a corrupt/hostile manifest and joins the refuse-to-a-human class -- its extent cannot be established, so nothing is renamed or deleted. Verified BEHAVIOURALLY (the refusal fires and the outside file is untouched) because an intervening stale build made a source-only check pass silently -- the estate's own trap; the behavioural probe is what cut through it.
MEDIUM 1: sweep-debris! walks the full tree (folio slugs nest -- debris two dirs deep is the normal shape here), skipping directory symlinks and deleting only files whose real path is under the real root.
MEDIUM 2: discovery drops dot-prefixed names without a census record, so population-guard reported COMPLETE over a root with a .hidden note -- the guard failing at its own job. Rather than change discovery's shared dotfile skip (it correctly hides this coordinator's .folio-* artifacts), the guard independently scans the root's top level for any non-artifact dot-entry and refuses every root-wide destructive op on it.
Battery grows to 15 arms: b13 (escaping manifest refused to a human, outside file untouched, manifest kept), b14 (depth-2 debris swept), b15 (dot-directory refuses delete AND rename, artifacts do not trip it). 15/15 green.
src/folio/doc/manifest.sgl | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
src/folio/doc/structural.sgl | 46 +++++++++++++++++++++++++++++++++++++++++-----
test/f3b-probe.sgl | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
test/gate-f3b.sh | 6 +++---
4 files changed, 220 insertions(+), 36 deletions(-)src/folio/doc/manifest.sglmodified
(define (hash-of-file path) (guard (e (#t #f)) (revision-of-file path))) ;; THE MANIFEST IS UNTRUSTED INPUT AT RECOVERY TIME. A crashed writer -- ;; or anything that can drop a file into the root -- authored its fields, ;; and recovery renames and deletes paths built from them. The forward ;; engine only ever writes root-relative in-root paths, but recovery must ;; not TRUST that: a manifest whose target/temp/backup contains `../` ;; makes recovery rename OUTSIDE the root (reviewer HIGH, 2026-08-25, ;; reproduced -- `outside victim changed: #t`). Every manifest path is ;; resolved through real-path containment before it is acted on, exactly ;; as the forward doors resolve a request's path; anything escaping is a ;; corrupt/hostile manifest and joins the refuse-to-a-human class -- the ;; extent of a hostile recovery cannot be established, so nothing is ;; renamed or deleted. The check is on the PARENT dir (where the rename ;; lands / the file sits), matching `require-contained-path`'s domain. (define (path-contained? root rel) (let ((root-real (realpath root))) (and root-real (let* ((joined (path-join root-real rel)) (parent-real (realpath (path-dirname joined)))) (and parent-real (or (equal? parent-real root-real) (string-starts-with? parent-real (string-append root-real "/")))))))) (define (manifest-paths-contained? root entries) (let loop ((rest entries)) (cond ((null? rest) #t) ((and (path-contained? root (mentry-target (car rest))) (path-contained? root (mentry-temp (car rest))) (path-contained? root (mentry-backup (car rest)))) (loop (cdr rest))) (else #f)))) ;;; RECOVERY. Runs inside the root lock (the coordinator calls it before ;;; accepting any mutation). Returns a LIST OF ACTION STRINGS -- '() when ;;; there was nothing to recover -- which the coordinator threads into the (let ((txn-id (list-ref parsed 0)) (op-label (list-ref parsed 1)) (entries (list-ref parsed 2))) ;; Containment FIRST, before proving anything else and before ;; touching any file: a path that escapes the root cannot be ;; safely renamed OR rolled back, and it is the signature of a ;; corrupt or hostile manifest, so it is refused to a human like ;; any other unprovable state. (unless (manifest-paths-contained? root entries) (error (format "recovery: the transaction manifest at ~a/~a names a path outside the root. A manifest is written only with in-root relative paths, so this one is corrupt or hostile; recovery will not rename or delete along it. A human must inspect the manifest and the .folio-backup-* files." root manifest-file-name))) ;; First pass: PROVE every entry is finishable before touching ;; anything -- completion is all-or-refuse, not best-effort. (for-each actions)))) ;;; Sweep staged temps and backups from a transaction that never reached ;;; its commit point. Only THIS MODULE'S prefixes, only at the root's top ;;; level and one directory deep (where notes live); nothing else is ;;; touched, and every removal is reported. ;;; its commit point. Only THIS MODULE'S prefixes; every removal reported. ;;; ;;; DEPTH IS UNBOUNDED, matching discovery: folio slugs nest routinely ;;; (topics/, archive/tasks/), so a cross-file transaction stages debris ;;; beside notes two and three directories deep, and a one-level sweep ;;; leaves it to accumulate silently (reviewer MEDIUM, 2026-08-25). The ;;; walk skips directory symlinks -- the same cycle hazard discovery ;;; refuses to descend -- so it cannot loop, and it deletes ONLY a file ;;; whose real path is under the real root, so a symlinked debris-named ;;; file pointing outside cannot make the sweep delete out-of-root. (define (sweep-debris! root) (let ((actions '())) (for-each (lambda (dir) (for-each (lambda (name) (when (or (string-starts-with? name staging-prefix) (string-starts-with? name backup-prefix)) (let ((full (path-join dir name))) (guard (e (#t #f)) (delete-file full)) (set! actions (cons (format "recovery: ROLLED BACK (swept pre-commit debris ~a -- no manifest existed, so no target was ever renamed)" full) actions))))) (guard (e (#t '())) (directory-list dir)))) (cons root (filter (lambda (p) (directory-exists? p)) (map (lambda (n) (path-join root n)) (guard (e (#t '())) (directory-list root)))))) (reverse actions))) (define (filter pred xs) (let loop ((rest xs) (out '())) (cond ((null? rest) (reverse out)) ((pred (car rest)) (loop (cdr rest) (cons (car rest) out))) (else (loop (cdr rest) out))))))) (let ((root-real (realpath root)) (actions '())) (let walk ((dirs (list root))) (unless (null? dirs) (let ((dir (car dirs))) (let loop ((names (guard (e (#t '())) (or (directory-list dir) '()))) (subdirs '())) (cond ((null? names) (walk (append subdirs (cdr dirs)))) (else (let ((full (path-join dir (car names)))) (cond ((and (directory? full) (not (symlink? full))) (loop (cdr names) (cons full subdirs))) ((and (not (directory? full)) (or (string-starts-with? (car names) staging-prefix) (string-starts-with? (car names) backup-prefix))) (let ((parent-real (realpath (path-dirname full)))) (when (and root-real parent-real (or (equal? parent-real root-real) (string-starts-with? parent-real (string-append root-real "/")))) (guard (e (#t #f)) (delete-file full)) (set! actions (cons (format "recovery: ROLLED BACK (swept pre-commit debris ~a -- no manifest existed, so no target was ever renamed)" full) actions)))) (loop (cdr names) subdirs)) (else (loop (cdr names) subdirs)))))))))) (reverse actions)))))src/folio/doc/structural.sglmodified
(folio doc inspect) (folio doc discovery) (folio doc manifest) (folio doc lock) (folio doc coordinator) (folio doc transaction)) (export ;; outside the document population the ruled independent pass covers. ;; Returns #f when the census is trustworthy, or a refusal naming what ;; is not. (define (population-guard census) (define (population-guard coord census) (let ((unwalkable (append (map (lambda (d) (list d 'unreadable-directory)) ((eq? (skip-reason (car rest)) 'symlinked-directory) (loop (cdr rest) (cons (list (skip-path (car rest)) 'symlinked-directory) out))) (else (loop (cdr rest) out))))))) (else (loop (cdr rest) out)))) ;; DISCOVERY DROPS DOT-PREFIXED NAMES WITHOUT A CENSUS RECORD ;; (entries-of skips them, files AND directories), so a note ;; in `.hidden/` or a `.foo.sgd` is invisible and the census ;; still reads COMPLETE -- the guard failing at its own job ;; (reviewer MEDIUM, 2026-08-25). discovery's skip is shared ;; with F2a/F2b and correctly hides this coordinator's OWN ;; `.folio-*` artifacts; so rather than change it, the guard ;; independently scans the root's top level for any dot-entry ;; that is NOT one of those artifacts and treats it as an ;; unwalked location. Top level is where a `.hidden` dir or a ;; dot-note would sit; anything under it is unreachable by ;; discovery regardless. (dot-intruders coord census)))) (if (null? unwalkable) #f (%txn-refuse 'population-unprovable (length unwalkable) unwalkable) retryable: #t)))) ;; The coordinator's own well-known artifacts, which discovery is RIGHT ;; to hide. A dot-entry matching one of these prefixes is expected; any ;; other dot-entry is a note or directory discovery silently dropped. (define (artifact-dot-name? name) (or (equal? name coordination-file-name) (equal? name transaction-manifest-name) (string-starts-with? name staging-prefix) (string-starts-with? name backup-prefix) (string-starts-with? name ".folio-coordination-probe-"))) (define (dot-intruders coord census) (let ((root (coordinator-root coord))) (let loop ((names (guard (e (#t '())) (or (directory-list root) '()))) (out '())) (cond ((null? names) (reverse out)) ((and (string-starts-with? (car names) ".") (not (artifact-dot-name? (car names)))) (loop (cdr names) (cons (list (path-join root (car names)) 'hidden-unwalked) out))) (else (loop (cdr names) out)))))) (define (root-population coord) ;; Returns (census . entries) where each entry is ;; (list rel-path abs-path format bytes-or-#f). Bytes are read for (let* ((doomed (cons node-id (%txn-subtree-declared-ids node))) (doomed (filter string? doomed)) (population (root-population coord)) (guard-refusal (population-guard (car population))) (guard-refusal (population-guard coord (car population))) (found (find-referrers coord (cdr population) doomed)) (semantic (list-ref found 0)) (raw-only (list-ref found 1)) (let* ((population (root-population coord)) (census (car population)) (entries (cdr population)) (guard-refusal (population-guard census))) (guard-refusal (population-guard coord census))) (if guard-refusal guard-refusal ;; 1. The NEW id must be completely fresh -- no boundary occurrence ;; discipline as rename. (let* ((root (coordinator-root coord)) (population (root-population coord)) (guard-refusal (population-guard (car population))) (guard-refusal (population-guard coord (car population))) (entries (cdr population)) (found (find-referrers coord entries moved-ids)) (semantic (list-ref found 0))test/f3b-probe.sglmodified
(assert-true "bystander.md untouched by the entire battery" (equal? (revision-of-file (rooted "bystander.md")) bystander-hash))));; ---------------------------------------------------------------------------;; b13-recovery-containment -- the reviewer's HIGH: a manifest whose target;; escapes the root via `..` must REFUSE TO A HUMAN and rename nothing.;; Recovery treats the on-disk manifest as UNTRUSTED input.;; ---------------------------------------------------------------------------(arm "b13-recovery-containment" (lambda () ;; A private escape root so the outside file is disposable. (let* ((base (path-dirname root)) (esc-root (path-join base "b13-esc-root")) (esc-out (path-join base "b13-esc-outside")) (victim (path-join esc-out "victim.sgd"))) (unless (directory-exists? esc-root) (make-directory esc-root)) (unless (directory-exists? esc-out) (make-directory esc-out)) (write-file-bytes victim (read-file-bytes (rooted "ref-one.sgd"))) (write-file-string (path-join esc-root ".folio-staging-esc.tmp") "PAYLOAD: recovery wrote outside the locked root\n") (let ((tr (revision-of-file (path-join esc-root ".folio-staging-esc.tmp"))) (vr (revision-of-file victim))) (write-file-bytes (path-join esc-root ".folio-backup-esc") (read-file-bytes victim)) (write-transaction-manifest! esc-root "b13" "rename-id" (list (mentry "../b13-esc-outside/victim.sgd" vr ".folio-staging-esc.tmp" tr tr ".folio-backup-esc" vr))) (let* ((victim-before (revision-of-file victim)) (coord2 (make-coordinator esc-root svc)) (raised (guard (e (#t (if (exception? e) (exception-message e) "raised"))) (folio-insert-node! coord2 (path-join esc-root "x.sgd") "c" 'update "sha256:0000000000000000000000000000000000000000000000000000000000000000" timestamp: "2026-08-25" id: "u-z") #f))) (assert-true "recovery refused the escaping manifest to a human" (and (string? raised) (string-contains? raised "human"))) (assert-true "the file outside the root was NOT written" (equal? (revision-of-file victim) victim-before)) (assert-true "the manifest is kept for the human" (file-exists? (path-join esc-root ".folio-transaction-manifest"))))))));; ---------------------------------------------------------------------------;; b14-sweep-depth -- the reviewer's MEDIUM 1: pre-commit debris nested TWO;; directories deep is swept by the rollback branch, not left to accumulate.;; ---------------------------------------------------------------------------(arm "b14-sweep-depth" (lambda () (let ((d0 (rooted ".folio-staging-b14-d0.tmp")) (d1 (rooted "sub14/.folio-staging-b14-d1.tmp")) (d2 (rooted "sub14/deep14/.folio-backup-b14-d2"))) (unless (directory-exists? (rooted "sub14")) (make-directory (rooted "sub14"))) (unless (directory-exists? (rooted "sub14/deep14")) (make-directory (rooted "sub14/deep14"))) (write-file-string d0 "d0") (write-file-string d1 "d1") (write-file-string d2 "d2") ;; No manifest exists -> the next transaction's recovery rolls back. (let ((r (folio-insert-node! coord (rooted "dest.sgd") "incoming" 'update (revision-of-file (rooted "dest.sgd")) timestamp: "2026-08-25" id: "u-b14"))) (assert-txn "the transaction runs" r) (assert-true "depth-0 debris swept" (not (file-exists? d0))) (assert-true "depth-1 debris swept" (not (file-exists? d1))) (assert-true "depth-2 debris swept" (not (file-exists? d2)))))));; ---------------------------------------------------------------------------;; b15-dot-population -- the reviewer's MEDIUM 2: discovery drops dot-prefixed;; names without a census record, so a referrer in a dot-directory is unseen.;; The population guard must independently detect the dot-entry and refuse;; every root-wide destructive op (delete AND rename) rather than proceed;; against a population it cannot prove complete.;; ---------------------------------------------------------------------------(arm "b15-dot-population" (lambda () ;; A fresh, unreferenced node so the population guard (not unknown-node or ;; node-referenced) is the reason under test. (assert-txn "seed a fresh target" (folio-insert-node! coord (rooted "owner.sgd") "work" 'task (revision-of-file (rooted "owner.sgd")) id: "t-b15" title: "Guard subject" status: 'ready)) (unless (directory-exists? (rooted ".hidden15")) (make-directory (rooted ".hidden15"))) (write-file-string (rooted ".hidden15/note.sgd") "#lang folio/doc\n@note[\"topics/h15\" title: \"H\"]\n@p{prose only, no referrer}\n@end note\n") (assert-refusal "delete refuses over a dot-directory population" (folio-delete-node! coord (rooted "owner.sgd") "t-b15" (revision-of-file (rooted "owner.sgd"))) 'population-unprovable) (assert-refusal "rename refuses over the same population" (folio-rename-id! coord "t-b15" "t-fresh15" (revision-of-file (rooted "owner.sgd"))) 'population-unprovable) ;; And the coordinator's OWN dot-artifacts must NOT trip the guard: after ;; removing the intruder, an ordinary op proceeds. (delete-file (rooted ".hidden15/note.sgd")) (delete-directory (rooted ".hidden15")) (assert-txn "an ordinary op proceeds once the dot-intruder is gone" (folio-insert-node! coord (rooted "owner.sgd") "work" 'update (revision-of-file (rooted "owner.sgd")) timestamp: "2026-08-25" id: "u-b15"))))(println "F3B_PROBE_DONE arms=~a green=~a red=~a" (+ (car greens) (car reds)) (car greens) (car reds))(exit (if (= (car reds) 0) 0 1))test/gate-f3b.shmodified
if want 2; then r2="$work/b"; fresh_f3b_root "$r2" F3B_ROOT="$r2" run_sgl test/f3b-probe.sgl >"$work/b.log" 2>&1 || fail "arm2: battery exited non-zero" grep -q "F3B_PROBE_DONE arms=12 green=12 red=0" "$work/b.log" \ || { fail "arm2: expected 12/12; got $(grep F3B_PROBE_DONE "$work/b.log" || echo none)"; \ grep -q "F3B_PROBE_DONE arms=15 green=15 red=0" "$work/b.log" \ || { fail "arm2: expected 15/15; got $(grep F3B_PROBE_DONE "$work/b.log" || echo none)"; \ grep "_RED" "$work/b.log" >&2 || true; } arm_summary 2fiecho "---"if [ "$fails" -eq 0 ]; then echo "F3B_GATE_GREEN arms_run=[$only_arms] f3b_arms=12 f3a_arms=22 kill_rounds=5 sha=$(git -C "$repo_dir" rev-parse --short HEAD 2>/dev/null || echo unknown)" echo "F3B_GATE_GREEN arms_run=[$only_arms] f3b_arms=15 f3a_arms=22 kill_rounds=5 sha=$(git -C "$repo_dir" rev-parse --short HEAD 2>/dev/null || echo unknown)" exit 0else echo "F3B_GATE_RED failures=$fails arms_run=[$only_arms]"