From e44d404aecbefc763b02462d36a10b8ee0d7a396 Mon Sep 17 00:00:00 2001 From: XVVH Date: Tue, 14 Jul 2026 20:20:24 -0400 Subject: [PATCH 1/6] W-20 item (3): SI-32 filesystem-attacker-model candidate (ADR 0008, PROPOSED) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-tier attacker model + candidate rules A27.1-A27.4, grounded in a complete publication-site inventory. Spec untouched; SI-32 stays open; authorizes no implementation. Queued for operator ratification. - T1 offline tampering: content-address verification for substitution/corruption/truncation; A23 (global_seq + anchor) for rollback; keys and the R7 unsigned-index gap are the carve-outs - T2 same-uid active writer: no pathname check wins — W-4 containment; every T2 claim labeled "COOP now; W-4 at G-ADVERSARIAL". Super-user out of scope (trust boundary = the Unix account, P7) - T3 human edit: not an attack; M8 + A24/R14 attribution - A27.1 staged-bytes (ratifies RF-20); A27.2 verify-on-read-back, tier-labeled, three read classes; A27.3 per-kind incl. hardlinks; A27.4 containment sentence - Surfaces one genuine T1 gap (R7: unsigned fabric.db meta store-paths trusted while sibling events signed; feeds broker authority via substrate_span) -> proposed RF-41, posture-bounded under SU - One uniformity correction (R3: sync_store fs walk omits .git exclusion + no-follow) - Seven ratification choices enumerated Passed the four-audit battery + an internal adversarial pre-review (7 findings, one blocking: the first draft wrongly folded T1-rollback into content-addressing when A23 owns it -- fixed, promoted to a decision point). Ledger mirrors: spec-issues SI-32 candidate note, roadmap W-20 progress. Co-Authored-By: Claude Fable 5 --- docs/adr/0008-filesystem-attacker-model.md | 297 +++++++++++++++++++++ docs/roadmap.md | 19 +- docs/spec-issues.md | 28 ++ 3 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0008-filesystem-attacker-model.md diff --git a/docs/adr/0008-filesystem-attacker-model.md b/docs/adr/0008-filesystem-attacker-model.md new file mode 100644 index 0000000..dd70cda --- /dev/null +++ b/docs/adr/0008-filesystem-attacker-model.md @@ -0,0 +1,297 @@ +# ADR 0008 — Filesystem attacker model for store publication and read-back + +**Status: PROPOSED — SI-32 ratification candidate, awaiting human +ratification. This ADR is not a specification amendment and authorizes no +implementation. SI-32 remains OPEN. The tier labels, the staged-bytes rule, +the per-kind entry rules, and the containment boundary below are candidate +normative language; none is normative until the human choices in +"Ratification decision points" are ratified and integrated into the schema +spec under the amendment discipline (candidate amendment id: A27). W-20 item +(3).** + +## Context + +The spec assumes content-addressed preparation and coherent restore but never +defines the filesystem adversary those operations run against. RF-20's +remediation (PR #43) implemented a publication discipline — retained verified +bytes, exclusive randomized no-follow siblings, rehash through the retained +handle, same-inode non-symlink recheck immediately before rename, fsync of +file and parent — **without a written threat model saying what those steps +must defeat**. Each review therefore re-derives the attacker and finds a new +residue (the RF-20 → RF-40 chain is five rounds of exactly this). SI-32 asks: +define the attacker in tiers, label every guarantee with the tier it holds +under, and state which publication-safety claims require containment before +they hold. + +This candidate is grounded in a complete inventory of every filesystem +publication and authority-bearing read-back site (the "Publication site +inventory" appendix). The finding that motivates a written model: the code +already has one gold-standard publisher (`write_atomic_verified`, every guard +present) and three verifying read-backs (CAS, payload, recovery journal — all +rehash), but eleven other sites carry partial guard sets whose *sufficiency +cannot be judged without knowing which tier they must survive*. (Two verifying +read-backs — CAS and payload — rehash content; the third, the recovery +journal, verifies its Ed25519 signature.) Two are genuine gaps this model +surfaces (R7 unsigned-meta trust; the T2-unwinnable class); the rest are +tier-appropriate and this model is what lets us say so. + +## The three-tier attacker model (candidate) + +Each tier names a distinct adversary against fabric-home storage; every +guarantee in the spec and the code is labeled with the *weakest* tier under +which it still holds. Tiers are cumulative in capability but **not** in the +honest answer: T1 is defeated by cryptography (content-addressing for +substitution, A23 for rollback), T2 by topology, T3 is not an attack at all. + +**Out of scope (the trust boundary, stated so the tiers are not read as +total).** A super-user or different-uid local adversary is **out of scope** — +it is strictly more capable than T2 and defeats T2's uid-scoped containment +answer by construction. The trust boundary is the Unix account (P7); nothing +in this model defends against local root. This is the deliberate boundary, not +a gap; naming it is what keeps "distinct adversary / cumulative" from reading +as a completeness claim it does not make. + +**T1 — offline storage tampering between processes.** CAS blobs, branch +files, the SQLite databases, key material, or the recovery journal are +changed, substituted, or replanted while no fabric process holds them open +— the backup/restore case, the copied-home case, and the +same-uid-writes-while-idle case. The honest answer for **substitution, +corruption, and truncation** is **content-address verification on every +authority-bearing read-back**: a substituted or truncated byte string fails +its rehash and degrades to a denial, never an authority bypass. T1 is where +"verify what you read, never trust the pathname" wins, with **three +carve-outs** where content-addressing is not the answer: (a) the read target +is the trust root itself (keys — nothing to verify against); (b) an unsigned +index the code still trusts (the R7 gap); and (c) **rollback/replay to a +prior, genuinely-signed, genuinely-content-addressed whole-home state** — +where every blob rehashes correctly and every signature verifies, because the +old state was legitimately produced, so content-addressing is silent by +construction. Rollback's answer is **A23, not content-addressing**: +layer 1's authenticated monotonic `global_seq`/`VerifiedPrefix` detects +partial or internally-inconsistent rollback single-machine now, and the +graduation-gated external anchor (layer 2) detects **whole-home** rollback at +its gates — which content-addressing structurally cannot see. Under the +current **SU** posture the T1 tamperer and the operator are the same Unix +principal (so whole-home rollback is undetectable locally — the operator +controls any local head), making T1's live bite today *accidental* (backup +staleness, a copied home); its adversarial bite arrives at G-MULTITENANT / +when the home leaves the local filesystem boundary, and its confidentiality +half is P17/RF-15. + +**T2 — active same-privilege writer across prepare→publish.** An adversary +of the *same* Unix uid holds an open descriptor, a hardlink, or a racing +thread across a publication's prepare→verify→rename window. Here **no +sequence of pathname checks can win**: the same-inode-before-rename recheck +narrows the race but a sufficiently determined same-uid attacker can still +swap an inode in the sub-microsecond gap, hold a descriptor that survives the +rename, or pre-plant a hardlink. The honest answer is **not a better check — +it is OS-enforced exclusion**: the fabric home reachable by exactly one +principal, and the agent reaching it only through the broker (the two-surface +convention made topology). T2 publication-safety claims therefore hold under +**COOP** (the agent is not adversarial) and **require W-4 containment** to +hold under **G-ADVERSARIAL**. This tier is P7's "trust boundary is the Unix +account," stated as a storage adversary. + +**T3 — the legitimate concurrent human edit.** A human edits the vault while +the fabric is idle or between attested roots. This is **not an attack** and +must never be handled as corruption or silent loss: it lands in **M8 drift +attribution** (`human_local` quiet for the solo operator, `unattributed` +loud), and — during recovery's own downtime window — in the A24/R14 +capture-and-attribute-before-restore path. A model that fails T3 closed +(bricking on an honest edit) or open (overwriting it unrecorded) is wrong in +both directions; T3's correctness criterion is *attributed, never lost*. + +## Candidate normative rules (A27) + +**A27.1 — The staged-bytes rule (normative; ratifies RF-20's discipline).** +A commit consumes bytes **verified in memory during prepare and never +re-reads mutable storage to source the bytes it installs**. Every +store-mutating commit path holds its verified image (fs: per-entry verified +content; sqlite: the whole verified image) in the prepared plan and installs +only those bytes; re-reading a CAS blob, a live file, or any pathname at +commit time *to obtain install bytes* is prohibited. Re-reading live storage +for **comparison or enumeration** is permitted and safe (the fs apply walks +the live tree to enumerate deletions and reads a live target only to skip an +identical-by-hash write — a mismatch triggers a write of already-verified +bytes, a match means the live bytes already equal the verified content): +the invariant is on the *provenance of installed bytes*, not on avoiding all +reads. This is the T1 defense at the write +boundary — it makes prepare the single verification point and commit a pure +function of already-trusted bytes. (As-built: `prepare_restore` retains +verified bytes; `commit_restore`/`write_atomic_verified` consume them; the +`RESTORE-INTEGRITY` contract proves a blob mutated after prepare cannot reach +the live store. This rule ratifies that as the general requirement, not an +implementation accident.) + +**A27.2 — Verify-on-read-back (normative, tier-labeled).** Every read-back of +storage bytes that will *bear authority* MUST content-address-verify (rehash +to the requested address) before the bytes are consumed; a mismatch is a +loud denial, never a fallback. This holds against **T1**. Three classes are +explicitly outside "content-address-verify" and each carries its own rule: +- **Trust-root reads** (key/secret material): there is no address to verify + against — the bytes *are* the trust root. These hold against T1 only by the + storage boundary (0o600, symlink-rejecting open), and their substitution + degrades to downstream signature failure, not silent authority. Labeled + **T1-boundary**, not T1-cryptographic. +- **Signed-object reads** (events, manifests, capabilities, and the recovery + journal / `state_change_recovery` txn): verified by Ed25519 signature, which + subsumes content-addressing (A19). Holds against T1 substitution and, for + order/completeness and the rollback carve-out (c), composes with A23's + `VerifiedPrefix` and anchor. +- **Unsigned-index reads** (the R7 gap): the code reads store paths, spans, + and other operational pointers from unsigned `meta`/index rows that a + substituted `fabric.db` controls, while the *events* in the same DB are + signature-verified. This is the one authority-bearing read-back that + currently trusts a pathname under T1. **Candidate rule:** operational + pointers consumed for authority (which store a capture/restore targets; + which span is the substrate span) MUST derive from signed substrate or be + bound into it, never from an unsigned row a T1 tamperer controls. Reach + note: `substrate_span` is not merely a capture/restore pointer — it feeds + broker authority evaluation (grant ordering, A22 closure), so the gap's + reach is if anything understated. This is A23's seam from the read side: the + anchor proves the head is current, but the store paths and span its events + reference must themselves derive from signed substrate, or a T1 tamperer + redirects them under an otherwise-fresh head. Surfaced as a gap (proposed + RF-41); posture-bounded under SU (the tamperer is the operator) but a real + T1 hole once the home leaves the boundary. + +**A27.3 — Per-kind entry rules (candidate).** Publication safety is stated +per store kind because the fs tree and the SQLite file have different +substitution surfaces: +- **fs-tree stores:** capture and restore reject symlinks (a symlink is not a + canonical entry — this composes with A24/R21's kind-complete recovery + scan); the canonical grammar is regular files and directories only; the + atomic publisher writes through an O_EXCL|O_NOFOLLOW randomized sibling + and rechecks the same non-symlink inode immediately before rename. Excluded + directories (`.git`) are never written, deleted, or (candidate correction) + fsync-walked — the one inconsistency the inventory found (R3: + `sync_store`'s fs walk omits the exclusion and the no-follow open) is a + fsync-only path with no integrity impact but should be brought into line so + "excluded means untouched" is uniform. +- **SQLite stores:** the whole file is the unit; the image is captured + WAL-checkpointed (folded, no live WAL), published through the same atomic + primitive, and its stale `-wal`/`-shm` sidecars removed after the swap so a + restored image cannot be polluted by a leftover WAL. A symlinked store DB is + rejected before use (`symlink_metadata` no-follow). +- **Hardlinks** (the filing's explicit third entry-kind ask): a hardlinked + regular file is captured by *content* like any regular file (capture reads + by value), so a hardlink to in-scope content adds no capture surface. A + hardlink pre-planted at a *publication* target is a T2 same-uid act, not a + pathname-checkable defense — and the atomic publisher already defeats the + write-through-the-link variant structurally: it creates a randomized O_EXCL + sibling and renames *over* the target, replacing the directory entry rather + than writing through any existing link. So hardlinks need no new rule beyond + A27.1/the atomic publisher (write-through variant, covered) and A27.4 (the + pre-plant-and-race variant, COOP/W-4-bounded). Stated so the filing's ask is + visibly closed, not silently dropped. + +**A27.4 — The containment boundary (candidate, the load-bearing tier +statement).** Every T2 publication-safety claim is labeled **"holds under +COOP; requires W-4 containment at G-ADVERSARIAL."** No amount of pathname +checking is claimed to defeat a same-uid active adversary; the spec must not +imply it does. Concretely: `write_atomic_verified`'s same-inode recheck MUST be +documented as *race-narrowing, not race-closing* (as-built its comment claims +neither; this ADR is where the honest label originates), and the security +argument for it MUST name COOP as the assumption it rests on. This is the +honest sentence RF-20→RF-40 kept rediscovering the absence of. + +## Residue disposition (inventory R1–R11 → tier → action) + +| Site | Tier | Disposition | +|---|---|---| +| R1 `Cas::put` no fsync/O_EXCL | T1 (durability) + T1 (integrity) | Integrity held lazily by `Cas::get` rehash (A27.2). Durability fsync is **G-PRODUCTION** (DEBUG posture; same class as the WAL+NORMAL durability seam). Label, don't fix now. | +| R2 `materialize_*` plain write | T1 | Writes CAS-verified bytes into a **branch**, re-captured/re-hashed before bearing authority — outside A27.1's commit boundary. Holds; label as branch-scratch, not a publication. | +| R3 `sync_store` fs walk omits `.git` exclusion + no-follow | T1 | fsync-only, no integrity impact, but breaks "excluded means untouched" uniformity. **Candidate correction** (A27.3); small, file with the ratification or as a follow-up RF. | +| R4 `Cas::get` exists→read TOCTOU | T1 | Rehash makes substitution a denial. Holds (A27.2). | +| R5 `gate_lock` no O_EXCL/O_NOFOLLOW | T2 | Advisory flock; a pre-planted `gate.lock` symlink is a same-uid act → T2/COOP. Label; W-4 owns. | +| R6 key/secret read-back trusts pathname | T1-boundary | Inherent — the trust root. A27.2's trust-root class. Confidentiality is P17/RF-14. | +| **R7 unsigned `fabric.db` meta rows trusted** | **T1** | **The one genuine T1 authority gap.** A27.2's unsigned-index rule; **propose RF-41**, posture-bounded under SU, real at G-MULTITENANT. | +| R8 `write_private_atomic` rename target no-follow | T1-boundary | Plaintext secret, in-memory source; rename replaces a symlink node. Bounded; note under A27.3. | +| R9 journal/sidecar remove no symlink guard | T1 | Fixed paths; removes the link not a target. Minor; note. | +| R10 `capture_sqlite` read no O_NOFOLLOW | T1 | Fabric-internal path, trust-on-capture. Bounded; note. | +| R11 CLI/demo/tooling writes | n/a | Non-authority-bearing (sockets, agent working store, demo fixtures, reports). Out of scope; state so. | + +## Ratification decision points (the human choices) + +1. **Tier count and boundaries.** Three tiers as above, or split T1 into + T1-offline vs T1-at-rest-confidentiality (the latter is P17/RF-15's, and + this model currently folds confidentiality into T1's note rather than a + fourth tier). Recommend: three tiers, confidentiality cross-referenced not + re-tiered. Sub-choice: is the super-user/different-uid out-of-scope + boundary (P7) stated correctly as a boundary rather than a gap? +2. **T1-rollback carve-out (the pre-review's blocking find).** Ratify that + content-addressing answers T1 *substitution/corruption/truncation* but + **not rollback**, whose answer is A23 (layer-1 `global_seq` now, the + graduation-gated anchor for whole-home rollback) — and that under SU + whole-home rollback is undetectable locally, an accepted accidental-only + residual until G-MULTITENANT. This is the seam where SI-32 hands off to + A23; ratifying it wrong (content-addressing "answers T1") ratifies an + unsound tier. Recommend ratify as stated; the alternative is to declare + rollback wholly out of SI-32's scope and purely A23's — cleaner boundary + but leaves the SI-32 reader without the cross-reference. +3. **A27.1 staged-bytes as normative** — ratify RF-20's discipline as the + general rule, or leave it implementation-internal? Recommend normative: + it is the T1 write-boundary invariant and future stores (Tier-2/3) must + inherit it. +4. **A27.2's unsigned-index rule and RF-41.** Is R7 a ratifiable gap to file + now (recommend: yes, posture-bounded, carried when the home-boundary + posture graduates), or folded into P17/RF-15's at-rest work? This decides + whether SI-32 spawns a new RF or reuses one. +5. **A27.4 containment boundary wording** — the "COOP; W-4 at G-ADVERSARIAL" + label on every T2 claim. Ratify as the standing sentence, or scope it + per-site? Recommend standing sentence, since it is the same honest answer + at every T2 site. +6. **R3 correction** — bring `sync_store` into the exclusion/no-follow + discipline now (small), or file as a follow-up? Recommend follow-up RF + (it is fsync-only; not worth growing a threat-model ratification with a + mechanism change). +7. **Where this lands in the spec.** §5.3 (owned-state transition, where + publication lives) plus a §9 fork note, or a new §-level "storage adversary + model" subsection? Recommend a §5.3 subsection cross-referenced from §1 + (payload store) and §9 (F2 addressing), since publication is §5.3's and the + tiers label claims spec-wide. + +## Reserved seams (not resolved here) + +W-4 owns the T2 containment topology (the sandbox whose only door is the +broker); this ADR states the *requirement* that T2 claims rest on it, not the +mechanism. P17/RF-14/RF-15 own at-rest confidentiality (T1's confidentiality +half). G-PRODUCTION owns durability (R1's fsync, the WAL+NORMAL seam). The F2 +CID/DAG-CBOR transition (ADR 0002) changes the *addressing* of CAS blobs but +not this model — A27's rules are stated over content addresses generally, so +they survive the transition; the migration note belongs to W-6/F2, not here. +SI-26 owns the signed-transcript/type binding that would let A27.2's +signed-object class extend to the new §6.2 objects. + +## Validation plan (candidate, on ratification) + +Two-sided contracts per tier claim: a `STAGED-BYTES` contract (positive: a +commit installs the prepared bytes; negative: a blob/file/row mutated after +prepare cannot reach the live store — the existing `RESTORE-INTEGRITY` tests +are its core); a `READBACK-VERIFY` contract (positive: a valid address reads; +negative: every authority-bearing read-back rejects a substituted byte string +without effect — CAS, payload, journal, and the R7-fixed meta path once +RF-41 lands); and the per-kind entry negatives (symlink rejected at capture +and publication; sidecar-WAL removed; excluded-dir untouched including under +R3's correction). Every tier label in the ratified spec text carries a +conformance-sweep row naming its enforcing line or its RF/gate deferral. The +T2 claims explicitly carry *no* negative test that asserts race-closure — +their conformance is the documented COOP dependency plus W-4's future +topology, and the sweep says so rather than implying a test proves T2 safety. + +## Rejected alternatives (candidate) + +- **A single flat "trusted local filesystem" assumption** (what exists today, + implicitly): it is why every review re-derives the attacker — an unlabeled + claim cannot be checked, and the reviewer cannot tell a tier-appropriate + gap from a real one. +- **Claiming pathname checks defeat T2**: the same-inode recheck is + race-narrowing; asserting it closes the race is the SI-10/A21 lie surface + (a guarantee enforcement does not provide), and it would let an + actuation/G-ADVERSARIAL grant proceed on a false floor. +- **Folding T3 into the attacker model**: an honest human edit handled as + corruption is the M8 gap A20 closed and A24/R14 hardened; re-tiering it as + an attack would reopen that. +- **Deferring the whole model to W-4**: W-4 is the T2 *answer*, but T1 is + live now (backups, copied homes, the R7 gap) and needs its cryptographic + guarantees labeled independently of containment. diff --git a/docs/roadmap.md b/docs/roadmap.md index 356e178..8b5aa27 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -130,7 +130,24 @@ items: the internal independent-context pre-review before each paid external round caught 13 findings (two high) at a fraction of a round's cost — carry it forward for items (3)–(6) and consider it in the W-20 close-out as standing practice for authority-surface PRs. -Next: item (3), SI-32. +**Item (3), SI-32 — candidate drafted 2026-07-14 (ADR 0008, +PROPOSED), queued for operator ratification.** Three-tier filesystem +attacker model (T1 offline tampering → content-address verification; +T2 same-uid active writer → W-4 containment, not pathname checks; T3 +human edit → M8/A24 attribution), candidate rules A27.1–A27.4, grounded +in a complete publication-site inventory. Surfaces one genuine T1 gap +(unsigned `fabric.db` meta rows trusted while sibling events are signed +— proposed RF-41, posture-bounded under SU) and one uniformity +correction (R3: `sync_store` fs walk omits the `.git` exclusion and the +no-follow open). Seven ratification choices enumerated in the ADR — the +internal pre-review's blocking find promoted rollback/freshness to its +own decision point (content-addressing answers T1 substitution, **not** +rollback — that seam hands off to A23, which the first draft wrongly +folded into content-addressing). Passed the audit battery + +internal adversarial pre-review before filing. Spec untouched; SI-32 +open. Ratification session (challenge pass → determinations) is +tomorrow's operator work. Next after item (3): SI-26/28/29, SI-27, the +§0 posture-qualifier convention (item 6). **W-21 — workboard dogfood profile (revival).** Owner: agent; operator ratifies the registration shape. Recovered from `codex/workboard-dogfood` diff --git a/docs/spec-issues.md b/docs/spec-issues.md index 588557d..2189d1a 100644 --- a/docs/spec-issues.md +++ b/docs/spec-issues.md @@ -411,6 +411,34 @@ verification); the symlink/hardlink/directory-entry rules per store kind (fs tree vs. SQLite file); and which publication-safety claims require containment before G-PUBLISH. +**Candidate awaiting ratification (ADR 0008, 2026-07-14; W-20 item 3):** +the three-tier model — T1 offline tampering (answered by content-address +verification on every authority-bearing read-back), T2 active same-uid +writer (no pathname check wins; the honest answer is W-4 containment, so +every T2 claim is labeled "holds under COOP; requires W-4 at +G-ADVERSARIAL"), T3 legitimate human edit (not an attack; M8 attribution +and A24/R14 recovery-window capture). Candidate normative rules +(candidate amendment A27): A27.1 the staged-bytes rule (ratifies RF-20's +discipline); A27.2 verify-on-read-back, tier-labeled, with three classes +(cryptographic content-address; trust-root by-boundary; and the +**unsigned-index gap** — the code reads store paths from unsigned +`fabric.db` meta rows a T1 tamperer controls while the events beside them +are signed, proposed **RF-41**, posture-bounded under SU); A27.3 per-kind +entry rules (fs-tree symlink rejection composing with A24/R21; SQLite +whole-image + sidecar removal); A27.4 the containment-boundary sentence. +The ADR enumerates seven human choices (tier count + super-user +boundary, the T1-rollback/A23 seam, staged-bytes normativity, RF-41 vs +P17 folding, containment wording, the R3 `sync_store` correction, spec +placement) and grounds every claim in a complete publication-site +inventory. The T1-rollback seam is the internal pre-review's blocking +find: content-addressing answers T1 substitution/corruption/truncation +but **not** rollback to a legitimately-signed older state (every rehash +and signature passes) — that is A23's anchor, and the first draft wrongly +folded it into content-addressing. SI-32 remains **open**; no tier +label or rule is normative until those choices are ratified. Grounded at +`ae5ee45`; the candidate passed the audit battery and an internal +adversarial pre-review before filing. + ## SI-31 — owned-state transition: "atomically" has no commit point, journal semantics, or crash matrix (§5.3) — RESOLVED (author, 2026-07-14) **Resolution: ratified as amendment A24 (spec v0.9, §5.3) — the owned-state From 22d6e2bf2b7fca04ed8f5db01d6c856f9ae45232 Mon Sep 17 00:00:00 2001 From: XVVH Date: Wed, 15 Jul 2026 09:59:24 -0400 Subject: [PATCH 2/6] Fold external review round 1 (8 findings) into the SI-32 candidate Applied via the codified review battery (PR #51): completeness table from the SI-32 filing deliverables drove the fold, every code-fact verified at source, Tier-1 hygiene green, Tier-3 internal adversarial pre-review (2 LOW nits, no new soundness hole, ready for challenge pass). - F1 T3 active-publication-window edit: T3 is orthogonal not cumulative; a human write during a live promotion/revert is overwritten with no capture/drift (the item ADR 0007 R14 deferred here) -> decision point, protocol-class, not designed - F2 T1 rollback precise domain: layer 1 detects only rollback inconsistent vs a surviving expected terminal; coherent whole-DB suffix regression is layer 2's (was imprecise "partial rollback") - F3 trust-root unsound: keys/KEK/credentials split; substitution is an SI-27/RF-14 residual (self-consistent forged home; auth-as-other- account), not "degrades to signature failure"; out-of-scope splits root (out) from unprivileged different-uid (defended by perms) - F4 capture_sqlite follows symlinks for registered stores (only fabric.db is checked) -> RF-42 - F5 directory/kind semantics carried from A24/R25 + R24, per operation - F6 G-PUBLISH conditional-publication decision point (the filing's explicit ask, previously dropped) - F7 real inventory appendix with file:line anchors; R3 no-follow recast (capture_fs rejects symlinks via a check, not O_NOFOLLOW) - F8 SHA cite -> PR #48; wrong-base scope fixed by rebase onto main Decision points now 10; SI-32 stays open, no implementation authorized. Co-Authored-By: Claude Fable 5 --- docs/adr/0008-filesystem-attacker-model.md | 265 +++++++++++++++------ docs/roadmap.md | 23 +- docs/spec-issues.md | 36 +-- 3 files changed, 229 insertions(+), 95 deletions(-) diff --git a/docs/adr/0008-filesystem-attacker-model.md b/docs/adr/0008-filesystem-attacker-model.md index dd70cda..33a62b9 100644 --- a/docs/adr/0008-filesystem-attacker-model.md +++ b/docs/adr/0008-filesystem-attacker-model.md @@ -24,32 +24,41 @@ under, and state which publication-safety claims require containment before they hold. This candidate is grounded in a complete inventory of every filesystem -publication and authority-bearing read-back site (the "Publication site -inventory" appendix). The finding that motivates a written model: the code -already has one gold-standard publisher (`write_atomic_verified`, every guard -present) and three verifying read-backs (CAS, payload, recovery journal — all -rehash), but eleven other sites carry partial guard sets whose *sufficiency -cannot be judged without knowing which tier they must survive*. (Two verifying -read-backs — CAS and payload — rehash content; the third, the recovery -journal, verifies its Ed25519 signature.) Two are genuine gaps this model -surfaces (R7 unsigned-meta trust; the T2-unwinnable class); the rest are -tier-appropriate and this model is what lets us say so. +publication and authority-bearing read-back site (the appendix at the end of +this ADR, with file:line anchors). The finding that motivates a written model: +the code already has one gold-standard publisher (`write_atomic_verified`, +every guard present) and three verifying read-backs — CAS and payload rehash +content, the recovery journal verifies its Ed25519 signature — but eleven other +sites carry partial guard sets whose *sufficiency cannot be judged without +knowing which tier they must survive*. Genuine gaps this model surfaces: R7 +(unsigned-meta trust, proposed RF-41), the registered-SQLite symlink gap +(proposed RF-42), and the T2-unwinnable class; the rest are tier-appropriate +and this model is what lets us say so. ## The three-tier attacker model (candidate) Each tier names a distinct adversary against fabric-home storage; every guarantee in the spec and the code is labeled with the *weakest* tier under -which it still holds. Tiers are cumulative in capability but **not** in the -honest answer: T1 is defeated by cryptography (content-addressing for -substitution, A23 for rollback), T2 by topology, T3 is not an attack at all. +which it still holds. T1 and T2 are cumulative in capability (T2 is T1 plus a +live descriptor/race); T3 is **orthogonal to both** — a legitimate edit is not +a weaker attacker but a non-attacker whose window (idle, between roots, or +*inside a publication*) cuts across T1/T2 (external review round 1, finding 1). +The honest answers do not share a mechanism: T1 is defeated by cryptography +(content-addressing for substitution, A23 for rollback), T2 by topology, T3 by +attribution — never by treating it as an attack. **Out of scope (the trust boundary, stated so the tiers are not read as -total).** A super-user or different-uid local adversary is **out of scope** — -it is strictly more capable than T2 and defeats T2's uid-scoped containment -answer by construction. The trust boundary is the Unix account (P7); nothing -in this model defends against local root. This is the deliberate boundary, not -a gap; naming it is what keeps "distinct adversary / cumulative" from reading -as a completeness claim it does not make. +total).** Two different-principal cases, correctly distinguished (external +review round 1, finding 3): a **local root / privileged-host** adversary is +strictly more capable than T2 and defeats its uid-scoped answer by +construction — **out of scope**; the trust boundary is the Unix account (P7), +and nothing here defends against root. An **ordinary (unprivileged) +different-uid** process is the opposite — it is *blocked* by the enforced +0700/0600 home permissions (the posture ledger's G-MULTITENANT boundary), so it +is *defended*, not out of scope, exactly as long as those permissions hold. The +out-of-scope line is root, not "any other uid"; naming it is what keeps +"distinct adversary / cumulative" from reading as a completeness claim it does +not make. **T1 — offline storage tampering between processes.** CAS blobs, branch files, the SQLite databases, key material, or the recovery journal are @@ -66,11 +75,16 @@ index the code still trusts (the R7 gap); and (c) **rollback/replay to a prior, genuinely-signed, genuinely-content-addressed whole-home state** — where every blob rehashes correctly and every signature verifies, because the old state was legitimately produced, so content-addressing is silent by -construction. Rollback's answer is **A23, not content-addressing**: -layer 1's authenticated monotonic `global_seq`/`VerifiedPrefix` detects -partial or internally-inconsistent rollback single-machine now, and the -graduation-gated external anchor (layer 2) detects **whole-home** rollback at -its gates — which content-addressing structurally cannot see. Under the +construction. Rollback's answer is **A23, not content-addressing**, and its +domain must be stated precisely (external review round 1, finding 2): layer 1's +authenticated monotonic `global_seq`/`VerifiedPrefix` detects only rollback +that is **inconsistent relative to a non-rolled-back expected terminal** — an +interior gap, or a reset that leaves a surviving caller pin ahead of it. A +*coherent suffix regression* — rolling `fabric.db` (and with it `global_seq` +and the local checkpoint) back to an internally-consistent older copy while no +external head or pin survives — verifies cleanly at layer 1; that is +**layer 2's** job (the external anchor / caller-pinned expected head), which +content-addressing and single-machine layer-1 both structurally cannot see. Under the current **SU** posture the T1 tamperer and the operator are the same Unix principal (so whole-home rollback is undetectable locally — the operator controls any local head), making T1's live bite today *accidental* (backup @@ -101,6 +115,21 @@ capture-and-attribute-before-restore path. A model that fails T3 closed (bricking on an honest edit) or open (overwriting it unrecorded) is wrong in both directions; T3's correctness criterion is *attributed, never lost*. +**The active-publication window (T3, unresolved — external review round 1, +finding 1; the item ADR 0007 R14 explicitly deferred here).** M8 covers edits +*between* attested roots and A24/R14 covers the crash-recovery downtime, but +neither covers an edit made **during a live promotion/revert**: the gate lock +(`kernel.rs`) serializes fabric *processes*, not a human with a text editor, +so a vault edit landing after the prepare-time capture-equals-`before` check +and before the apply's rename is overwritten by the rename with no CAS capture +and no drift event — T3's "never lost" violated inside the one window the model +had not addressed. This is **protocol-class** (its remedy either narrows the +topology — the human's edit surface is excluded from the publication window — +or adds a preservation/refusal step, a new commit point), so it is surfaced as +a decision point, not designed here. It is the direct continuation of the R14 +note that named "the concurrent human-edit exposure … exactly SI-32 tier 3's +item — compose there." + ## Candidate normative rules (A27) **A27.1 — The staged-bytes rule (normative; ratifies RF-20's discipline).** @@ -128,11 +157,22 @@ storage bytes that will *bear authority* MUST content-address-verify (rehash to the requested address) before the bytes are consumed; a mismatch is a loud denial, never a fallback. This holds against **T1**. Three classes are explicitly outside "content-address-verify" and each carries its own rule: -- **Trust-root reads** (key/secret material): there is no address to verify - against — the bytes *are* the trust root. These hold against T1 only by the - storage boundary (0o600, symlink-rejecting open), and their substitution - degrades to downstream signature failure, not silent authority. Labeled - **T1-boundary**, not T1-cryptographic. +- **Trust-root reads** (signing keys, KEK, and live credentials — kept + distinct, external review round 1, finding 3): there is no address to verify + against — the bytes *are* the trust root — so under T1 they hold **only by + the storage boundary** (0o700/0o600 permissions, symlink-rejecting open), and + 0o600 does **not** constrain a same-uid T1 tamperer. Their substitution does + *not* reliably "degrade to a signature failure": swapping a **signing key** + *together with* the signed storage it verifies produces a **locally + self-consistent forged home** (nothing external is trusted — SI-27's trust + anchor is unresolved); swapping a **live credential** authenticates + successfully *as a different account* rather than failing. So under same-uid + T1 the trust root is **not defended by this model** — it is an + **SI-27/RF-14-backed residual**, accepted under SU (the tamperer is the + operator) and closed only when SI-27 lands an external trust anchor and RF-14 + moves key/credential custody behind an independent boundary. Labeled + **T1-boundary (residual)**, never T1-cryptographic; whether to accept it now + or scope T1 to a trusted verifier key is a decision point. - **Signed-object reads** (events, manifests, capabilities, and the recovery journal / `state_change_recovery` txn): verified by Ed25519 signature, which subsumes content-addressing (A19). Holds against T1 substitution and, for @@ -158,21 +198,39 @@ explicitly outside "content-address-verify" and each carries its own rule: **A27.3 — Per-kind entry rules (candidate).** Publication safety is stated per store kind because the fs tree and the SQLite file have different substitution surfaces: -- **fs-tree stores:** capture and restore reject symlinks (a symlink is not a - canonical entry — this composes with A24/R21's kind-complete recovery - scan); the canonical grammar is regular files and directories only; the - atomic publisher writes through an O_EXCL|O_NOFOLLOW randomized sibling - and rechecks the same non-symlink inode immediately before rename. Excluded - directories (`.git`) are never written, deleted, or (candidate correction) - fsync-walked — the one inconsistency the inventory found (R3: - `sync_store`'s fs walk omits the exclusion and the no-follow open) is a - fsync-only path with no integrity impact but should be brought into line so - "excluded means untouched" is uniform. +- **fs-tree stores:** the canonical grammar is exactly A24/R25's ratified + tagged path-state domain — `absent | file(content hash, executable mode) | + implicit-directory` (a directory is implicit as the proper ancestor of a + tracked file; empty and untracked directories are outside the boundary), and + any other kind (symlink, fifo, socket, device) is definitionally + non-canonical (external review round 1, finding 5 — the directory/non-kind + rules the filing asked for are A24's, carried here rather than re-derived). + The rule per operation: **capture** rejects symlinks (`capture_fs`'s explicit + `path_is_symlink` check) and folds implicit-directory structure by content; + **recovery** runs A24/R24's kind-complete scan (every non-canonical live + entry fails closed in place); **publication** writes through an + O_EXCL|O_NOFOLLOW randomized sibling and rechecks the same non-symlink inode + immediately before rename. Excluded directories (`.git`) are never written or + deleted; the one inconsistency the inventory found — R3: `sync_store`'s fs + walk omits the `.git` exclusion (and, unlike `capture_fs`, has no symlink + guard at all) — is fsync-only with no integrity impact, and the fix is to + bring it into line with capture's exclusion (the "no-follow open" framing is + corrected: `capture_fs` rejects symlinks with an explicit check, it does not + use an `O_NOFOLLOW` open, so no-follow on `sync_store` is *proposed + hardening*, not an existing discipline `sync_store` alone violates). - **SQLite stores:** the whole file is the unit; the image is captured WAL-checkpointed (folded, no live WAL), published through the same atomic primitive, and its stale `-wal`/`-shm` sidecars removed after the swap so a - restored image cannot be polluted by a leftover WAL. A symlinked store DB is - rejected before use (`symlink_metadata` no-follow). + restored image cannot be polluted by a leftover WAL. **As-built gap (external + review round 1, finding 4):** only the fabric's *own* `fabric.db` is + symlink-rejected before use (`w13_validate_existing_fabric_home`'s + `symlink_metadata` no-follow); a **registered** SQLite store (e.g. + `db:memory`) is captured by `capture_sqlite`, which uses `is_file` → + `Connection::open` → `fs::read`, **all symlink-following** — so a symlinked + registered store DB redirects capture to an attacker-chosen database. The + candidate rule is that *every* SQLite store path, registered ones included, + is symlink-rejected before capture/open; the merged code does this only for + `fabric.db`. Proposed **RF-42**, with a no-effect negative after ratification. - **Hardlinks** (the filing's explicit third entry-kind ask): a hardlinked regular file is captured by *content* like any regular file (capture reads by value), so a hardlink to in-scope content adds no capture surface. A @@ -201,14 +259,14 @@ honest sentence RF-20→RF-40 kept rediscovering the absence of. |---|---|---| | R1 `Cas::put` no fsync/O_EXCL | T1 (durability) + T1 (integrity) | Integrity held lazily by `Cas::get` rehash (A27.2). Durability fsync is **G-PRODUCTION** (DEBUG posture; same class as the WAL+NORMAL durability seam). Label, don't fix now. | | R2 `materialize_*` plain write | T1 | Writes CAS-verified bytes into a **branch**, re-captured/re-hashed before bearing authority — outside A27.1's commit boundary. Holds; label as branch-scratch, not a publication. | -| R3 `sync_store` fs walk omits `.git` exclusion + no-follow | T1 | fsync-only, no integrity impact, but breaks "excluded means untouched" uniformity. **Candidate correction** (A27.3); small, file with the ratification or as a follow-up RF. | +| R3 `sync_store` fs walk omits `.git` exclusion (no symlink guard) | T1 | fsync-only, no integrity impact, but breaks "excluded means untouched" uniformity. **Candidate correction** (A27.3): match capture's `.git` exclusion; the no-follow half is proposed hardening, not a capture discipline it violates. Follow-up RF. | | R4 `Cas::get` exists→read TOCTOU | T1 | Rehash makes substitution a denial. Holds (A27.2). | | R5 `gate_lock` no O_EXCL/O_NOFOLLOW | T2 | Advisory flock; a pre-planted `gate.lock` symlink is a same-uid act → T2/COOP. Label; W-4 owns. | -| R6 key/secret read-back trusts pathname | T1-boundary | Inherent — the trust root. A27.2's trust-root class. Confidentiality is P17/RF-14. | -| **R7 unsigned `fabric.db` meta rows trusted** | **T1** | **The one genuine T1 authority gap.** A27.2's unsigned-index rule; **propose RF-41**, posture-bounded under SU, real at G-MULTITENANT. | +| R6 key/secret read-back trusts pathname | T1-boundary (residual) | The trust root; substitution can forge a self-consistent home or authenticate as another account under same-uid T1. A27.2's trust-root class; **SI-27/RF-14 residual**, accepted under SU. | +| **R7 unsigned `fabric.db` meta rows trusted** | **T1** | **A genuine T1 authority gap.** A27.2's unsigned-index rule; **proposed RF-41**, posture-bounded under SU, real at G-MULTITENANT. | | R8 `write_private_atomic` rename target no-follow | T1-boundary | Plaintext secret, in-memory source; rename replaces a symlink node. Bounded; note under A27.3. | | R9 journal/sidecar remove no symlink guard | T1 | Fixed paths; removes the link not a target. Minor; note. | -| R10 `capture_sqlite` read no O_NOFOLLOW | T1 | Fabric-internal path, trust-on-capture. Bounded; note. | +| **R10 `capture_sqlite` follows symlinks (registered stores)** | **T1** | **A genuine gap** (external review round 1, finding 4): `is_file`/`Connection::open`/`fs::read` all follow a symlink; only `fabric.db` is symlink-checked, not registered SQLite stores. A27.3's SQLite rule; **proposed RF-42**, posture-bounded under SU. | | R11 CLI/demo/tooling writes | n/a | Non-authority-bearing (sockets, agent working store, demo fixtures, reports). Out of scope; state so. | ## Ratification decision points (the human choices) @@ -217,39 +275,67 @@ honest sentence RF-20→RF-40 kept rediscovering the absence of. T1-offline vs T1-at-rest-confidentiality (the latter is P17/RF-15's, and this model currently folds confidentiality into T1's note rather than a fourth tier). Recommend: three tiers, confidentiality cross-referenced not - re-tiered. Sub-choice: is the super-user/different-uid out-of-scope - boundary (P7) stated correctly as a boundary rather than a gap? -2. **T1-rollback carve-out (the pre-review's blocking find).** Ratify that + re-tiered. Sub-choice: is the root-vs-unprivileged-different-uid boundary + (P7 / the permissions boundary) stated correctly — root out of scope, an + unprivileged other uid defended by permissions? +2. **T1-rollback carve-out and its precise domain.** Ratify that content-addressing answers T1 *substitution/corruption/truncation* but - **not rollback**, whose answer is A23 (layer-1 `global_seq` now, the - graduation-gated anchor for whole-home rollback) — and that under SU - whole-home rollback is undetectable locally, an accepted accidental-only - residual until G-MULTITENANT. This is the seam where SI-32 hands off to - A23; ratifying it wrong (content-addressing "answers T1") ratifies an + **not rollback**, whose answer is A23 — layer 1 detecting only rollback + *inconsistent relative to a surviving expected terminal*, and a coherent + suffix regression (whole-DB rollback with no surviving pin) being layer 2's + at its gates. Under SU it is undetectable locally, an accepted + accidental-only residual until G-MULTITENANT. This is the seam where SI-32 + hands off to A23; ratifying "content-addressing answers T1" ratifies an unsound tier. Recommend ratify as stated; the alternative is to declare - rollback wholly out of SI-32's scope and purely A23's — cleaner boundary - but leaves the SI-32 reader without the cross-reference. -3. **A27.1 staged-bytes as normative** — ratify RF-20's discipline as the + rollback wholly A23's and out of SI-32's scope. +3. **Trust-root substitution under same-uid T1 (external review finding 3).** + Accept key/KEK/credential substitution as an **SI-27/RF-14 residual** + (recommend: yes, posture-bounded under SU — the tamperer is the operator — + closed when SI-27 lands an external anchor and RF-14 moves custody behind an + independent boundary), or scope T1's guarantees to a *trusted verifier key* + assumed outside the tamperable home. The first is honest about today; the + second is the shape production takes. +4. **Active-publication-window human edit (external review finding 1; + protocol-class).** T3's one uncovered window — a human edit during a live + promotion/revert, overwritten by the apply with no capture or drift. Resolve + by **topology** (the human edit surface is excluded from the publication + window — e.g. the vault is not concurrently human-writable while a gate + holds) or by a **ratified preservation/refusal protocol** (a new commit + point that captures-or-refuses on an in-window edit, the A24/R14 shape + extended to the in-process gate window). Protocol-class either way — file as + an SI before implementation. Recommend: decide the topology question first; + it may dissolve the protocol need. +5. **A27.1 staged-bytes as normative** — ratify RF-20's discipline as the general rule, or leave it implementation-internal? Recommend normative: it is the T1 write-boundary invariant and future stores (Tier-2/3) must inherit it. -4. **A27.2's unsigned-index rule and RF-41.** Is R7 a ratifiable gap to file +6. **A27.2's unsigned-index rule and RF-41.** Is R7 a ratifiable gap to file now (recommend: yes, posture-bounded, carried when the home-boundary posture graduates), or folded into P17/RF-15's at-rest work? This decides - whether SI-32 spawns a new RF or reuses one. -5. **A27.4 containment boundary wording** — the "COOP; W-4 at G-ADVERSARIAL" + whether SI-32 spawns a new RF or reuses one. (RF-42, the registered-SQLite + symlink gap, is the same call — file now or fold.) +7. **A27.4 containment boundary wording** — the "COOP; W-4 at G-ADVERSARIAL" label on every T2 claim. Ratify as the standing sentence, or scope it per-site? Recommend standing sentence, since it is the same honest answer at every T2 site. -6. **R3 correction** — bring `sync_store` into the exclusion/no-follow - discipline now (small), or file as a follow-up? Recommend follow-up RF - (it is fsync-only; not worth growing a threat-model ratification with a - mechanism change). -7. **Where this lands in the spec.** §5.3 (owned-state transition, where - publication lives) plus a §9 fork note, or a new §-level "storage adversary - model" subsection? Recommend a §5.3 subsection cross-referenced from §1 - (payload store) and §9 (F2 addressing), since publication is §5.3's and the - tiers label claims spec-wide. +8. **G-PUBLISH containment mapping (external review finding 6; the filing's + explicit ask).** The filing asked *which publication-safety claims require + containment before G-PUBLISH*. The real fork: may the spec **publish the + conditional T2 claims** (labeled "holds under COOP; W-4 at G-ADVERSARIAL") + with W-4 deferred, or must **W-4 containment land before publication** so no + published claim rests on an unbuilt topology? Recommend: publish the + conditional claims with the label — the label *is* the honest disclosure — + and record the determination as a **G-PUBLISH row in the posture ledger** + (T2 publication-safety claims are conditional-published, W-4-gated), so a + reader of the published spec sees the dependency. +9. **R3 correction** — bring `sync_store` into the `.git` exclusion now + (small), or file as a follow-up? Recommend follow-up RF (it is fsync-only; + not worth growing a threat-model ratification with a mechanism change). +10. **Where this lands in the spec.** §5.3 (owned-state transition, where + publication lives) plus a §9 fork note, or a new §-level "storage adversary + model" subsection? Recommend a §5.3 subsection cross-referenced from §1 + (payload store) and §9 (F2 addressing), since publication is §5.3's and the + tiers label claims spec-wide. ## Reserved seams (not resolved here) @@ -271,9 +357,11 @@ prepare cannot reach the live store — the existing `RESTORE-INTEGRITY` tests are its core); a `READBACK-VERIFY` contract (positive: a valid address reads; negative: every authority-bearing read-back rejects a substituted byte string without effect — CAS, payload, journal, and the R7-fixed meta path once -RF-41 lands); and the per-kind entry negatives (symlink rejected at capture -and publication; sidecar-WAL removed; excluded-dir untouched including under -R3's correction). Every tier label in the ratified spec text carries a +RF-41 lands); and the per-kind entry negatives (symlink rejected at fs-tree +capture and publication *and at registered-SQLite capture* once RF-42 lands; +non-canonical kinds — fifo/socket/device — fail closed per A24/R24; directory +path-states resolve per A24/R25; sidecar-WAL removed; excluded-dir untouched +including under R3's correction). Every tier label in the ratified spec text carries a conformance-sweep row naming its enforcing line or its RF/gate deferral. The T2 claims explicitly carry *no* negative test that asserts race-closure — their conformance is the documented COOP dependency plus W-4's future @@ -295,3 +383,38 @@ topology, and the sweep says so rather than implying a test proves T2 safety. - **Deferring the whole model to W-4**: W-4 is the T2 *answer*, but T1 is live now (backups, copied homes, the R7 gap) and needs its cryptographic guarantees labeled independently of containment. + +## Appendix — publication site inventory + +Grounded against the PR #48 merge base. Every filesystem publication and +authority-bearing read-back, by artifact class, with file:line anchors; +`write_atomic_verified` is the gold standard the residue table measures against. +R-numbers match the residue disposition table. + +- **Gold-standard publisher.** `write_atomic_verified` / `_with_hook` + (`snapshot.rs:534`/`543`, the guard sequence in the hook variant): + randomized `O_EXCL|O_NOFOLLOW` sibling, mode set, file fsync, retained-handle + rehash-before-rename, same-inode non-symlink recheck, atomic rename, parent + fsync. `commit_restore` Swap and `apply_fs_in_place` pass 2 route through it. +- **Verifying read-backs (authority-bearing).** `Cas::get` (`snapshot.rs:152`) + and `payload::get` (`payload.rs:122`) rehash; `recover_pending_state_change` + (`kernel.rs:816`) verifies the journal signature + type; key/secret reads + (`keys.rs:186`) trust by pathname — the trust root (R6). +- **CAS blobs (R1, R4).** `Cas::put` (`snapshot.rs:128`): randomized tmp + + rename, no fsync/`O_EXCL`/`O_NOFOLLOW`; dedup branch re-verifies via `get`; + integrity enforced lazily on read. +- **Recovery journal (R9).** `publish_state_change_journal` (`kernel.rs:122`): + `O_EXCL|O_NOFOLLOW`, fsync file + parent, fixed path (presence = pending bit). +- **Keys/secrets (R6, R8).** `Keystore::initialize` (`keys.rs:67`): staged dir + + atomic rename; per-file `create_private_new` (`keys.rs:261`) = `O_EXCL` + + 0o600 + fsync. +- **SQLite (R7, R10).** ledger `fabric.db` WAL+NORMAL (`kernel.rs:432`); + `w13_validate_existing_fabric_home` (`kernel.rs:328`) symlink-checks + `fabric.db` **only**; `capture_sqlite` (`snapshot.rs:268`) follows symlinks + (RF-42); meta `stores`/`substrate_span` read unsigned (`kernel.rs:505`, + RF-41). Payload bytes live in `fabric.db`, none on the filesystem. +- **Branch / lock / sync / CLI (R2, R3, R5, R11).** `materialize_*` + (`snapshot.rs:337`) plain write into branch scratch; `gate_lock` + (`kernel.rs:1015`) advisory `flock`, no `O_EXCL`/`O_NOFOLLOW`; `sync_store` fs + walk (`snapshot.rs:608`) omits the `.git` exclusion (R3); CLI/demo/tooling + writes are non-authority-bearing (R11). diff --git a/docs/roadmap.md b/docs/roadmap.md index 8b5aa27..0c9bcf6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -135,16 +135,19 @@ PROPOSED), queued for operator ratification.** Three-tier filesystem attacker model (T1 offline tampering → content-address verification; T2 same-uid active writer → W-4 containment, not pathname checks; T3 human edit → M8/A24 attribution), candidate rules A27.1–A27.4, grounded -in a complete publication-site inventory. Surfaces one genuine T1 gap -(unsigned `fabric.db` meta rows trusted while sibling events are signed -— proposed RF-41, posture-bounded under SU) and one uniformity -correction (R3: `sync_store` fs walk omits the `.git` exclusion and the -no-follow open). Seven ratification choices enumerated in the ADR — the -internal pre-review's blocking find promoted rollback/freshness to its -own decision point (content-addressing answers T1 substitution, **not** -rollback — that seam hands off to A23, which the first draft wrongly -folded into content-addressing). Passed the audit battery + -internal adversarial pre-review before filing. Spec untouched; SI-32 +in a publication-site inventory appendix with file:line anchors. +Surfaces two genuine T1 gaps (RF-41: unsigned `fabric.db` meta rows — +incl. `substrate_span`, which feeds broker authority — trusted while +sibling events are signed; RF-42: `capture_sqlite` follows symlinks for +registered SQLite stores, only `fabric.db` is symlink-checked), an +unresolved protocol-class T3 case (a human edit during a live +promotion/revert overwritten with no capture/drift — the item ADR 0007 +R14 deferred here), and one uniformity correction (R3: `sync_store` +omits the `.git` exclusion). Ten ratification choices enumerated. Built +via the codified review battery (PR #51): audit battery + internal +adversarial pre-review before filing (which caught the rollback/A23 +blocking find), then external review round 1 (8 findings — 5 the battery +now guards against, all folded). Spec untouched; SI-32 open. Ratification session (challenge pass → determinations) is tomorrow's operator work. Next after item (3): SI-26/28/29, SI-27, the §0 posture-qualifier convention (item 6). diff --git a/docs/spec-issues.md b/docs/spec-issues.md index 2189d1a..d369bb4 100644 --- a/docs/spec-issues.md +++ b/docs/spec-issues.md @@ -424,20 +424,28 @@ discipline); A27.2 verify-on-read-back, tier-labeled, with three classes **unsigned-index gap** — the code reads store paths from unsigned `fabric.db` meta rows a T1 tamperer controls while the events beside them are signed, proposed **RF-41**, posture-bounded under SU); A27.3 per-kind -entry rules (fs-tree symlink rejection composing with A24/R21; SQLite -whole-image + sidecar removal); A27.4 the containment-boundary sentence. -The ADR enumerates seven human choices (tier count + super-user -boundary, the T1-rollback/A23 seam, staged-bytes normativity, RF-41 vs -P17 folding, containment wording, the R3 `sync_store` correction, spec -placement) and grounds every claim in a complete publication-site -inventory. The T1-rollback seam is the internal pre-review's blocking -find: content-addressing answers T1 substitution/corruption/truncation -but **not** rollback to a legitimately-signed older state (every rehash -and signature passes) — that is A23's anchor, and the first draft wrongly -folded it into content-addressing. SI-32 remains **open**; no tier -label or rule is normative until those choices are ratified. Grounded at -`ae5ee45`; the candidate passed the audit battery and an internal -adversarial pre-review before filing. +entry rules (fs-tree canonical grammar = A24/R25's path-state domain, +non-canonical kinds via A24/R24's kind-complete scan; SQLite whole-image + +sidecar removal, plus the RF-42 registered-store symlink gap); A27.4 the +containment-boundary sentence. +The ADR enumerates **ten** human choices (tier count + root-vs-uid +boundary; the T1-rollback/A23 seam and its precise layer-1 domain; +trust-root substitution as an SI-27/RF-14 residual; the active- +publication-window human edit, protocol-class; staged-bytes normativity; +RF-41/RF-42 vs P17 folding; containment wording; G-PUBLISH conditional +publication; the R3 correction; spec placement) and grounds every claim +in a publication-site inventory appendix with file:line anchors. Two +gaps beyond RF-41 surfaced by folding external review round 1: +**RF-42** (`capture_sqlite` follows symlinks for registered SQLite +stores; only `fabric.db` is symlink-checked), and the active-publication- +window T3 edit (a human write during a live promotion/revert is +overwritten with no capture/drift — the item ADR 0007 R14 deferred here, +protocol-class). SI-32 remains **open**; no tier label or rule is +normative until those choices are ratified. Candidate filed as PR #48-era +main; passed the audit battery and an internal adversarial pre-review +before filing, then folded external review round 1 (8 findings — 5 that +the codified review battery, PR #51, now guards against) before this +revision. ## SI-31 — owned-state transition: "atomically" has no commit point, journal semantics, or crash matrix (§5.3) — RESOLVED (author, 2026-07-14) From f73f4ec2830fb91d3ae8a11fb050be2f9c2c18fe Mon Sep 17 00:00:00 2001 From: XVVH Date: Wed, 15 Jul 2026 14:22:07 -0400 Subject: [PATCH 3/6] Ratify SI-32 as A27 (spec v0.10): storage adversary model, determinations D32-1..D32-10 ADR 0008 ACCEPTED with the ratification addendum. Spec v0.10 gains the A27 bullet family in section 5.3 (three adversary tiers with the rollback/trust-root/unsigned-index carve-outs, staged-bytes and verify-on-read-back normative, per-kind entry rules, the standing T2 containment sentence), cross-refs in sections 1 and 9, and the changelog. Filings: SI-40 (active-publication-window edit; topology arm foreclosed by D32-4, preservation protocol leading candidate, substrate-assisted CoW-snapshot direction recorded from the operator), RF-41/RF-42/RF-43 with carriers, P29 and the G-PUBLISH gate entry, G14 negatives. Internal adversarial pre-review before the external round: 14 findings (4 medium), all applied and recorded in the addendum. Per the stopping rule (PR #52): one delta-scoped external round next. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 +- docs/adr/0008-filesystem-attacker-model.md | 118 +++++++++++++++++++-- docs/asf-schema-spec.md | 20 +++- docs/posture-assumptions.md | 28 ++++- docs/review-findings.md | 74 +++++++++++++ docs/roadmap.md | 26 ++++- docs/spec-issues.md | 104 +++++++++++++++++- docs/testing-theory.md | 26 +++++ 8 files changed, 378 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 201d9e7..236de09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ caveats down). Read `docs/agent-state-fabric-brief.md` (why/what, v0.2) and `docs/asf-schema-spec.md` (the constitution, v0.9) before writing any code. The spec wins over this file wherever they disagree. Spec ambiguities found while implementing go to `docs/spec-issues.md` (never silently interpret); -SI-1…SI-21 are resolved (SI-20 → A20/M8 in v0.5; SI-21 → A21/M7 in v0.6); SI-22 is interpreted (gate replay clock, W-2); SI-24 is resolved (A22/§5.4 in v0.7 — capability early closure; implemented by W-8 under its gated matrix: A22 two-sided contract, targeted `a22_*` mutation lane, W-9 verdict-invariance reproduced bit-for-bit); SI-23 (actuation vs approval surfaces) is OPEN — no actuation-scoped tool may register before SI-23 resolves. SI-25 is resolved (A23/§6.2 in v0.8 — authenticated global order: signed per-home global_seq layer 1 normative now, external anchor layer 2 graduation-gated; W-15 implements; ratified via W-20 determinations D1–D7/S1–S5 in ADR 0006). SI-26…SI-30 remain OPEN from the 2026-07-12 cryptographic mechanism audit (signed type/domain, key lifecycle, AEAD envelope, post-shred generations, redaction commitments). SI-31/SI-33/SI-34 are resolved (A24–A26 in v0.9 — owned-state transition protocol §5.3, consumed authority §5.5, approval candidate binding §6; W-20 retro-ratification of the merged W-14 protocols via PR #48, determinations D31/D33/D34 plus seven rounds of review adjustments R1–R25 (round 7 scoped) and an internal pre-round-6 review in ADR 0007 — round 2 added the recovery capture record and positional freshness, round 3 hardened them, round 4 extended temporal binding to both consumption consumers, rounds 4–6 surfaced as-built defects and completed canonical-entry identity and the recovery path-state domain; spec-code deltas tracked as RF-35–RF-37/RF-39 (adjusted beyond as-built) and RF-40 (as-built fs-pipeline defects), carried by W-15/W-22; G13 files the outstanding negatives; unenforced clauses bind at their carrier gates per the ledger). SI-32 (store publication/filesystem attacker model) is OPEN from the same 2026-07-13 PR #43 review-cycle analysis and is next in W-20's batch. SI-35 (workboard domain labels vs domain-taxonomy governance, recovered from codex/workboard-dogfood at the W-21 revival decision), SI-37 (TracePosition agreement predicate; W-15 enforces fail-closed from day one), and SI-38 (TracePosition genesis representation; W-15 implements a bootstrap-event floor provisionally) are OPEN; SI-36 (mid-run "actually do Y" amendments, §3.1/M1/M5) merged via PR #46. SI-39 (recovery-window divergence: automated multi-window preservation; filed by PR #48's round-3 review, the narrow-and-file remedy ratified as R14) is OPEN. New issues start at SI-40. +SI-1…SI-21 are resolved (SI-20 → A20/M8 in v0.5; SI-21 → A21/M7 in v0.6); SI-22 is interpreted (gate replay clock, W-2); SI-24 is resolved (A22/§5.4 in v0.7 — capability early closure; implemented by W-8 under its gated matrix: A22 two-sided contract, targeted `a22_*` mutation lane, W-9 verdict-invariance reproduced bit-for-bit); SI-23 (actuation vs approval surfaces) is OPEN — no actuation-scoped tool may register before SI-23 resolves. SI-25 is resolved (A23/§6.2 in v0.8 — authenticated global order: signed per-home global_seq layer 1 normative now, external anchor layer 2 graduation-gated; W-15 implements; ratified via W-20 determinations D1–D7/S1–S5 in ADR 0006). SI-26…SI-30 remain OPEN from the 2026-07-12 cryptographic mechanism audit (signed type/domain, key lifecycle, AEAD envelope, post-shred generations, redaction commitments). SI-31/SI-33/SI-34 are resolved (A24–A26 in v0.9 — owned-state transition protocol §5.3, consumed authority §5.5, approval candidate binding §6; W-20 retro-ratification of the merged W-14 protocols via PR #48, determinations D31/D33/D34 plus seven rounds of review adjustments R1–R25 (round 7 scoped) and an internal pre-round-6 review in ADR 0007 — round 2 added the recovery capture record and positional freshness, round 3 hardened them, round 4 extended temporal binding to both consumption consumers, rounds 4–6 surfaced as-built defects and completed canonical-entry identity and the recovery path-state domain; spec-code deltas tracked as RF-35–RF-37/RF-39 (adjusted beyond as-built) and RF-40 (as-built fs-pipeline defects), carried by W-15/W-22; G13 files the outstanding negatives; unenforced clauses bind at their carrier gates per the ledger). SI-32 is resolved (A27/§5.3 in v0.10 — the three-tier storage adversary model: T1 offline tampering defeated by verify-on-read-back with the trust-root/unsigned-index/rollback carve-outs, T2 same-uid writer answered by topology never pathname checks — every T2 claim carries "holds under COOP; requires W-4 containment at G-ADVERSARIAL" — T3 human edit answered by attribution; staged-bytes normative; ratified 2026-07-15 via W-20 determinations D32-1…D32-10 in ADR 0008; RF-41 (unsigned meta rows → W-15a/b, the W-15 split in PR #52), RF-42/RF-43 (symlink/exclusion gaps → the RF-40 mechanical bugfix lane) file the gaps; SI-40 files the active-publication-window edit, its topology arm foreclosed by D32-4). SI-35 (workboard domain labels vs domain-taxonomy governance, recovered from codex/workboard-dogfood at the W-21 revival decision), SI-37 (TracePosition agreement predicate; W-15 enforces fail-closed from day one), and SI-38 (TracePosition genesis representation; W-15 implements a bootstrap-event floor provisionally) are OPEN; SI-36 (mid-run "actually do Y" amendments, §3.1/M1/M5) merged via PR #46. SI-39 (recovery-window divergence: automated multi-window preservation; filed by PR #48's round-3 review, the narrow-and-file remedy ratified as R14) and SI-40 (a human edit inside a live promotion/revert apply window is overwritten uncaptured; filed by the SI-32 ratification, P29-bounded, preservation-protocol candidate) are OPEN. New issues start at SI-41. ## Built — Stages 1–3 (kernel, spine, promotion gate). Current queue: docs/roadmap.md diff --git a/docs/adr/0008-filesystem-attacker-model.md b/docs/adr/0008-filesystem-attacker-model.md index 33a62b9..533e98e 100644 --- a/docs/adr/0008-filesystem-attacker-model.md +++ b/docs/adr/0008-filesystem-attacker-model.md @@ -1,13 +1,14 @@ # ADR 0008 — Filesystem attacker model for store publication and read-back -**Status: PROPOSED — SI-32 ratification candidate, awaiting human -ratification. This ADR is not a specification amendment and authorizes no -implementation. SI-32 remains OPEN. The tier labels, the staged-bytes rule, -the per-kind entry rules, and the containment boundary below are candidate -normative language; none is normative until the human choices in -"Ratification decision points" are ratified and integrated into the schema -spec under the amendment discipline (candidate amendment id: A27). W-20 item -(3).** +**Status: ACCEPTED — ratified 2026-07-15 (operator session; W-20 item 3). +SI-32 is RESOLVED. The determinations for the ten decision points are +recorded in the ratification addendum at the end of this document +(D32-1…D32-10) and integrated into the schema spec as amendment A27 +(v0.10, §5.3). The body below is preserved as the candidate the +determinations judged; where the addendum adjusts it, the addendum wins. +Per the review-battery stopping rule (adopted 2026-07-15, PR #52): one +delta-scoped external round on the ratification text; continuation only +on blocking findings.** ## Context @@ -418,3 +419,104 @@ R-numbers match the residue disposition table. (`kernel.rs:1015`) advisory `flock`, no `O_EXCL`/`O_NOFOLLOW`; `sync_store` fs walk (`snapshot.rs:608`) omits the `.git` exclusion (R3); CLI/demo/tooling writes are non-authority-bearing (R11). + +## Ratification addendum — determinations (2026-07-15) + +Operator session, challenge pass over the round-1-folded candidate. One +divergence from the candidate's stated lean (D32-4); everything else +ratified as drafted, with the reasoning recorded so it is citable. + +- **D32-1 — three tiers, confidentiality cross-referenced.** Ratified as + drafted. At-rest confidentiality stays P17/RF-15's (a fourth tier would + duplicate a ledger row as a threat tier). The boundary statement is + ratified precisely: root/privileged-host out of scope (the trust + boundary is the Unix account, P7); an unprivileged different uid is + *defended* by the enforced 0700/0600 permissions, not out of scope. +- **D32-2 — the T1-rollback carve-out, kept inside the model.** Ratified + as drafted, and deliberately NOT moved out of SI-32's scope: the tier + table is where future readers look up "what defeats T1," and a model + that omits the rollback seam invites a later wholesale ratification of + "content-addressing answers T1." Layer 1 detects rollback inconsistent + relative to a surviving expected terminal; a coherent suffix regression + is layer 2's at its gates; under SU the T1 rollback bite is accidental + (backup staleness, copied homes), adversarial at G-MULTITENANT. +- **D32-3 — trust-root substitution accepted as the SI-27/RF-14 + residual.** Ratified option 1. The alternative — scoping T1 to a + trusted verifier key outside the tamperable home — labels an anchor + that does not exist (SI-27 is open; RF-14 keys are plaintext in the + home); that is aspirational labeling, the disease A27.4 treats. The + residual is honest about today and names its own closure (SI-27 + external anchor + RF-14 custody boundary). Revisit the verifier-key + shape when SI-27 ratifies. +- **D32-4 — the active-publication window: topology arm FORECLOSED; + SI-40 filed (the addendum's one divergence from the candidate's + lean).** The candidate recommended deciding the topology question + first. Determination: the topology remedy is structurally unavailable + for shared-state stores — native, unmediated human access to the vault + is the product thesis (brief §2), W-4's sandbox contains the *agent* + and never the human, and momentary exclusion (chmod/lock during apply) + fails T3 in the other direction: a bricked save is failing closed on + an honest edit. The eventual answer is the preservation/refusal + protocol (the A24/R14 capture-or-refuse shape at a per-entry + pre-rename point), and it is NOT designed now: SI-40 files it with the + leading candidate named, P29 bounds the live window (1HUMAN/1SESS, + sub-second Tier-1 applies, self-inflicted concurrency), and the + triggers are any non-sub-second apply window (Tier-2/3 stores), + G-2HUMAN, or an observed loss in dogfooding. The R14/SI-39 + narrow-and-file pattern, applied at ratification time. +- **D32-5 — A27.1 staged-bytes normative.** Ratified. As-built with a + two-sided contract (`RESTORE-INTEGRITY`); normative status costs + nothing today and binds Tier-2/3 stores before they exist. +- **D32-6 — RF-41 and RF-42 filed now, not folded.** Folding integrity + gaps into P17/RF-15 buries them in a confidentiality tracker (wrong + ledger). Carriers assigned at filing: RF-41 → W-15a/W-15b (the W-15 + split in flight in PR #52; deriving + operational pointers from signed substrate is the same region as the + signed-chain work; `substrate_span` binding is the read-side of the + `VerifiedPrefix`); RF-42 → the RF-40 mechanical bugfix lane (defined + at RF-42's entry). +- **D32-7 — A27.4 standing sentence.** Ratified as the standing sentence + on every T2 claim; per-site scoping re-derives the same sentence N + times, which is the RF-20→RF-40 disease this ADR treats. The + `write_atomic_verified` comment correction (race-narrowing, never + race-closing) is mechanism-class and rides any PR. +- **D32-8 — conditional T2 publication.** Ratified: T2 claims publish as + labeled conditionals; the label is the honest disclosure, and gating + publication on W-4 would put a containment project on the spec's + critical path for no honesty gain (W-7/F2 gate publication regardless). + Recorded as the T2 entry under the posture ledger's G-PUBLISH gate; a + published T2 claim missing its label is a publication defect. +- **D32-9 — R3 filed as RF-43.** Follow-up RF riding RF-42's mechanical + lane; a threat-model ratification is not grown with a mechanism change + (the triage rule: remediation may shrink a PR under review, never grow + it). +- **D32-10 — spec placement.** §5.3 bullet family (matching the + section's protocol-bullet architecture), cross-referenced from §1 + (payload read-back) and §9 F2 (rules survive the CID transition); + changelog v0.10. A new top-level section was rejected: publication + lives in §5.3, and the tiers label claims spec-wide by reference. + +**Validation mapping (per the candidate's plan).** A27.1's pair exists +(`RESTORE-INTEGRITY`); the outstanding negatives are G14(a)–(c) landing +with their RF carriers (RF-41 → W-15a/b; RF-42/RF-43 → the mechanical +lane); G14(d) records the deliberate absence of any T2 race-closure +negative — the conformance for T2 claims is the documented COOP +dependency plus W-4's topology, never a test. Enforcement binds at the +carrier gates per the ledger. + +**Internal pre-review (2026-07-15, before the external delta round, per +the battery).** A fresh-context adversarial pass over the ratification +fold returned 14 findings (4 medium), all applied before push. The +substantive ones: the SI-40 filing's refusal-disposition parenthetical +wrongly routed a preserving refusal through A24's ordinary-failure +rollback, which restores `before` over the divergent entry with no +capture record — recreating the loss SI-40 exists to prevent (corrected: +capture-before-rollback or fail-like-a-crash); the spec's "every +guarantee carries a label" opener over-claimed in the indicative and is +now stated by-reference with an inline-label obligation for new claims; +carrier references to W-15a/W-15b and the RF-40 mechanical lane were +unresolvable on this branch and now cite PR #52; the T1 headline gained +its capture-time per-kind dependency and the D32-1 confidentiality +cross-reference; the A27.4 label was quoted without "containment" in +CLAUDE.md; P29 was missing the dogfooding-loss trigger; and the +adversary-tier notation is now disambiguated from §10's store tiers. diff --git a/docs/asf-schema-spec.md b/docs/asf-schema-spec.md index 549fb70..244fc99 100644 --- a/docs/asf-schema-spec.md +++ b/docs/asf-schema-spec.md @@ -1,8 +1,8 @@ # Agent State Fabric — Schema Specification -**Draft v0.9 — July 2026 — Companion to the Architecture Brief** +**Draft v0.10 — July 2026 — Companion to the Architecture Brief** -*v0.3 integrated amendments A1–A14 from the wedge paper runs (Hermes agent; workflows: web research → vault distillation, Discord message management, vault maintenance). v0.4 integrated A15–A19 from the Stage 1–3 reference implementation (Coppice): the first amendments forced by running code rather than paper runs. Every A15–A19 decision traces to the implementation sessions via `docs/spec-issues.md` (SI-1…SI-19, all resolved in that version). v0.5 integrates A20 (SI-20, M8 attribution completeness) — the first amendment forced by dogfooding rather than by implementation. v0.6 integrates A21 (SI-21): brokered authority binds by mode declaration plus grant event, never by an embedded capability id — resolving the content-address cycle at the kernel object. v0.7 integrates A22 (SI-24): capabilities gain early closure — a signed `revoke` event as the permanent, prospective, descendant-closing dual of A21's `grant`, with liveness a pure event-derived view evaluated at the operation's durable authorization offset (§5.4). v0.8 integrates A23 (SI-25): the trace substrate gains an authenticated global order — every event binds a signed per-home `global_seq`/`global_prev` (§6.2), making the "verified substrate prefix" that A21/A22 quantify over a mechanically available object; a graduation-gated external monotonic anchor adds freshness against rollback, and the owned-state transition of §5.3 (SI-31) shares its single commit point. v0.9 integrates A24–A26 (SI-31, SI-33, SI-34) — the W-20 retro-ratification of the three protocols W-14 (PR #43) designed inside its remediation cycle: the owned-state transition protocol of §5.3 (one commit point, fabric-signed recovery journal, fail-closed recovery), consumed authority as an event-derived view (§5.5), and approval candidate binding (§6). Ratified with adjustments beyond as-built, extended by review rounds 1–7 plus an internal pre-round-6 review (ADR 0007, determinations plus R1–R25 — round 2 adding the recovery capture record and positional freshness, round 3 hardening them, round 4 extending temporal binding to both consumers and surfacing as-built defects, round 5 completing canonical-entry identity and widening RF-40 to the planner, round 6 closing the first-attempt scan bypass and defining the tagged path-state domain, round 7 — scoped to the round-6 delta — completing R24/R25 in place); W-15 and W-22 carry the mechanisms, RF-35–RF-37/RF-39/RF-40 track the interim (RF-38 files the anomaly-recovery question), SI-39 files the multi-window recovery enhancement. Unenforced clauses bind implementations at their carrier gates per the ledger (the W-20 §0 posture-qualifier convention will make this status first-class). Changelog at end. Risk-review requirements carried since v0.1: **(R2)** read authority is first-class, **(R3)** payloads are hash-referenced and destroyable, **(R6)** trust is domain-scoped, never scalar.* +*v0.3 integrated amendments A1–A14 from the wedge paper runs (Hermes agent; workflows: web research → vault distillation, Discord message management, vault maintenance). v0.4 integrated A15–A19 from the Stage 1–3 reference implementation (Coppice): the first amendments forced by running code rather than paper runs. Every A15–A19 decision traces to the implementation sessions via `docs/spec-issues.md` (SI-1…SI-19, all resolved in that version). v0.5 integrates A20 (SI-20, M8 attribution completeness) — the first amendment forced by dogfooding rather than by implementation. v0.6 integrates A21 (SI-21): brokered authority binds by mode declaration plus grant event, never by an embedded capability id — resolving the content-address cycle at the kernel object. v0.7 integrates A22 (SI-24): capabilities gain early closure — a signed `revoke` event as the permanent, prospective, descendant-closing dual of A21's `grant`, with liveness a pure event-derived view evaluated at the operation's durable authorization offset (§5.4). v0.8 integrates A23 (SI-25): the trace substrate gains an authenticated global order — every event binds a signed per-home `global_seq`/`global_prev` (§6.2), making the "verified substrate prefix" that A21/A22 quantify over a mechanically available object; a graduation-gated external monotonic anchor adds freshness against rollback, and the owned-state transition of §5.3 (SI-31) shares its single commit point. v0.9 integrates A24–A26 (SI-31, SI-33, SI-34) — the W-20 retro-ratification of the three protocols W-14 (PR #43) designed inside its remediation cycle: the owned-state transition protocol of §5.3 (one commit point, fabric-signed recovery journal, fail-closed recovery), consumed authority as an event-derived view (§5.5), and approval candidate binding (§6). Ratified with adjustments beyond as-built, extended by review rounds 1–7 plus an internal pre-round-6 review (ADR 0007, determinations plus R1–R25 — round 2 adding the recovery capture record and positional freshness, round 3 hardening them, round 4 extending temporal binding to both consumers and surfacing as-built defects, round 5 completing canonical-entry identity and widening RF-40 to the planner, round 6 closing the first-attempt scan bypass and defining the tagged path-state domain, round 7 — scoped to the round-6 delta — completing R24/R25 in place); W-15 and W-22 carry the mechanisms, RF-35–RF-37/RF-39/RF-40 track the interim (RF-38 files the anomaly-recovery question), SI-39 files the multi-window recovery enhancement. v0.10 integrates A27 (SI-32): the storage adversary model — three tiers (T1 offline tampering, T2 same-uid active writer, T3 legitimate human edit), a tier label on every publication-safety and read-back guarantee, the staged-bytes and verify-on-read-back disciplines ratified normative, per-kind entry rules, and the standing T2 containment-boundary sentence; determinations D32-1…D32-10 in ADR 0008, one protocol-class deferral (SI-40, the active-publication window). Unenforced clauses bind implementations at their carrier gates per the ledger (the W-20 §0 posture-qualifier convention will make this status first-class). Changelog at end. Risk-review requirements carried since v0.1: **(R2)** read authority is first-class, **(R3)** payloads are hash-referenced and destroyable, **(R6)** trust is domain-scoped, never scalar.* --- @@ -30,6 +30,8 @@ PayloadRef { "hash": "sha256:…", "size": 18742, **Cipher suite (A19).** v1 payload encryption and DEK wrapping are AES-256-GCM; each DEK record carries its `alg`, so the suite is per-payload upgradable without a schema change. +**Adversary tiers (A27).** Payload reads are authority-bearing read-backs under §5.3's storage adversary model: bytes rehash to their `PayloadRef.hash` before consumption (A27.2), and the store's publication safety carries A27's tier labels. + ## 2. Principals ```json @@ -216,7 +218,11 @@ Promotion merges a completed branch to trunk and is the only mutation in the sys - **Owned-state transition protocol (A24; resolves SI-31).** Promotion and revert are owned-state transitions: simultaneously a Tier-1 root mutation and a globally-ordered trace event, executed as **one SQLite transaction with one commit point** (§6.2 S1–S5 ratified the seam and the durable-commit profile; this bullet is the §5.3-owned half). Canonical sequence, under M8's gate serialization: (1) verify every live root still equals the transition's `before` image — divergence fails the transition closed; the gate's M8 attribution owns consuming it; (2) prepare forward *and* rollback images for every root, hash-verified out of the CAS into immutable plans before any mutation; (3) stage, in one uncommitted transaction: the signed transition event, the expected-root attestations, and every companion row (promotion status, the A26 companion approval); (4) publish the fabric-signed **recovery journal**, fsyncing file and parent directory *before* any store is touched (R8: the record precedes what it records); (5) apply and fsync every store; (6) **commit the transaction** — the single authoritative commit point; the linked verified event is the authoritative name of the committed root tuple; (7) remove the journal. The naming claim requires **exact realization at canonical-entry grain** — content, presence, kind, and mode — at every stage: the merge planner must surface every canonical-entry difference as an op or conflict (a mode-only branch change silently dropped at planning defeats realization before apply begins — round 5, R23), and the store apply must write every differing dimension and complete legal topology changes (file↔directory) under its pass ordering (round 4). Composition with A17 (R23, completed at the internal pre-round-6 review): rename/move **pairing** remains exact content-hash — A17 unchanged, so a chmod'd rename never un-pairs into the mass-deletion rendering A13 forbids — and a paired rename/move whose endpoints differ in mode or kind additionally surfaces that delta as a `modify` refinement op on the destination path (A18); conflict identity and conflict cards compare canonical entries, not content hashes alone. The merged implementation fails all three stages — as-built defects, RF-40, carried by W-15. Ordinary failure restores and fsyncs every before-root, rolls the transaction back, and removes the journal; failure *during that rollback* retains the journal loudly for reopen recovery. A retried transition is a fresh event — the journal carries authority only to complete or undo the transition it names, never to re-execute it. - **The recovery journal (A24).** A fabric-signed, typed (`state_change_recovery`), versioned artifact linking exactly one transition event and carrying the full before/after root tuples. It is **not a §6 event kind** and never becomes one: §6 records are permanent substrate history, while the journal's *removal* is protocol-meaningful (presence is the recovery discriminator, S4), and the permanence job belongs to the transition event. It is necessarily a **file outside the database**: it is the write-ahead record for the transaction itself, so a row would vanish in exactly the crash it exists to recover (the inverse of R8's checkpoint-is-a-row argument — one artifact per side of the commit point, each durable where its consumer looks). It gains signed `home`/`epoch` binding (its linked event's TracePosition) and the per-store prior-attestation positions (R11) at the layer-1 implementation (W-15) — H3's one-home/one-epoch rule extended to recovery artifacts. The epoch guard splits by arm (R3): roll-forward requires the linked event to lie in the current epoch's activation prefix (ADR 0006 R9); roll-back — where the linked event is by definition absent, absence being the roll-direction discriminator — requires the journal's own signed home/epoch to name the current home and epoch. The guard never reuses the discriminator. - **Recovery (A24).** Reopen with a journal present runs recovery under the gate lock before any other operation; recovery failure keeps the home closed, and recovery is re-runnable — a crash during recovery re-runs it, completing or refusing per the explanation check below. Validation precedes any restore: symlinked, non-file, mistyped, wrong-version, unsupported-kind, malformed-tuple, or event/manifest-misbound journals are rejected. The roll decision reads the verified prefix at the **local verified terminal** (R6, S4): linked event present → roll forward to `after`; absent → roll back to `before`. Two clauses guard the decision. **Freshness (D31-4, as revised in rounds 1–2):** recovery derives the substrate's current-roots view **V** from the verified prefix — per store, the latest signed root attestation in composite order, intensionally *every signed event field attesting that store's live trunk root*, enumerated today as: manifest `snapshot` events, unbranched `tool_call.state_root_after` (non-`branch:` keys only — a branch capture never joins trunk V; R8), promotion `merged`, revert `roots_restored`, drift `observed_root`, and the closing record below — never from the unsigned expected-roots cache (§5.5's doctrine applied to recovery's inputs). V is evaluated once, against pre-recovery signed state, after the closing-record idempotency check and before any recovery emission; comparison projects V onto the journal's store set (a manifest deliberately scoped to a subset of the home's stores journals only that subset); a journal naming a store with no signed attestation fails closed. Freshness is **positional, not value-only** (R11 — roots recur, positions do not: the ABA replay `A → C → A` defeats value equality): the journal records, per store, the event id of the latest root attestation at prepare time (well-defined under the gate lock after drift attribution); roll-back requires each recorded position to still be the latest attestation for its store, roll-forward requires the linked event itself to be the latest, and value agreement (`after == V` / `before == V`) is retained as belt-and-braces. Any other relation fails closed, journal retained, loud. Scope of the guarantee (R22): layer 1 rejects a stale journal **relative to a non-rolled-back local verified prefix** — backup-restored, copied, replanted, and same-epoch ABA-replayed journals all fail the positional predicate there. A journal and database rolled back **together** present an internally consistent history layer 1 cannot see through; detecting that is A23 layer 2's anchor-ahead signal, and under the unanchored `local-integrity` profile such a joint rollback can drive a restore — with the capture record and window drift still preserving the overwritten live state as CAS-resident, ledger-visible evidence (wrong, but never silent). **Attribution (D31-6, as revised in rounds 1–2):** before any restore mutation, recovery captures every affected live root to the CAS and publishes the **recovery capture record** (R9) — a second fabric-signed, typed, versioned recovery artifact naming the journal id and the per-store captured roots, fsynced (file and parent) before any store is touched. This is the write-ahead principle at its third boundary (journal↔stores, database↔anchor, capture↔restore), crystallizing a two-sided rule: **intentions are write-ahead files; attestations are write-behind events** — without the record, a crash between restore and emission leaves the captured bytes in the CAS with no durable name, and the retry, seeing live == V, has nothing to say: the divergence window vanishes unrecorded (round 2's refutation of round 1's convergence claim); while a pre-restore closing *event* would attest a restore that has not happened. The record is **first-write-wins** — a retry never re-captures over an existing record; the original capture is the evidence and live may already be half-restored — and is validated by the journal's fail-closed family (signature, type, version, journal-id linkage, and store set **exactly equal to the journal's** — R15: pair-or-neither needs every journaled store's captured root to prove non-divergence, and first-write-wins forbids repairing a partial record) before being consulted. Because first-write-wins blinds the record to divergence arising during a crashed recovery's own downtime, a retry verifies — per store, before any mutation — that **every path-state is explained by the capture record's version or the restore target's version**, quantified over the **union** of paths present in live state, the capture, and the target, with **absence as a value** (R14/R18 — quantifying over live elements alone is vacuous for deletions: a path deleted during the window is not a live element, and the restore would silently recreate it). Canonical entry identity is (relative path, **kind**, presence, content hash, executable mode) — content alone is not identity, and because the canonical store grammar contains only regular files and directories, a live entry of any other kind (symlink, fifo, socket, device) can match neither the capture nor the target and is definitionally unexplained (R21). The kind-complete scan runs on **every active restore attempt** (R24 as completed in round 7): an attempt that has not yet published a capture record scans **before publishing it** and before any mutation (round 6: the round-5 retry-only scoping left the first attempt free to capture-around a socket and restore over it unrecorded; capture rejects symlinks but silently *skips* other non-file kinds, so the grammar exclusion must be enforced by the scan itself, never inherited from a capture-shaped walk); a retry with an existing capture record scans **before any restore mutation** — its scan is the explanation check's own kind-complete walk; and a retry that finds committed closing records naming the journal is **not an active restore attempt**: it routes directly to artifact cleanup per the settled idempotency rule, mutating only the two recovery artifacts — a non-canonical entry appearing after the restore completed is ordinary post-recovery live state, owned by M8 drift attribution at the next gate consumption, never by recovery. On active attempts the walk MUST surface every directory entry regardless of kind, failing closed on unreadable or unstatable entries, and any non-canonical entry within the canonical store boundary fails recovery closed in place. Path-state is a **tagged domain** (R25 as completed in round 7): the path universe is the **non-empty canonical relative paths strictly below the store root** — the root itself is the boundary, not a path-state: always present, created at materialization, never pruned (so an empty capture against an empty target recovers trivially instead of failing closed on the root) — and each path carries `absent` | `file(content hash, executable mode)` | `implicit-directory`, where a path is implicit-directory in the capture or target exactly when it is a proper ancestor of a tracked file path in that files-only tree (directories carry no content or mode dimensions; a live directory is explained by implicit-directory status on either side, a live file where both sides are implicit-directory — or the converse — is a kind mismatch and unexplained); excluded directories and untracked empty directories are scoped to the canonical store boundary, matching capture (their apply-time obstruction handling is RF-40's widened topology rule). Per file for fs stores (whose tmp+rename restore leaves each entry at exactly one of the two versions); whole-image for sqlite (whose swap is atomic). All-explained is an innocent interrupted restore and completes idempotently; any unexplained path-state is a new divergence window and **fails closed in place** — no mutation, the new state stays live, the home stays closed, operator resolution. The automated capture-and-attribute guarantee therefore covers the divergence present when recovery first begins; later-window divergence is detected and preserved by refusal, never silently normalized — automated multi-window preservation is SI-39. **No event is emitted until the restore completes** (emitting first would make the captured root the newest attestation and poison V — round 1). After the stores are restored and fsynced, **one atomic transaction** appends, per divergent store, the window drift (V → captured root, `attribution: "unattributed"` — the crash-to-reopen window is unattended) ordered before that store's **closing record** (drift-kind, `attribution: "fabric_recovery"`, carrying `recovery: `, re-attesting the restored root, §6) — **pair-or-neither per store** (R13): a store whose capture equals V emits nothing. Then the capture record is removed, **then** the journal — that order, so "journal present, capture record absent, closing records committed" is a legible cleanup cell and an orphaned capture record is unreachable. A committed closing record naming the journal id is the idempotency marker: a retry finding one routes directly to artifact cleanup, before the freshness predicate (whose "latest attestation" the closing record has by then become). **Recovery never moves V** (normative): both arms' restore target equals V by the freshness predicate and every pair nets V to itself — the round-1 poisoning is unrepresentable, not merely avoided. Content that diverged while the fabric was down is preserved and ledger-visible, never silently overwritten (M8 applied to the recovery consumer). When live state equals the restore target and no capture differs from V, recovery completes as pure cleanup. -- **Scope fences (A24).** Tier-1-local only: the analog for external effects (a remote side effect landing before its record) is the parked durable external-effect protocol, never this one. The filesystem adversary tiers under which the publication discipline holds — for stores and for both transient recovery artifacts — are SI-32's question. The hard-exit-at-each-syscall crash matrix remains G3 evidence work. +- **Scope fences (A24).** Tier-1-local only: the analog for external effects (a remote side effect landing before its record) is the parked durable external-effect protocol, never this one. The filesystem adversary tiers under which the publication discipline holds — for stores and for both transient recovery artifacts — are A27's, below. The hard-exit-at-each-syscall crash matrix remains G3 evidence work. +- **Storage adversary model (A27; resolves SI-32).** Every publication-safety and authority-bearing read-back guarantee is tier-labeled *by reference* — through this bullet family and ADR 0008's matrices, inventory, and determinations D32-1…D32-10 — naming the *weakest* adversary tier under which it holds; new normative claims MUST carry their label inline. (Adversary tiers, distinct from the Tier-1/2/3 *store* tiers of the brief §5.2 and §10's walkthrough.) **T1 — offline storage tampering between processes** (backup/restore, copied homes, same-uid writes while idle): defeated by cryptography — content-address or signature verification on every authority-bearing read-back, plus A27.3's per-kind rules at capture (a first read has no address to verify against yet) — with three named carve-outs that are NOT content-addressing's to win: *trust-root reads* (keys, KEK, credentials — nothing to verify against; under same-uid T1 an SI-27/RF-14-backed residual, accepted under SU, labeled T1-boundary and never T1-cryptographic), *unsigned-index reads* (operational pointers — store paths, `substrate_span` — MUST derive from signed substrate or be bound into it, never from an unsigned row; RF-41 until enforced), and *rollback/replay to a prior genuinely-signed whole-home state* — rollback is **A23's**, not content-addressing's: layer 1 detects only rollback inconsistent relative to a surviving expected terminal; a coherent suffix regression is layer 2's at its gates (D32-2 — the seam stays inside this model so T1 is never read as complete). T1's at-rest-confidentiality half is P17/RF-15's — cross-referenced, not re-tiered (D32-1). **T2 — active same-privilege writer** across a publication's prepare→verify→rename window: no sequence of pathname checks wins; the honest answer is OS-enforced exclusion (W-4's topology). **T3 — the legitimate concurrent human edit**: not an attack; its correctness criterion is *attributed, never lost* (M8 between attested roots; A24/R14 during recovery downtime; the remaining in-publication window is SI-40, posture-bounded by P29). Out-of-scope boundary: root/privileged-host is out of scope (the trust boundary is the Unix account, P7); an ordinary different-uid process is *defended* by the enforced 0700/0600 home permissions, not out of scope. +- **Publication and read-back discipline (A27.1/A27.2, normative).** *Staged bytes (A27.1):* a commit consumes bytes verified in memory during prepare and never re-reads mutable storage to source the bytes it installs; re-reading live storage for comparison or enumeration is permitted — the invariant is on the provenance of installed bytes. Prepare is the single verification point; commit is a pure function of already-trusted bytes. *Verify-on-read-back (A27.2):* every read-back of storage bytes that will bear authority MUST content-address-verify (or signature-verify, which subsumes it — A19) before the bytes are consumed; a mismatch is a loud denial, never a fallback. Holds against T1, with the three carve-out classes above carrying their own rules. +- **Per-kind entry rules (A27.3, normative).** *fs-tree stores:* the canonical grammar is exactly A24/R25's tagged path-state domain — `absent | file(content hash, executable mode) | implicit-directory`; any other kind (symlink, fifo, socket, device) is definitionally non-canonical. Capture rejects symlinks; recovery runs the R24 kind-complete scan; publication writes through an `O_EXCL|O_NOFOLLOW` randomized sibling with a same-inode non-symlink recheck immediately before rename — documented as *race-narrowing, never race-closing* (see A27.4); excluded directories are never written, deleted, or traversed ("excluded means untouched," uniform through every walk — RF-43 until `sync_store`'s walk complies). *SQLite stores:* the whole file is the unit; images are captured WAL-checkpointed, published through the same atomic primitive, stale `-wal`/`-shm` sidecars removed after the swap; **every** SQLite store path — registered stores included, not only `fabric.db` — is symlink-rejected before capture/open (RF-42 until enforced). *Hardlinks:* captured by content like any regular file; the write-through-a-link variant is defeated structurally by the atomic publisher (rename replaces the directory entry); the pre-plant-and-race variant is T2, COOP/W-4-bounded. +- **Containment boundary (A27.4, normative).** Every T2 publication-safety claim carries the standing sentence: **"holds under COOP; requires W-4 containment at G-ADVERSARIAL."** No pathname check is ever claimed to defeat a same-uid active adversary, and the security argument for any race-narrowing step MUST name COOP as the assumption it rests on. T2 claims are publishable as labeled conditionals (D32-8; the posture ledger's G-PUBLISH gate entry makes the dependency reader-visible); a published T2 claim missing its label is a publication defect. ### 5.4 Closure and revocation (A22) @@ -380,7 +386,7 @@ Not a score: raw, trace-backed counters per (principal, domain, skill-version). ## 9. Forks - **F1 — resolved (v1):** broker-minted capabilities. Central, revocable, meterable; format stays compatible with offline attenuation (the `parent` chain + §5.2); revisit when deep delegation trees arrive and metering has an answer. "Revocable" has normative semantics since A22 (§5.4): signed `revoke` events, permanent per id, prospective, descendant-closing through the ancestry view. -- **F2 — resolved in direction (A16; ADR 0002 in the reference implementation):** the control plane stays JCS (with the §0 integer rule); state roots become CIDv1 over DAG-CBOR tree nodes with raw leaf blobs and chunked large objects (sqlite chunked on page boundaries). `sha256:` is the sanctioned interim root encoding until the execution checkpoint (post-dogfooding storage tripwires); readers MUST accept both during the transition. Scope fence: CIDs ≠ IPFS — an addressing/serialization format only; no DHT, no gateways, no sync protocol. +- **F2 — resolved in direction (A16; ADR 0002 in the reference implementation):** the control plane stays JCS (with the §0 integer rule); state roots become CIDv1 over DAG-CBOR tree nodes with raw leaf blobs and chunked large objects (sqlite chunked on page boundaries). `sha256:` is the sanctioned interim root encoding until the execution checkpoint (post-dogfooding storage tripwires); readers MUST accept both during the transition. Scope fence: CIDs ≠ IPFS — an addressing/serialization format only; no DHT, no gateways, no sync protocol. A27's publication/read-back rules (§5.3) are stated over content addresses generally and survive this transition unchanged; the migration note is W-6/F2's, not the adversary model's. - **F3 — partially resolved:** sensitivity derived (domain defaults × taint propagation), ceilings from channel strength. Only finer-than-domain granularity remains open. - **F4 — resolved (default):** `summary` carries only fields the capability's caveats reference — minimization is automatic because the consent-relevant extract is definitionally the caveat-relevant extract. All fields redactable. Per-tool overrides possible at registration. @@ -394,6 +400,12 @@ Not a score: raw, trace-backed counters per (principal, domain, skill-version). 4. Run: each tool call traced with checks, meters, summary, `state_root_after`. Unknown-recipient draft → escalation → one-tap approval (channel-stamped). Third similar approval this month → clerk drafts a rule (k=3, least-general, counterfactuals attached, domain-matched, domain-scoped pin) for ratification at `approval.min_auth`. 5. Promotion gate: trace re-verified against capability; three-way merge to trunk; promotion event. Sixty days on, email payloads hit TTL → shred events. The run stays forever explainable, no longer readable. +## Changelog — v0.10 (amendment A27) + +*Resolves SI-32 (filed 2026-07-13 from the PR #43 review-cycle analysis; W-20 item 3). Candidate ADR 0008 built under the codified review battery (PR #51): publication-site inventory with file:line anchors, audit battery + internal adversarial pre-review before filing (which caught the rollback/A23 blocking find), external review round 1 folded (8 findings, among them the active-window T3 edit, the rollback carve-out's precise layer-1 domain, and the trust-root/different-uid boundary split; 5 of the class the battery now guards against). Ratified 2026-07-15, determinations D32-1…D32-10 in ADR 0008's addendum; the pre-round internal adversarial review returned 14 findings (4 medium), all applied before push. Per the review-battery stopping rule (adopted 2026-07-15, PR #52): one delta-scoped external round on this ratification text; continuation only on blocking findings.* + +A27 — storage adversary model: §5.3 gains the three-tier model (T1 offline tampering / T2 same-uid active writer / T3 legitimate human edit) with a tier label on every publication-safety and authority-bearing read-back guarantee. T1 is cryptography's (content-address/signature verification on read-back) with three carve-outs that are not: the trust root (SI-27/RF-14 residual under same-uid T1, accepted under SU — D32-3 rejected scoping T1 to a trusted verifier key as labeling an anchor that does not exist), unsigned indexes (operational pointers derive from signed substrate — RF-41 files the gap; `substrate_span` feeds broker authority, so its reach exceeds capture targeting), and rollback — **A23's, never content-addressing's** (D32-2: layer 1 detects only rollback inconsistent relative to a surviving expected terminal; a coherent suffix regression is layer 2's at its gates; the seam is kept inside the tier model so T1 cannot be read as complete). T2 is topology's: no pathname check is claimed to close the same-uid race; A27.4's standing sentence — "holds under COOP; requires W-4 containment at G-ADVERSARIAL" — attaches to every T2 claim, the same-inode recheck is documented race-narrowing, and T2 claims publish as labeled conditionals with the posture ledger's G-PUBLISH gate entry (D32-8). T3 is attribution's: attributed-never-lost, M8 between roots and A24/R14 during recovery downtime; the in-publication window is the ratification's one protocol-class deferral — **SI-40**, with D32-4 foreclosing the topology arm (native human access to shared state is the product thesis; momentary exclusion fails T3 closed on an honest save) and naming the preservation/refusal protocol (the A24/R14 capture-or-refuse shape at a per-entry pre-rename commit point) as leading candidate, bounded by P29 (1HUMAN/1SESS, sub-second Tier-1 windows) with triggers at slow-apply stores, G-2HUMAN, or a first observed loss in dogfooding. A27.1 ratifies the staged-bytes rule as normative (commit installs only prepare-verified bytes; comparison/enumeration reads permitted — the invariant is installed-byte provenance); A27.2 verify-on-read-back; A27.3 per-kind entry rules (fs-tree = A24/R25's tagged domain with non-canonical kinds rejected; SQLite whole-image, WAL-folded, sidecars removed, **every** store path symlink-rejected — RF-42 files the registered-store gap; hardlinks closed by content-capture plus the atomic publisher). RF-43 files the `sync_store` exclusion-uniformity defect; G14 files the outstanding negatives; enforcement binds at the carriers (RF-41 → W-15's signed-chain lane, the W-15a/W-15b split in flight in PR #52; RF-42/RF-43 → the RF-40 mechanical bugfix lane, defined at RF-42's entry). Rejected: a flat trusted-local-filesystem assumption (unlabeled claims are why every review re-derived the attacker); claiming pathname checks defeat T2 (the SI-10/A21 lie surface); folding T3 into the attacker model (re-opens the M8 gap A20 closed); deferring the whole model to W-4 (T1 is live now — backups, copied homes, RF-41/RF-42). + ## Changelog — v0.9 (amendments A24–A26) *Resolves SI-31, SI-33, SI-34 (filed 2026-07-13 from the PR #43/W-14 review cycle; ratified 2026-07-14 in the W-20 retro-ratification session, PR #48 — challenge pass over the merged implementation as candidate, fresh-eyes source verification, determinations D31-1…D31-6, D33-1…D33-5, D34-1…D34-4 as adjusted by seven rounds of review adjustments R1–R25 (round 7 scoped) plus an internal pre-round-6 review, all recorded in `docs/adr/0007-owned-state-consumed-authority-approval-binding.md`; provenance in `docs/spec-issues.md`). Claims about the candidate code carry three distinct labels — ratified as built, adjusted beyond as-built (RF-35–RF-37/RF-39; W-15/W-22 carry), and as-built defect (RF-40) — and unenforced clauses bind implementations at their carrier gates per the ledger. The first retro-ratifications: protocols designed inside a remediation cycle (the review-finding triage rule's founding case), ratified after the fact — with adjustments the merged code must still meet, which is what distinguishes a challenge pass from a rubber stamp. Round 1 of the independent-context review returned REQUEST CHANGES (nine findings, four high); it refuted one determination rationale outright (D34-3's narrowing theorem), caught a composition defect between two others (D31-4 × D31-6), a self-contradictory predicate (D31-2's epoch guard), and a false authority-surface evidence claim (gate replay) — ratified as R1–R7, filing RF-36/RF-37/RF-38 and W-22. Round 2 (seven findings, three high) refuted the round-1 repairs in turn: V's totality claim omitted unbranched `tool_call.state_root_after` (a genuine revert-crash recovery bricks — R8); the convergence claim did not survive a second crash (captured evidence had no durable name — R9's recovery capture record, the write-ahead principle's third boundary); value-only freshness passes ABA replays (R11's positional binding); "same per-store results" recreated the blanket re-park R2 rejects (R12's agent-originated quantifier); the exemption candidate was version-ambiguous under A9 batching and displayed from unsigned rows (R10, RF-39); a singular closing record cannot attest a multi-store tuple (R13's pair-or-neither). Ratified as R8–R13; recovery is now V-preserving by construction. Round 3 (five findings, two high, confined to the round-2 additions and evidence bookkeeping) caught the repeated-crash window R9's first-write-wins opened (an edit during a crashed recovery's own downtime could be normalized unrecorded — R14's element-wise explanation check fails closed in place, the guarantee honestly narrowed, SI-39 filing the multi-window enhancement), the capture record's too-weak subset validation (R15: exact set), and the undefined comparison basis for multi-path ops (R16: per-op touched-path result equality against the previewed merged tree); ratified as R14–R16, with the reviewer confirming no original SI decision point was silently dropped. Round 4 (five findings, three high) changed character: alongside R17 (the temporal-binding gap proved to live at **both** consumers — a later approval retro-funds an earlier effect at decision time too; RF-36 widened, the round-1 "gate weaker than decision" record corrected), R18 (R14's quantifier was vacuous for deletions — path-state over the union of live/capture/target, absence as a value, canonical-entry identity), and R20 (R16's claimed merged-tree referent did not exist — previews gain CAS-retained per-store `merged` roots, digest-covered), it surfaced the cycle's first **as-built defects**: the merged fs apply skips mode-only differences and deadlocks on file↔directory topology swaps (RF-40 — a committed event could name a root live state does not realize; a legitimate recovery bricks). Ratified as R17–R20. Round 5 (three blocking findings) completed canonical-entry identity with **kind** — a symlink matching on bytes and mode was indistinguishable under the R18 tuple; non-canonical kinds are now definitionally unexplained (R21) — **layer-qualified the stale-journal guarantee** (R22: joint journal+database rollback is layer 2's anchor-ahead case; the drafted "can never drive a restore" overclaimed layer 1), and widened RF-40 to the merge planner (R23: entries reduced to content hash drop mode-only branch changes as silent no-op promotions — a deliberate, unledgered Stage-3 shortcut whose own rationale fails when no side is chosen). It also cleared three seeded attack surfaces as sound (R17 cross-span positions; R20 referent retention with parked promotions as future GC roots; RF-40's topology remedy under excluded dirs). Ratified as R21–R23. The internal pre-round-6 review then corrected 13 findings in the round-5 text (two high: the R23×A17 rename/mode composition; the kind-complete-walk vacuity), and round 6 — the narrowest of the cycle, completeness table fully green — ratified **R24** (the kind-complete scan runs on every recovery attempt, before the capture record and any mutation: the retry-scoped version left the first attempt free to restore over a socket unrecorded) and **R25** (the tagged path-state domain — absent | file(hash, mode) | implicit-directory — defining directory path-states over the files-only tree so the union quantifier neither bricks nested stores nor misses file↔directory mixed states), with G13(m)–(q) filing the completion negatives and the SI-33/SI-34 labels corrected to the three-category discipline. Round 7, scoped to the round-6 delta, completed R24/R25 in place: the scan quantifies over **active restore attempts** (first attempt: before capture-record publication; retry: before any restore mutation; closing-record cleanup exempt — a post-completion non-canonical entry is M8's at next consumption, never recovery's) and the path universe excludes the store root (the boundary, not a path-state — an empty-capture/empty-target recovery completes instead of bricking on the root); the G13(m–q) carrier roll reached W-15/W-22. Three mediums, no highs, no skeleton findings; findings 1–2 of round 6 verified closed by these completions, 3–4 by the round-6 commit.* diff --git a/docs/posture-assumptions.md b/docs/posture-assumptions.md index 4549ccd..c949853 100644 --- a/docs/posture-assumptions.md +++ b/docs/posture-assumptions.md @@ -171,6 +171,18 @@ default cannot save this class retroactively. (`human_local`); `tool_known` / `unattributed` deferred. → multi-actor roots/visibility (roadmap parked); SI-20/A20 (timing resolved); `dogfooding.md` "invite a second person" gate. +- **P29** *(filed 2026-07-15, SI-40/D32-4)* A human edit landing inside a + live promotion/revert apply window (after the prepare-time + capture-equals-`before` check, before an entry's rename) is overwritten + with no CAS capture and no drift event — T3's "attributed, never lost" + violated in the one window M8 and A24/R14 do not cover. Safe now: + 1HUMAN/1SESS make the colliding writer the same person who initiated + the transition, and the Tier-1 apply window is sub-second. Un-safed by: + a second human (this gate), or — earlier, in expectation — any store + whose apply window is not sub-second (Tier-2/3 applies), or a first + observed loss in dogfooding (evidence this rationale failed). → SI-40 + (preservation/refusal protocol, leading candidate; the topology arm is + foreclosed by D32-4 — the human is never excluded from shared state). ### G-CONCURRENT — before a second concurrent session in one home *(relaxes 1SESS)* - **P20** `current_manifest` / `current_span` are mutable **home-global** @@ -271,12 +283,21 @@ in scope, or when storage leaves that filesystem boundary. extract §5.4 conformance vectors from the A22 contract tests. - SI-23 constraints and the brief §8 landscape claim should also be settled before publication (both already tracked). +- **T2 conditional publication (A27/D32-8, 2026-07-15 — NEW):** every + published T2 publication-safety claim carries the A27.4 label — "holds + under COOP; requires W-4 containment at G-ADVERSARIAL." The label is the + honest disclosure; W-4 need not land before publication, but a published + T2 claim missing its label is a publication defect, and the conformance + sweep for any published spec text checks the labels rather than implying + a test proves T2 safety. --- ## The shortcut ledger (backing index) -All 27 currently tracked, grouped by filing status. `SU/COOP/LOCAL/NOACT/ +All 28 currently tracked, grouped by filing status (P28 is allocated by +the in-flight heading-check filings, PR #52 — numbered around here to +avoid the SI-22/SI-35 collision class). `SU/COOP/LOCAL/NOACT/ 1SESS/1HUMAN/1TEN/DEBUG` = the invariant(s) that make each safe now. ### Tier 1 — items this sweep filed or newly gated @@ -292,6 +313,7 @@ All 27 currently tracked, grouped by filing status. `SU/COOP/LOCAL/NOACT/ | P25 | ~~F1 calls capabilities revocable with expiry-only enforcement~~ **CLOSED by W-8 (PR #33)**: signed `revoke` (§5.4), `a22_*` event-derived liveness at decision + gate, `asf revoke` kill switch | `broker.rs` (`a22_*`, `revoke_capability`), `proxy.rs` revoke surface | — (implemented) | SI-24 → A22 (v0.7) → W-8; residuals: P22 (in-flight dispatch), P15/RF-13 (rollback erasure), P21 (partitions) | | P26 | Corpus-ingest homes: placeholder identities/behavior signed into a real substrate; evidence-quarantined by convention only (second P8 site) | `asf-cli corpus/ingest.rs` (`placeholder_key`, behavior literal) | COOP SU | W-3 mechanical exclusion (G-RATCHET); `agent-trace-corpora-2026-07-11.md` boundaries | | P27 | Injected credentials enter a downstream adapter whose response reaches the agent unchanged | `broker.rs:560-576`, `proxy.rs:501-571` | COOP LOCAL, first-party/no credential | RF-23; G-EGRESS/G-3P-TOOL; W-18 | +| P29 | Human edit inside a live apply window overwritten uncaptured (post-prepare-check, pre-rename) | gate window (`kernel.rs`); fs apply (`snapshot.rs`) | 1HUMAN 1SESS (sub-second Tier-1 windows; self-inflicted concurrency) | SI-40 (D32-4); G-2HUMAN / first slow-apply store / first observed loss | ### Tier 2 — items already tracked (this ledger just indexes and gates them) @@ -344,3 +366,7 @@ All 27 currently tracked, grouped by filing status. `SU/COOP/LOCAL/NOACT/ switch, closed-parent/closed-id refusals). First closed row in this ledger; the row is kept struck-through as the record. Residual exposure moved to where it already lived: P22, P15/RF-13, P21. +- **Follow-up (SI-32 ratification, 2026-07-15, A27):** added P29 (the + active-publication-window edit, SI-40/D32-4) and the G-PUBLISH T2 + conditional-publication row (D32-8) — the ledger's same-change rule, + applied at ratification time. diff --git a/docs/review-findings.md b/docs/review-findings.md index 23325da..12d43c8 100644 --- a/docs/review-findings.md +++ b/docs/review-findings.md @@ -1128,6 +1128,80 @@ entry-identity requirement by cross-reference. RF-40 may constitute the parked RF-27 recovery-hardening trigger; un-parking is an operator gate decision, not a review outcome. +## RF-41 — unsigned `fabric.db` meta rows are consumed for authority while sibling events are signed — open (medium, posture-bounded) + +**Severity: medium. Direction: FAIL-OPEN at future boundaries (copied, +substituted, or shared homes); none under SU.** Filed by the SI-32 +ratification (ADR 0008 inventory R7, A27.2's unsigned-index rule; +surfaced drafting the candidate, confirmed by external round 1). Store +paths (the `stores` rows) and `substrate_span` are read from unsigned +meta/index rows ([kernel.rs:505](crates/asf-kernel/src/kernel.rs:505)) +that a substituted `fabric.db` controls, while the *events* in the same +database are signature-verified. Reach: `substrate_span` is not merely a +capture/restore pointer — it feeds broker authority evaluation (grant +ordering, A22 closure liveness), so a T1 tamperer can redirect which +store a capture/restore targets or which span is the substrate span +under an otherwise-fresh, fully-verifying head. This is A23's seam from +the read side: the anchor proves the head is current, but the +operational pointers its events reference must themselves derive from +signed substrate. + +**Why not currently exploitable:** SU — the same-uid tamperer holds the +fabric key and forges signed events outright (RF-13's boundary +argument); real at G-MULTITENANT and whenever the home leaves the local +filesystem boundary. + +**Fix direction:** operational pointers consumed for authority derive +from signed substrate or are bound into it (A27.2, normative) — never +from an unsigned row. Natural carrier: **W-15a/W-15b** (the W-15 split +in flight in PR #52's heading-check filings) — the signed +global chain work touches the same region, and binding `substrate_span` +and store registration into signed events is the read-side complement of +the `VerifiedPrefix`. Negative (G14(a)): a substituted meta row must not +redirect any authority-bearing read — no effect, loud denial. + +## RF-42 — `capture_sqlite` follows symlinks for registered SQLite stores; only `fabric.db` is symlink-rejected — open (medium, posture-bounded) + +**Severity: medium. Direction: FAIL-OPEN (wrong bytes bear authority).** +Filed by the SI-32 ratification (ADR 0008 inventory R10, A27.3's SQLite +rule; external round 1, finding 4). `capture_sqlite` +([snapshot.rs:268](crates/asf-kernel/src/snapshot.rs:268)) reaches the +store via `is_file` → `Connection::open` → `fs::read`, all +symlink-following; only the fabric's own `fabric.db` is symlink-checked +(`w13_validate_existing_fabric_home`, +[kernel.rs:328](crates/asf-kernel/src/kernel.rs:328)). A symlinked +registered store (e.g. `db:memory`) redirects capture to an +attacker-chosen database: the captured root — and every manifest, +promotion, and drift comparison attesting it — derives from bytes +outside the store boundary. + +**Why not currently exploitable:** SU/COOP — planting the symlink is a +same-uid act, and registration is first-party only (P6). + +**Fix direction:** symlink-reject every SQLite store path, registered +stores included, before capture/open — the same `symlink_metadata` +no-follow discipline `fabric.db` already receives (A27.3, normative). +Carrier: the **RF-40 mechanical bugfix lane** — PR #52's W-15 re-cut +records the operator option to pull RF-40's mechanism-class fixes +forward of W-15 as an independent PR; RF-42 and RF-43 ride whichever +runs first, that pull-forward or W-15b. Negative (G14(b)): a symlinked +registered store fails capture with no effect. + +## RF-43 — `sync_store` fs walk omits the `.git` exclusion and carries no symlink guard — open (low, hygiene) + +**Severity: low. Direction: none (fsync-only; no integrity impact) — +a uniformity defect.** Filed by the SI-32 ratification (ADR 0008 +inventory R3, A27.3). `sync_store` +([snapshot.rs:608](crates/asf-kernel/src/snapshot.rs:608)) walks the +store for durability fsyncs without `capture_fs`'s `.git` exclusion, +breaking the "excluded means untouched" uniformity A27.3 ratifies; it +also has no symlink check — *proposed hardening*, not an existing +discipline it violates (capture rejects symlinks via an explicit +`path_is_symlink` check, not an `O_NOFOLLOW` open). Fix: bring the walk +into line with capture's exclusion; take the no-follow hardening in the +same touch. Carrier: the same mechanical lane as RF-42. Negative +(G14(c)): the excluded directory is untouched through `sync_store`. + ## Verified sound during review (recorded so they aren't re-litigated) - Per-payload DEKs each perform exactly one encryption → no GCM nonce reuse diff --git a/docs/roadmap.md b/docs/roadmap.md index 0c9bcf6..66e2258 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -147,10 +147,28 @@ omits the `.git` exclusion). Ten ratification choices enumerated. Built via the codified review battery (PR #51): audit battery + internal adversarial pre-review before filing (which caught the rollback/A23 blocking find), then external review round 1 (8 findings — 5 the battery -now guards against, all folded). Spec untouched; SI-32 -open. Ratification session (challenge pass → determinations) is -tomorrow's operator work. Next after item (3): SI-26/28/29, SI-27, the -§0 posture-qualifier convention (item 6). +now guards against, all folded). **Item (3) ratified 2026-07-15** as +A27 (spec v0.10 §5.3; determinations D32-1…D32-10 in ADR 0008's +addendum): three tiers with the rollback carve-out kept inside the +model (D32-2) and the trust-root residual accepted (D32-3); +staged-bytes and verify-on-read-back normative; the A27.4 standing +containment sentence; conditional T2 publication with the G-PUBLISH +gate entry (D32-8). One divergence from the candidate's lean: D32-4 +forecloses the topology arm for the active-publication window (native +human access to shared state is the product thesis) and files **SI-40** +with the preservation/refusal protocol as leading candidate, bounded by +P29. Filings: RF-41 (carrier W-15a/b — the split PR #52 files), +RF-42/RF-43 (the RF-40 mechanical bugfix lane, defined at RF-42), SI-40, +P29, the G-PUBLISH gate entry, G14 negatives. Internal adversarial +pre-review before the external round: 14 findings (4 medium), all +applied — recorded in ADR 0008's addendum. Per the +review-battery stopping rule (adopted 2026-07-15, PR #52): one +delta-scoped external round on the ratification text, continuation only +on blocking findings. **Batch close-out per the operator decision filed +in PR #52:** items (4)–(6) defer to their named triggers — SI-26/28/29 +to W-16 and the W-6 publication gate, SI-27 to W-17, the §0 convention +riding with whichever fires first; W-20 closes when this ratification +merges. **W-21 — workboard dogfood profile (revival).** Owner: agent; operator ratifies the registration shape. Recovered from `codex/workboard-dogfood` diff --git a/docs/spec-issues.md b/docs/spec-issues.md index d369bb4..3e90a8b 100644 --- a/docs/spec-issues.md +++ b/docs/spec-issues.md @@ -33,7 +33,12 @@ > (TracePosition genesis representation) by the fourth (2026-07-14). > **SI-39** (recovery-window divergence: automated multi-window > preservation) was filed by round 3 of PR #48's review — the -> narrow-and-file remedy ratified as R14. New issues start at **SI-40**. +> narrow-and-file remedy ratified as R14. **SI-32 is resolved in v0.10 +> as A27** (W-20 item 3, ratified 2026-07-15: the three-tier storage +> adversary model, determinations D32-1…D32-10 in ADR 0008; RF-41–RF-43 +> file the surfaced gaps; the active-publication-window T3 edit is +> re-filed as **SI-40**, the ratification's one protocol-class +> deferral). New issues start at **SI-41**. Tracked per the handoff: where the spec is ambiguous or contradicts itself, we record the question, the interpretation the kernel implements, and why — @@ -46,6 +51,72 @@ tests encode it; flipping the reading is cheap. --- +## SI-40 — a human edit during a live promotion/revert apply window is overwritten with no capture or drift (§5.3, A24, M8) — open + +The one T3 window ADR 0008's model found uncovered (external review +round 1, finding 1 — the item ADR 0007 R14 explicitly deferred to +SI-32): M8 covers edits *between* attested roots and A24/R14 covers the +crash-recovery downtime, but the gate lock (`kernel.rs`) serializes +fabric *processes*, not a human with a text editor. A vault edit landing +after the prepare-time capture-equals-`before` check and before the +apply's rename is overwritten by the rename with no CAS capture and no +drift event — T3's "attributed, never lost" criterion violated inside a +live publication. + +Protocol-class (a remedy adds a preservation/refusal step — a new commit +point — or changes the write topology), so designed here, not in a PR. +**D32-4 (ADR 0008 addendum) forecloses one arm:** the topology remedy — +excluding the human edit surface during the publication window — is +rejected as structurally unavailable for shared-state stores: native, +unmediated human access to the vault is the product thesis (brief §2, +open-world shared state), and momentary exclusion (chmod/lock games) +fails T3 in the *other* direction — a bricked save is failing closed on +an honest edit. The **leading candidate** is therefore the +preservation/refusal protocol: the A24/R14 capture-or-refuse shape +extended to the in-process gate window — per-entry, immediately before +each rename, detect that the live target diverged from the prepare-time +image and either capture-and-attribute the divergent bytes (a new +write-ahead point inside the apply) or refuse that entry and fail the +transition closed with the divergence preserved. Design must answer: +where the mid-apply refusal leaves the half-applied tree — NOT the A24 +ordinary-failure rollback as-is: that arm restores `before` over the +divergent entry with no capture record (the record exists only on the +crash-recovery path), destroying exactly what the refusal exists to +preserve; a preserving refusal captures the divergent bytes before any +rollback, or fails like a crash (journal retained, home closed) so +reopen recovery's capture path owns them — what the capture artifact is +(the R9 capture record generalizes), and the cost budget (a per-entry recheck on every +apply pays a stat per file to defend a sub-second window). + +A third remedy direction, noted by the operator at ratification: +**substrate-assisted preservation.** On a CoW filesystem (ZFS, btrfs, +APFS) an instantaneous snapshot taken at gate-lock acquisition — or a +clone-and-swap publication — makes the window loss unrepresentable at +the block layer: an in-window edit lands either before the snapshot +(preserved there) or after the swap (ordinary M8 drift), never in a +clobberable middle. Costs, stated so the comparison is honest when a +trigger fires: a platform dependency the fabric has so far refused (the +CAS is deliberately CoW-snapshots-in-userspace, portable anywhere); a +second snapshot mechanism outside the CAS attribution pipeline — +preserved bytes must still be captured *into* the CAS and +drift-attributed to satisfy T3's criterion, so the fs snapshot is the +preservation substrate, never the ledger entry; and per-platform +divergence exactly where A27.3 just unified per-kind semantics. Shape: +a deployment-floor option (the RF-15/P17 "OS/full-disk floor" class), +not the portable default — evaluate against the per-entry +capture-or-refuse candidate when a trigger fires. + +Bounded today by **P29**: under 1HUMAN/1SESS the colliding writer is +the same person who initiated the transition, the Tier-1 apply window is +sub-second, and the loss is one file version usually still in an editor +buffer. Triggers, in expected order: any store whose apply window is not +sub-second (Tier-2/3 applies — minutes, not milliseconds — are when this +protocol earns ratification); G-2HUMAN (a second human makes the window +adversary-reachable in spirit); or a first observed loss in dogfooding. +*Provenance: PR #50 external review round 1, finding 1; ADR 0007 R14's +deferral; ratified as the narrow-and-file remedy in D32-4 +(2026-07-15).* + ## SI-39 — recovery-window divergence: automated preservation across repeated recovery crashes (§5.3, A24, M8) — open A24's recovery protocol (D31-6/R9/R14, ratified in PR #48) preserves and @@ -385,7 +456,36 @@ tables are demoted to compatibility caches never read for authorization; signer-anomalous capability, caveat, manifest, auth-strength, zero-use, duplicate-binding, and cross-capability edges fail closed. -## SI-32 — store publication has no defined filesystem attacker or required OS primitives (§5.3, §9 F2) — open +## SI-32 — store publication has no defined filesystem attacker or required OS primitives (§5.3, §9 F2) — RESOLVED (author, 2026-07-15) + +**Resolution: ratified as amendment A27 (spec v0.10, §5.3) — the +three-tier storage adversary model, determinations D32-1…D32-10 in ADR +0008's ratification addendum.** Ratified as candidate-drafted: three +tiers with T1's answers split by mechanism (content-address/signature +verification for substitution/corruption/truncation; **rollback +carved out to A23** — layer 1 detects only rollback inconsistent +relative to a surviving expected terminal, coherent suffix regression +is layer 2's at its gates — D32-2 keeps the seam inside the tier model +so T1 is never read as complete); trust-root substitution under +same-uid T1 accepted as an **SI-27/RF-14 residual** (D32-3; the +trusted-verifier-key alternative rejected as labeling an anchor that +does not exist); A27.1 staged-bytes and A27.2 verify-on-read-back +normative with the three read-back classes; A27.3 per-kind entry rules +(fs-tree = A24/R25's tagged domain; every SQLite store path +symlink-rejected; hardlinks closed by content-capture + the atomic +publisher); A27.4's standing containment sentence on every T2 claim +(D32-7); conditional T2 publication with the G-PUBLISH ledger row +(D32-8; the posture ledger's G-PUBLISH gate entry). **One divergence +from the candidate's lean (D32-4):** the +active-publication-window human edit is filed as SI-40 with the +topology arm foreclosed (native human access is the product thesis) +and the preservation/refusal protocol as leading candidate, bounded by +P29. Gaps filed: RF-41 (unsigned meta rows → the W-15a/b carrier — the +W-15 split in flight in PR #52), RF-42 +(registered-SQLite symlink capture) and RF-43 (`sync_store` exclusion +uniformity) → the RF-40 mechanical lane; outstanding negatives G14. +Enforcement binds at the carriers per the ledger; the tier labels are +normative now. The spec assumes content-addressed preparation and coherent restore but never defines the filesystem adversary those operations run against. diff --git a/docs/testing-theory.md b/docs/testing-theory.md index 68fb22a..886acff 100644 --- a/docs/testing-theory.md +++ b/docs/testing-theory.md @@ -527,6 +527,32 @@ contract lanes land with W-15, as do RF-40's (i)/(j)/(l)/(n)/(o) and RF-35's (k)/(m); (p)/(q) land with RF-37's W-22 lane, and (d)/(h) with RF-36's. +**G14. A27 tier-claim negatives outstanding after the SI-32 ratification +(2026-07-15).** (a) **Unsigned-index redirect** (A27.2/RF-41): a +substituted `fabric.db` meta row (store path, `substrate_span`) must not +redirect any authority-bearing read — no effect, loud denial; lands with +RF-41's carrier (W-15a/b — the W-15 split in flight in PR #52), +completing the READBACK-VERIFY contract the ADR's validation plan names. +Its would-be members exist today under other registrations +(`cas_get_rehashes_content_before_returning_it`, +`row_substitution_cannot_change_what_a_hash_resolves_to`, +`w14_reopen_rejects_mistyped_or_unsafe_recovery_journal_before_mutation`); +the contract id is minted when the lane lands. +(b) **Registered-SQLite symlink capture** (A27.3/RF-42): a symlinked +registered store fails capture with no effect — the existing +`fabric_home_and_database_symlinks_cannot_redirect_reopen_or_initialization` +covers `fabric.db` only; lands with RF-42's mechanical lane. (c) +**Excluded-dir uniformity through `sync_store`** (A27.3/RF-43): `.git` +untouched by the durability walk; same lane. (d) **Deliberate absence, +recorded so it is never "fixed":** T2 claims carry NO race-closure +negative — their conformance is the documented COOP dependency plus +W-4's future topology (A27.4); a test asserting the same-inode recheck +closes the race would assert a guarantee enforcement does not provide +(the SI-10/A21 lie surface). A27.1's core pair already exists as the +`RESTORE-INTEGRITY` contract (blob mutated after prepare cannot reach +the live store); the SI-40 preservation protocol's negatives are +designed with SI-40, not before. + ## Automation lanes | Lane | Purpose | From eff73b2ce7f35d1670d07cf56acb0ddc5512f0b8 Mon Sep 17 00:00:00 2001 From: XVVH Date: Wed, 15 Jul 2026 14:35:59 -0400 Subject: [PATCH 4/6] Fold the delta round (5 findings): A27.1 T1 label, D32-2 caller-pin correction, SI-40 snapshot-timing fix External delta round returned REQUEST CHANGES; all five findings folded. Blocking: A27.1 regains its ratified holds-against-T1 label (D32-5); the candidate body's "layer 2's job (external anchor / caller-pinned expected head)" is corrected in the D32-2 addendum - caller pins are layer 1's completeness tier, only the external anchor is layer 2. Non-blocking: the SI-40 substrate-assisted paragraph no longer claims a gate-acquisition snapshot closes the window (the in-window edit postdates it); the preserving variant is outgoing-state retention at swap plus diff/CAS-ingest/attribute. Lows: A27.4 label variants in preserved candidate text normalized; v0.9 stamps in CLAUDE.md and the spec status line bumped to v0.10. Round record in ADR 0008's addendum. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 +- docs/adr/0008-filesystem-attacker-model.md | 29 +++++++++++++- docs/asf-schema-spec.md | 4 +- docs/roadmap.md | 10 ++++- docs/spec-issues.md | 45 +++++++++++++--------- 5 files changed, 65 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 236de09..58ebc5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ Neutral, agent-agnostic state fabric: delegation manifests binding four lineages (state, authority, behavior, trace), snapshot-backed undo for owned state, brokered capabilities for external effects, human-ratified compilation loops (skills up, caveats down). Read `docs/agent-state-fabric-brief.md` (why/what, v0.2) and -`docs/asf-schema-spec.md` (the constitution, v0.9) before writing any code. +`docs/asf-schema-spec.md` (the constitution, v0.10) before writing any code. The spec wins over this file wherever they disagree. Spec ambiguities found while implementing go to `docs/spec-issues.md` (never silently interpret); SI-1…SI-21 are resolved (SI-20 → A20/M8 in v0.5; SI-21 → A21/M7 in v0.6); SI-22 is interpreted (gate replay clock, W-2); SI-24 is resolved (A22/§5.4 in v0.7 — capability early closure; implemented by W-8 under its gated matrix: A22 two-sided contract, targeted `a22_*` mutation lane, W-9 verdict-invariance reproduced bit-for-bit); SI-23 (actuation vs approval surfaces) is OPEN — no actuation-scoped tool may register before SI-23 resolves. SI-25 is resolved (A23/§6.2 in v0.8 — authenticated global order: signed per-home global_seq layer 1 normative now, external anchor layer 2 graduation-gated; W-15 implements; ratified via W-20 determinations D1–D7/S1–S5 in ADR 0006). SI-26…SI-30 remain OPEN from the 2026-07-12 cryptographic mechanism audit (signed type/domain, key lifecycle, AEAD envelope, post-shred generations, redaction commitments). SI-31/SI-33/SI-34 are resolved (A24–A26 in v0.9 — owned-state transition protocol §5.3, consumed authority §5.5, approval candidate binding §6; W-20 retro-ratification of the merged W-14 protocols via PR #48, determinations D31/D33/D34 plus seven rounds of review adjustments R1–R25 (round 7 scoped) and an internal pre-round-6 review in ADR 0007 — round 2 added the recovery capture record and positional freshness, round 3 hardened them, round 4 extended temporal binding to both consumption consumers, rounds 4–6 surfaced as-built defects and completed canonical-entry identity and the recovery path-state domain; spec-code deltas tracked as RF-35–RF-37/RF-39 (adjusted beyond as-built) and RF-40 (as-built fs-pipeline defects), carried by W-15/W-22; G13 files the outstanding negatives; unenforced clauses bind at their carrier gates per the ledger). SI-32 is resolved (A27/§5.3 in v0.10 — the three-tier storage adversary model: T1 offline tampering defeated by verify-on-read-back with the trust-root/unsigned-index/rollback carve-outs, T2 same-uid writer answered by topology never pathname checks — every T2 claim carries "holds under COOP; requires W-4 containment at G-ADVERSARIAL" — T3 human edit answered by attribution; staged-bytes normative; ratified 2026-07-15 via W-20 determinations D32-1…D32-10 in ADR 0008; RF-41 (unsigned meta rows → W-15a/b, the W-15 split in PR #52), RF-42/RF-43 (symlink/exclusion gaps → the RF-40 mechanical bugfix lane) file the gaps; SI-40 files the active-publication-window edit, its topology arm foreclosed by D32-4). SI-35 (workboard domain labels vs domain-taxonomy governance, recovered from codex/workboard-dogfood at the W-21 revival decision), SI-37 (TracePosition agreement predicate; W-15 enforces fail-closed from day one), and SI-38 (TracePosition genesis representation; W-15 implements a bootstrap-event floor provisionally) are OPEN; SI-36 (mid-run "actually do Y" amendments, §3.1/M1/M5) merged via PR #46. SI-39 (recovery-window divergence: automated multi-window preservation; filed by PR #48's round-3 review, the narrow-and-file remedy ratified as R14) and SI-40 (a human edit inside a live promotion/revert apply window is overwritten uncaptured; filed by the SI-32 ratification, P29-bounded, preservation-protocol candidate) are OPEN. New issues start at SI-41. diff --git a/docs/adr/0008-filesystem-attacker-model.md b/docs/adr/0008-filesystem-attacker-model.md index 533e98e..66764d5 100644 --- a/docs/adr/0008-filesystem-attacker-model.md +++ b/docs/adr/0008-filesystem-attacker-model.md @@ -315,14 +315,14 @@ honest sentence RF-20→RF-40 kept rediscovering the absence of. posture graduates), or folded into P17/RF-15's at-rest work? This decides whether SI-32 spawns a new RF or reuses one. (RF-42, the registered-SQLite symlink gap, is the same call — file now or fold.) -7. **A27.4 containment boundary wording** — the "COOP; W-4 at G-ADVERSARIAL" +7. **A27.4 containment boundary wording** — the "holds under COOP; requires W-4 containment at G-ADVERSARIAL" label on every T2 claim. Ratify as the standing sentence, or scope it per-site? Recommend standing sentence, since it is the same honest answer at every T2 site. 8. **G-PUBLISH containment mapping (external review finding 6; the filing's explicit ask).** The filing asked *which publication-safety claims require containment before G-PUBLISH*. The real fork: may the spec **publish the - conditional T2 claims** (labeled "holds under COOP; W-4 at G-ADVERSARIAL") + conditional T2 claims** (labeled "holds under COOP; requires W-4 containment at G-ADVERSARIAL") with W-4 deferred, or must **W-4 containment land before publication** so no published claim rests on an unbuilt topology? Recommend: publish the conditional claims with the label — the label *is* the honest disclosure — @@ -440,6 +440,16 @@ ratified as drafted, with the reasoning recorded so it is citable. relative to a surviving expected terminal; a coherent suffix regression is layer 2's at its gates; under SU the T1 rollback bite is accidental (backup staleness, copied homes), adversarial at G-MULTITENANT. + **Correction to the preserved body (external delta round, finding 2 — + this addendum wins):** the body's parenthetical "layer 2's job (the + external anchor / caller-pinned expected head)" mis-classes the caller + pin. A caller-pinned expected head is **layer 1's** — §6.2's local + `TraceCheckpoint` supplies the caller-pinned completeness tier at + every profile — so a regression that leaves a surviving caller pin + ahead of it is layer-1-detectable; only the **external monotonic + anchor** is layer 2. The determination's operative wording + ("inconsistent relative to a surviving expected terminal" = layer 1) + was already correct; the body sentence was the error. - **D32-3 — trust-root substitution accepted as the SI-27/RF-14 residual.** Ratified option 1. The alternative — scoping T1 to a trusted verifier key outside the tamperable home — labels an anchor @@ -520,3 +530,18 @@ its capture-time per-kind dependency and the D32-1 confidentiality cross-reference; the A27.4 label was quoted without "containment" in CLAUDE.md; P29 was missing the dogfooding-loss trigger; and the adversary-tier notation is now disambiguated from §10's store tiers. + +**External delta round (2026-07-15, the stopping rule's one budgeted +round).** REQUEST CHANGES — five findings, all folded same-day: two +blocking mediums with prescribed remedies (A27.1's ratified T1 label +had been dropped in spec integration, restored; the body's layer-2 +misclass of caller-pinned expected heads, corrected above in D32-2), +one non-blocking medium (the SI-40 substrate-assisted paragraph +over-claimed what a gate-acquisition snapshot preserves — the in-window +edit postdates that snapshot; corrected to outgoing-state-retention at +swap with its reconciliation costs), and two lows (A27.4 label +variants in preserved candidate text normalized to the exact ratified +sentence; two v0.9 version stamps bumped). The round verified: all ten +determinations have integration sites, A27.3 preserves the A24/R24/R25 +grammar by exact reference, SI-40's corrected refusal disposition is +sound, and RF-41/RF-42/RF-43 and G14's source/test claims check out. diff --git a/docs/asf-schema-spec.md b/docs/asf-schema-spec.md index 244fc99..e4e0a26 100644 --- a/docs/asf-schema-spec.md +++ b/docs/asf-schema-spec.md @@ -220,7 +220,7 @@ Promotion merges a completed branch to trunk and is the only mutation in the sys - **Recovery (A24).** Reopen with a journal present runs recovery under the gate lock before any other operation; recovery failure keeps the home closed, and recovery is re-runnable — a crash during recovery re-runs it, completing or refusing per the explanation check below. Validation precedes any restore: symlinked, non-file, mistyped, wrong-version, unsupported-kind, malformed-tuple, or event/manifest-misbound journals are rejected. The roll decision reads the verified prefix at the **local verified terminal** (R6, S4): linked event present → roll forward to `after`; absent → roll back to `before`. Two clauses guard the decision. **Freshness (D31-4, as revised in rounds 1–2):** recovery derives the substrate's current-roots view **V** from the verified prefix — per store, the latest signed root attestation in composite order, intensionally *every signed event field attesting that store's live trunk root*, enumerated today as: manifest `snapshot` events, unbranched `tool_call.state_root_after` (non-`branch:` keys only — a branch capture never joins trunk V; R8), promotion `merged`, revert `roots_restored`, drift `observed_root`, and the closing record below — never from the unsigned expected-roots cache (§5.5's doctrine applied to recovery's inputs). V is evaluated once, against pre-recovery signed state, after the closing-record idempotency check and before any recovery emission; comparison projects V onto the journal's store set (a manifest deliberately scoped to a subset of the home's stores journals only that subset); a journal naming a store with no signed attestation fails closed. Freshness is **positional, not value-only** (R11 — roots recur, positions do not: the ABA replay `A → C → A` defeats value equality): the journal records, per store, the event id of the latest root attestation at prepare time (well-defined under the gate lock after drift attribution); roll-back requires each recorded position to still be the latest attestation for its store, roll-forward requires the linked event itself to be the latest, and value agreement (`after == V` / `before == V`) is retained as belt-and-braces. Any other relation fails closed, journal retained, loud. Scope of the guarantee (R22): layer 1 rejects a stale journal **relative to a non-rolled-back local verified prefix** — backup-restored, copied, replanted, and same-epoch ABA-replayed journals all fail the positional predicate there. A journal and database rolled back **together** present an internally consistent history layer 1 cannot see through; detecting that is A23 layer 2's anchor-ahead signal, and under the unanchored `local-integrity` profile such a joint rollback can drive a restore — with the capture record and window drift still preserving the overwritten live state as CAS-resident, ledger-visible evidence (wrong, but never silent). **Attribution (D31-6, as revised in rounds 1–2):** before any restore mutation, recovery captures every affected live root to the CAS and publishes the **recovery capture record** (R9) — a second fabric-signed, typed, versioned recovery artifact naming the journal id and the per-store captured roots, fsynced (file and parent) before any store is touched. This is the write-ahead principle at its third boundary (journal↔stores, database↔anchor, capture↔restore), crystallizing a two-sided rule: **intentions are write-ahead files; attestations are write-behind events** — without the record, a crash between restore and emission leaves the captured bytes in the CAS with no durable name, and the retry, seeing live == V, has nothing to say: the divergence window vanishes unrecorded (round 2's refutation of round 1's convergence claim); while a pre-restore closing *event* would attest a restore that has not happened. The record is **first-write-wins** — a retry never re-captures over an existing record; the original capture is the evidence and live may already be half-restored — and is validated by the journal's fail-closed family (signature, type, version, journal-id linkage, and store set **exactly equal to the journal's** — R15: pair-or-neither needs every journaled store's captured root to prove non-divergence, and first-write-wins forbids repairing a partial record) before being consulted. Because first-write-wins blinds the record to divergence arising during a crashed recovery's own downtime, a retry verifies — per store, before any mutation — that **every path-state is explained by the capture record's version or the restore target's version**, quantified over the **union** of paths present in live state, the capture, and the target, with **absence as a value** (R14/R18 — quantifying over live elements alone is vacuous for deletions: a path deleted during the window is not a live element, and the restore would silently recreate it). Canonical entry identity is (relative path, **kind**, presence, content hash, executable mode) — content alone is not identity, and because the canonical store grammar contains only regular files and directories, a live entry of any other kind (symlink, fifo, socket, device) can match neither the capture nor the target and is definitionally unexplained (R21). The kind-complete scan runs on **every active restore attempt** (R24 as completed in round 7): an attempt that has not yet published a capture record scans **before publishing it** and before any mutation (round 6: the round-5 retry-only scoping left the first attempt free to capture-around a socket and restore over it unrecorded; capture rejects symlinks but silently *skips* other non-file kinds, so the grammar exclusion must be enforced by the scan itself, never inherited from a capture-shaped walk); a retry with an existing capture record scans **before any restore mutation** — its scan is the explanation check's own kind-complete walk; and a retry that finds committed closing records naming the journal is **not an active restore attempt**: it routes directly to artifact cleanup per the settled idempotency rule, mutating only the two recovery artifacts — a non-canonical entry appearing after the restore completed is ordinary post-recovery live state, owned by M8 drift attribution at the next gate consumption, never by recovery. On active attempts the walk MUST surface every directory entry regardless of kind, failing closed on unreadable or unstatable entries, and any non-canonical entry within the canonical store boundary fails recovery closed in place. Path-state is a **tagged domain** (R25 as completed in round 7): the path universe is the **non-empty canonical relative paths strictly below the store root** — the root itself is the boundary, not a path-state: always present, created at materialization, never pruned (so an empty capture against an empty target recovers trivially instead of failing closed on the root) — and each path carries `absent` | `file(content hash, executable mode)` | `implicit-directory`, where a path is implicit-directory in the capture or target exactly when it is a proper ancestor of a tracked file path in that files-only tree (directories carry no content or mode dimensions; a live directory is explained by implicit-directory status on either side, a live file where both sides are implicit-directory — or the converse — is a kind mismatch and unexplained); excluded directories and untracked empty directories are scoped to the canonical store boundary, matching capture (their apply-time obstruction handling is RF-40's widened topology rule). Per file for fs stores (whose tmp+rename restore leaves each entry at exactly one of the two versions); whole-image for sqlite (whose swap is atomic). All-explained is an innocent interrupted restore and completes idempotently; any unexplained path-state is a new divergence window and **fails closed in place** — no mutation, the new state stays live, the home stays closed, operator resolution. The automated capture-and-attribute guarantee therefore covers the divergence present when recovery first begins; later-window divergence is detected and preserved by refusal, never silently normalized — automated multi-window preservation is SI-39. **No event is emitted until the restore completes** (emitting first would make the captured root the newest attestation and poison V — round 1). After the stores are restored and fsynced, **one atomic transaction** appends, per divergent store, the window drift (V → captured root, `attribution: "unattributed"` — the crash-to-reopen window is unattended) ordered before that store's **closing record** (drift-kind, `attribution: "fabric_recovery"`, carrying `recovery: `, re-attesting the restored root, §6) — **pair-or-neither per store** (R13): a store whose capture equals V emits nothing. Then the capture record is removed, **then** the journal — that order, so "journal present, capture record absent, closing records committed" is a legible cleanup cell and an orphaned capture record is unreachable. A committed closing record naming the journal id is the idempotency marker: a retry finding one routes directly to artifact cleanup, before the freshness predicate (whose "latest attestation" the closing record has by then become). **Recovery never moves V** (normative): both arms' restore target equals V by the freshness predicate and every pair nets V to itself — the round-1 poisoning is unrepresentable, not merely avoided. Content that diverged while the fabric was down is preserved and ledger-visible, never silently overwritten (M8 applied to the recovery consumer). When live state equals the restore target and no capture differs from V, recovery completes as pure cleanup. - **Scope fences (A24).** Tier-1-local only: the analog for external effects (a remote side effect landing before its record) is the parked durable external-effect protocol, never this one. The filesystem adversary tiers under which the publication discipline holds — for stores and for both transient recovery artifacts — are A27's, below. The hard-exit-at-each-syscall crash matrix remains G3 evidence work. - **Storage adversary model (A27; resolves SI-32).** Every publication-safety and authority-bearing read-back guarantee is tier-labeled *by reference* — through this bullet family and ADR 0008's matrices, inventory, and determinations D32-1…D32-10 — naming the *weakest* adversary tier under which it holds; new normative claims MUST carry their label inline. (Adversary tiers, distinct from the Tier-1/2/3 *store* tiers of the brief §5.2 and §10's walkthrough.) **T1 — offline storage tampering between processes** (backup/restore, copied homes, same-uid writes while idle): defeated by cryptography — content-address or signature verification on every authority-bearing read-back, plus A27.3's per-kind rules at capture (a first read has no address to verify against yet) — with three named carve-outs that are NOT content-addressing's to win: *trust-root reads* (keys, KEK, credentials — nothing to verify against; under same-uid T1 an SI-27/RF-14-backed residual, accepted under SU, labeled T1-boundary and never T1-cryptographic), *unsigned-index reads* (operational pointers — store paths, `substrate_span` — MUST derive from signed substrate or be bound into it, never from an unsigned row; RF-41 until enforced), and *rollback/replay to a prior genuinely-signed whole-home state* — rollback is **A23's**, not content-addressing's: layer 1 detects only rollback inconsistent relative to a surviving expected terminal; a coherent suffix regression is layer 2's at its gates (D32-2 — the seam stays inside this model so T1 is never read as complete). T1's at-rest-confidentiality half is P17/RF-15's — cross-referenced, not re-tiered (D32-1). **T2 — active same-privilege writer** across a publication's prepare→verify→rename window: no sequence of pathname checks wins; the honest answer is OS-enforced exclusion (W-4's topology). **T3 — the legitimate concurrent human edit**: not an attack; its correctness criterion is *attributed, never lost* (M8 between attested roots; A24/R14 during recovery downtime; the remaining in-publication window is SI-40, posture-bounded by P29). Out-of-scope boundary: root/privileged-host is out of scope (the trust boundary is the Unix account, P7); an ordinary different-uid process is *defended* by the enforced 0700/0600 home permissions, not out of scope. -- **Publication and read-back discipline (A27.1/A27.2, normative).** *Staged bytes (A27.1):* a commit consumes bytes verified in memory during prepare and never re-reads mutable storage to source the bytes it installs; re-reading live storage for comparison or enumeration is permitted — the invariant is on the provenance of installed bytes. Prepare is the single verification point; commit is a pure function of already-trusted bytes. *Verify-on-read-back (A27.2):* every read-back of storage bytes that will bear authority MUST content-address-verify (or signature-verify, which subsumes it — A19) before the bytes are consumed; a mismatch is a loud denial, never a fallback. Holds against T1, with the three carve-out classes above carrying their own rules. +- **Publication and read-back discipline (A27.1/A27.2, normative).** *Staged bytes (A27.1):* a commit consumes bytes verified in memory during prepare and never re-reads mutable storage to source the bytes it installs; re-reading live storage for comparison or enumeration is permitted — the invariant is on the provenance of installed bytes. Prepare is the single verification point; commit is a pure function of already-trusted bytes. Holds against **T1** — the write-boundary complement of A27.2's read-back rule (D32-5). *Verify-on-read-back (A27.2):* every read-back of storage bytes that will bear authority MUST content-address-verify (or signature-verify, which subsumes it — A19) before the bytes are consumed; a mismatch is a loud denial, never a fallback. Holds against T1, with the three carve-out classes above carrying their own rules. - **Per-kind entry rules (A27.3, normative).** *fs-tree stores:* the canonical grammar is exactly A24/R25's tagged path-state domain — `absent | file(content hash, executable mode) | implicit-directory`; any other kind (symlink, fifo, socket, device) is definitionally non-canonical. Capture rejects symlinks; recovery runs the R24 kind-complete scan; publication writes through an `O_EXCL|O_NOFOLLOW` randomized sibling with a same-inode non-symlink recheck immediately before rename — documented as *race-narrowing, never race-closing* (see A27.4); excluded directories are never written, deleted, or traversed ("excluded means untouched," uniform through every walk — RF-43 until `sync_store`'s walk complies). *SQLite stores:* the whole file is the unit; images are captured WAL-checkpointed, published through the same atomic primitive, stale `-wal`/`-shm` sidecars removed after the swap; **every** SQLite store path — registered stores included, not only `fabric.db` — is symlink-rejected before capture/open (RF-42 until enforced). *Hardlinks:* captured by content like any regular file; the write-through-a-link variant is defeated structurally by the atomic publisher (rename replaces the directory entry); the pre-plant-and-race variant is T2, COOP/W-4-bounded. - **Containment boundary (A27.4, normative).** Every T2 publication-safety claim carries the standing sentence: **"holds under COOP; requires W-4 containment at G-ADVERSARIAL."** No pathname check is ever claimed to defeat a same-uid active adversary, and the security argument for any race-narrowing step MUST name COOP as the assumption it rests on. T2 claims are publishable as labeled conditionals (D32-8; the posture ledger's G-PUBLISH gate entry makes the dependency reader-visible); a published T2 claim missing its label is a publication defect. @@ -452,4 +452,4 @@ A1 domain-scoped behavior pinning · A2 re-manifest on bundle change (M5) · A3 --- -*Status: v0.9, mid-dogfooding. v0.3's grammar survived three dissimilar paper workflows with amendments but no redesign; v0.4's amendments came from running code; A20 came from dogfooding, A21 from its readiness review, A22 from the revocation design review that separated key destruction from authority closure ahead of first egress, and A23 from the W-20 kernel-security-protocol pass — the first amendment ratifying the operational stratum beneath the schema (authenticated order for the substrate the invariants quantify over) rather than refining the schema itself. A24–A26 continue that pass with the first retro-ratifications: protocols an implementation designed inside a remediation cycle, challenged after the fact and adjusted across seven adversarial review rounds (round 7 scoped to the round-6 delta) plus an internal pre-round-6 review (five mechanism trackers filed — RF-35 recovery, RF-36 consumption-binding parity at both consumers, RF-37 re-merge equality, RF-39 exemption binding, RF-40 fs-pipeline entry identity — carried by W-15/W-22, plus RF-38 filing the anomaly-recovery question for the operator-surface pass; SI-39 filed for the multi-window recovery enhancement); rounds 4–5 notably surfaced as-built defects in merged dogfooding code, the strongest vindication of retro-ratifying under adversarial review — the ratify-first discipline holding even when code arrived first. Every wave still clusters on precision, not missing concepts; the compositional-grammar thesis is holding.* +*Status: v0.10, mid-dogfooding. v0.3's grammar survived three dissimilar paper workflows with amendments but no redesign; v0.4's amendments came from running code; A20 came from dogfooding, A21 from its readiness review, A22 from the revocation design review that separated key destruction from authority closure ahead of first egress, and A23 from the W-20 kernel-security-protocol pass — the first amendment ratifying the operational stratum beneath the schema (authenticated order for the substrate the invariants quantify over) rather than refining the schema itself. A24–A26 continue that pass with the first retro-ratifications: protocols an implementation designed inside a remediation cycle, challenged after the fact and adjusted across seven adversarial review rounds (round 7 scoped to the round-6 delta) plus an internal pre-round-6 review (five mechanism trackers filed — RF-35 recovery, RF-36 consumption-binding parity at both consumers, RF-37 re-merge equality, RF-39 exemption binding, RF-40 fs-pipeline entry identity — carried by W-15/W-22, plus RF-38 filing the anomaly-recovery question for the operator-surface pass; SI-39 filed for the multi-window recovery enhancement); rounds 4–5 notably surfaced as-built defects in merged dogfooding code, the strongest vindication of retro-ratifying under adversarial review — the ratify-first discipline holding even when code arrived first. A27 extends the same operational stratum sideways — the storage adversary beneath the publication discipline — and its ratification was the first full run of the codified battery with the stopping rule (one internal pre-review, one delta-scoped external round). Every wave still clusters on precision, not missing concepts; the compositional-grammar thesis is holding.* diff --git a/docs/roadmap.md b/docs/roadmap.md index 66e2258..2dcd92a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -164,7 +164,15 @@ pre-review before the external round: 14 findings (4 medium), all applied — recorded in ADR 0008's addendum. Per the review-battery stopping rule (adopted 2026-07-15, PR #52): one delta-scoped external round on the ratification text, continuation only -on blocking findings. **Batch close-out per the operator decision filed +on blocking findings. The delta round returned REQUEST CHANGES — five +findings, two blocking mediums with reviewer-prescribed remedies +(A27.1's dropped T1 label; the candidate body's layer-2 misclass of +caller-pinned heads, corrected in D32-2), one non-blocking medium (the +SI-40 substrate-assisted paragraph's snapshot-timing over-claim), two +lows — all folded same-day and recorded in ADR 0008's addendum; the +round verified all ten determinations integrated, the A24/R24/R25 +grammar preserved, and the RF/G14 source claims. Cycle closure under +the stopping rule is the operator's gate decision. **Batch close-out per the operator decision filed in PR #52:** items (4)–(6) defer to their named triggers — SI-26/28/29 to W-16 and the W-6 publication gate, SI-27 to W-17, the §0 convention riding with whichever fires first; W-20 closes when this ratification diff --git a/docs/spec-issues.md b/docs/spec-issues.md index 3e90a8b..41ec3be 100644 --- a/docs/spec-issues.md +++ b/docs/spec-issues.md @@ -88,23 +88,30 @@ reopen recovery's capture path owns them — what the capture artifact is (the R9 capture record generalizes), and the cost budget (a per-entry recheck on every apply pays a stat per file to defend a sub-second window). -A third remedy direction, noted by the operator at ratification: -**substrate-assisted preservation.** On a CoW filesystem (ZFS, btrfs, -APFS) an instantaneous snapshot taken at gate-lock acquisition — or a -clone-and-swap publication — makes the window loss unrepresentable at -the block layer: an in-window edit lands either before the snapshot -(preserved there) or after the swap (ordinary M8 drift), never in a -clobberable middle. Costs, stated so the comparison is honest when a -trigger fires: a platform dependency the fabric has so far refused (the -CAS is deliberately CoW-snapshots-in-userspace, portable anywhere); a -second snapshot mechanism outside the CAS attribution pipeline — -preserved bytes must still be captured *into* the CAS and -drift-attributed to satisfy T3's criterion, so the fs snapshot is the -preservation substrate, never the ledger entry; and per-platform -divergence exactly where A27.3 just unified per-kind semantics. Shape: -a deployment-floor option (the RF-15/P17 "OS/full-disk floor" class), -not the portable default — evaluate against the per-entry -capture-or-refuse candidate when a trigger fires. +A third remedy direction, noted by the operator at ratification and +corrected by the external delta round (finding 3): **substrate-assisted +preservation.** Stated precisely, because the intuitive version +over-claims: a CoW snapshot taken at gate-lock acquisition does NOT +close this window — the SI-40 edit is by definition made *after* that +snapshot (it lands post-prepare-check), so it is absent from the +snapshot and still overwritten by the rename; the snapshot preserves +exactly the state the prepare check already verified was not at risk. +What the block layer can genuinely buy is retention of the **outgoing +live state at swap time**: a clone-and-swap publication that retains +the outgoing dataset (or a per-file clone of each target immediately +before its rename), so divergent bytes survive the swap instead of +being unlinked. Even then the fs artifact is only the preservation +substrate: T3's criterion is attributed-never-lost, so the retained +outgoing state must still be diffed against the prepare image, ingested +into the CAS, and drift-attributed — retention, reconciliation, and +cleanup costs the per-entry capture-or-refuse candidate does not pay. +Plus the costs from the original note: a platform dependency the fabric +has so far refused (the CAS is deliberately CoW-snapshots-in-userspace, +portable anywhere) and per-platform divergence exactly where A27.3 just +unified per-kind semantics. Shape: a deployment-floor option (the +RF-15/P17 "OS/full-disk floor" class), not the portable default — +evaluate against the per-entry capture-or-refuse candidate when a +trigger fires. Bounded today by **P29**: under 1HUMAN/1SESS the colliding writer is the same person who initiated the transition, the Tier-1 apply window is @@ -515,8 +522,8 @@ containment before G-PUBLISH. the three-tier model — T1 offline tampering (answered by content-address verification on every authority-bearing read-back), T2 active same-uid writer (no pathname check wins; the honest answer is W-4 containment, so -every T2 claim is labeled "holds under COOP; requires W-4 at -G-ADVERSARIAL"), T3 legitimate human edit (not an attack; M8 attribution +every T2 claim is labeled "holds under COOP; requires W-4 containment +at G-ADVERSARIAL"), T3 legitimate human edit (not an attack; M8 attribution and A24/R14 recovery-window capture). Candidate normative rules (candidate amendment A27): A27.1 the staged-bytes rule (ratifies RF-20's discipline); A27.2 verify-on-read-back, tier-labeled, with three classes From a0e61cf7f63aa11dd84aa811dde8fc54db63e3a2 Mon Sep 17 00:00:00 2001 From: XVVH Date: Wed, 15 Jul 2026 14:42:49 -0400 Subject: [PATCH 5/6] Record the operator gate decision: confirming delta round on the fold commit Co-Authored-By: Claude Fable 5 --- docs/roadmap.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 2dcd92a..332f2c5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -171,8 +171,10 @@ caller-pinned heads, corrected in D32-2), one non-blocking medium (the SI-40 substrate-assisted paragraph's snapshot-timing over-claim), two lows — all folded same-day and recorded in ADR 0008's addendum; the round verified all ten determinations integrated, the A24/R24/R25 -grammar preserved, and the RF/G14 source claims. Cycle closure under -the stopping rule is the operator's gate decision. **Batch close-out per the operator decision filed +grammar preserved, and the RF/G14 source claims. Operator gate decision +2026-07-15: a confirming delta round, scoped to the fold commit +(`eff73b2`), within the stopping rule's blocking-round continuation; no +blocking finding there closes the cycle. **Batch close-out per the operator decision filed in PR #52:** items (4)–(6) defer to their named triggers — SI-26/28/29 to W-16 and the W-6 publication gate, SI-27 to W-17, the §0 convention riding with whichever fires first; W-20 closes when this ratification From 48f17cf2106aa1aa277301323910be4ce55f5d5a Mon Sep 17 00:00:00 2001 From: XVVH Date: Wed, 15 Jul 2026 14:54:03 -0400 Subject: [PATCH 6/6] Close the review cycle: confirming round clean, two non-blocking findings folded No blocking findings from the confirming delta round - the cycle closes under the stopping rule. Folded: SI-40's per-file clone variant now requires atomic clone-and-swap semantics (separate clone->rename re-opens the window in miniature; dataset-level retain-and-swap is the sound variant), and the roadmap's branch-local SHA citation replaced per the citation rule. Cycle records in ADR 0008's addendum and the roadmap: one internal pre-review (14 findings) + two external delta rounds (5 + 2), against PR #48's seven rounds. Co-Authored-By: Claude Fable 5 --- docs/adr/0008-filesystem-attacker-model.md | 12 ++++++++++++ docs/roadmap.md | 14 +++++++++++--- docs/spec-issues.md | 11 ++++++++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/docs/adr/0008-filesystem-attacker-model.md b/docs/adr/0008-filesystem-attacker-model.md index 66764d5..7da8257 100644 --- a/docs/adr/0008-filesystem-attacker-model.md +++ b/docs/adr/0008-filesystem-attacker-model.md @@ -545,3 +545,15 @@ sentence; two v0.9 version stamps bumped). The round verified: all ten determinations have integration sites, A27.3 preserves the A24/R24/R25 grammar by exact reference, SI-40's corrected refusal disposition is sound, and RF-41/RF-42/RF-43 and G14's source/test claims check out. + +**Confirming delta round (2026-07-15) — no blocking findings; the +cycle closes under the stopping rule.** Remedies 1, 2, 4, and 5 +verified correct; the external-round record and the v0.10 provenance +clause verified accurate. Two non-blocking findings folded under the +battery without a further round: SI-40's per-file clone variant now +requires atomic clone-and-swap semantics (a separate clone→rename pair +re-opens the window in miniature; the dataset-level retain-and-swap +variant is sound as stated), and a branch-local SHA citation in the +roadmap was replaced per the citation rule. Complete cycle cost for +this ratification: one internal adversarial pre-review (14 findings) +plus two delta-scoped external rounds (5 + 2 findings). diff --git a/docs/roadmap.md b/docs/roadmap.md index 332f2c5..7830053 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -172,9 +172,17 @@ SI-40 substrate-assisted paragraph's snapshot-timing over-claim), two lows — all folded same-day and recorded in ADR 0008's addendum; the round verified all ten determinations integrated, the A24/R24/R25 grammar preserved, and the RF/G14 source claims. Operator gate decision -2026-07-15: a confirming delta round, scoped to the fold commit -(`eff73b2`), within the stopping rule's blocking-round continuation; no -blocking finding there closes the cycle. **Batch close-out per the operator decision filed +2026-07-15: a confirming delta round, scoped to PR #50's five-finding +fold delta, within the stopping rule's blocking-round continuation. +**The confirming round returned no blocking findings — the cycle is +closed (2026-07-15).** Its two non-blocking findings were folded under +the battery: the per-file clone variant in SI-40's candidate text now +requires atomic clone-and-swap semantics (a separate clone→rename pair +re-opens the window in miniature), and this entry's own branch-local +SHA citation was replaced per the citation rule. Cycle cost, complete: +one internal adversarial pre-review (14 findings) + two external delta +rounds (5 + 2 findings) — against PR #48's seven. W-20 closes when this +PR merges, per the close-out decision filed in PR #52. **Batch close-out per the operator decision filed in PR #52:** items (4)–(6) defer to their named triggers — SI-26/28/29 to W-16 and the W-6 publication gate, SI-27 to W-17, the §0 convention riding with whichever fires first; W-20 closes when this ratification diff --git a/docs/spec-issues.md b/docs/spec-issues.md index 41ec3be..71da8c6 100644 --- a/docs/spec-issues.md +++ b/docs/spec-issues.md @@ -98,9 +98,14 @@ snapshot and still overwritten by the rename; the snapshot preserves exactly the state the prepare check already verified was not at risk. What the block layer can genuinely buy is retention of the **outgoing live state at swap time**: a clone-and-swap publication that retains -the outgoing dataset (or a per-file clone of each target immediately -before its rename), so divergent bytes survive the swap instead of -being unlinked. Even then the fs artifact is only the preservation +the outgoing dataset — the sound variant, since the dataset swap is one +atomic point — or a per-file clone of each target immediately before +its rename **only under atomic clone-and-swap semantics**: a separate +clone followed by a separate rename re-opens the window in miniature +(an edit landing between them is absent from the clone and still +unlinked), so absent atomicity the per-file variant is race-narrowing, +not window-closing (the confirming review's finding). Either way +divergent bytes survive the swap instead of being unlinked. Even then the fs artifact is only the preservation substrate: T3's criterion is attributed-never-lost, so the retained outgoing state must still be diffed against the prepare image, ingested into the CAS, and drift-attributed — retention, reconciliation, and