From d278e459c4b1e34af13422a626225139351e39fa Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:25:37 +0200 Subject: [PATCH 1/2] feat(core): close R-15 Gate 3 with its evidence matrix and residual owners (#445) Gate 3's definition (file sync, atomic replacement, directory sync, generation reconciliation, fault injection) is met headless for ordinary records; contract 20 records the evidence per requirement and assigns every residual: asset-pair marker body to Gate 5, the key-epoch crash window and reclamation to Gate 4, power loss to Gate 6, deletion transitions to #948, and #357's legacy TypeScript path to Gate 7. The protected path now refuses a non-ordinary record before writing. R15_GATE3=IMPLEMENTED_HEADLESS. --- CHANGELOG.md | 6 ++++ .../src/protected.rs | 17 ++++++++++ .../tests/gate3c_protected_test.rs | 13 +++++++ docs/native/CORE-MIGRATION-LEDGER.md | 2 +- docs/native/R15-SECURE-STORAGE-CONTRACT.md | 34 ++++++++++++++++--- 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03174f93e..0af8b64cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- **R-15 Gate 3 complete — crash-durable protected storage core (#445):** the protected-storage + core now covers everything Gate 3 requires — synced files, never-overwritten generations, synced + directories, startup recovery and fault-injection tests on Linux, macOS and Windows — for ordinary + records, and refuses any other record type before writing. What remains is assigned to later gates + (asset pairs, power-loss qualification, record deletion, and switching the app over). Nothing + reads or writes user data through it yet. PR #949. - **R-15 Gate 3, slice 3C part 3c-2b — protected writes through the root of trust (#445):** a protected write now commits each step — the intent to write and the finished write — through a new root of trust, and is reported as durably committed only after the second one. Reads serve diff --git a/crates/worldscript-secure-storage/src/protected.rs b/crates/worldscript-secure-storage/src/protected.rs index 2a0ee4cc6..ac60bc8f6 100644 --- a/crates/worldscript-secure-storage/src/protected.rs +++ b/crates/worldscript-secure-storage/src/protected.rs @@ -33,6 +33,7 @@ use crate::commit::{ CommitError, RecordStore, Resolution, WriteRequest, }; use crate::durable::{DirectoryDurability, DurableFs, WriteOperationId}; +use crate::identity::has_ordinary_marker; use crate::provider::{KeyProvider, RootKeyRefV1}; use crate::record::OpenedRecord; use crate::root_store::{RootCommitted, RootLayout}; @@ -68,6 +69,10 @@ pub enum ProtectedError { /// authority. In the protected path an uncatalogued record's chain can only be a rolled-back /// first write, so a chain no committed root ever named is never published. UnrootedChain, + /// Not an ordinary record: control-plane and retained-authority classes have no record-commit + /// marker, and an asset-pair member (`asset`, `asset-metadata`) is committed only through its + /// `asset-pair` marker (§8.4.1), which is Gate 5 — refused before anything is written. + NotAnOrdinaryRecord, } impl From for ProtectedError { @@ -118,6 +123,7 @@ pub fn protected_write( target: ProtectedTarget<'_>, write: ProtectedWrite<'_>, ) -> Result { + ensure_ordinary(target.store)?; let mut durability = reconcile_protected(fs, provider, target)?.durability; let request = WriteRequest { key_epoch: target.key_epoch, @@ -161,6 +167,7 @@ pub fn reconcile_protected( provider: &mut P, target: ProtectedTarget<'_>, ) -> Result { + ensure_ordinary(target.store)?; let catalog = load_catalog(fs, provider, target.layout)?; let named = named_descriptor(catalog.as_ref(), target.store); if let Some(named) = named { @@ -196,6 +203,7 @@ pub fn read_protected( layout: RootLayout<'_>, store: RecordStore<'_>, ) -> Result { + ensure_ordinary(store)?; let catalog = load_catalog(fs, provider, layout)?; let Some(named) = named_descriptor(catalog.as_ref(), store) else { return Ok(ProtectedRead::NotCatalogued); @@ -261,6 +269,15 @@ fn commit_chain_state( )?)) } +/// Only an ordinary record (§10.4.1 `MIGRATE_TO_R15`, not an asset-pair member) takes this path. +fn ensure_ordinary(store: RecordStore<'_>) -> Result<(), ProtectedError> { + if has_ordinary_marker(store.record.class()) { + Ok(()) + } else { + Err(ProtectedError::NotAnOrdinaryRecord) + } +} + /// An uncatalogued record that is not a rolled-back first write must have no authority at all: a /// chain no committed root ever named is never published. fn refuse_unrooted_chain( diff --git a/crates/worldscript-secure-storage/tests/gate3c_protected_test.rs b/crates/worldscript-secure-storage/tests/gate3c_protected_test.rs index b5e628f60..d17a292ee 100644 --- a/crates/worldscript-secure-storage/tests/gate3c_protected_test.rs +++ b/crates/worldscript-secure-storage/tests/gate3c_protected_test.rs @@ -452,3 +452,16 @@ fn an_ahead_generation_that_does_not_verify_is_not_published() { assert_eq!(fixture.listed_marker_states(), vec![(2, ACTIVE)]); assert_eq!(fixture.payload().as_deref(), Some(&b"first"[..])); } + +#[test] +fn an_asset_pair_member_is_refused_before_anything_is_written() { + let mut fixture = Fixture::new(); + fixture.record = RecordIdentity::new(RecordClass::Asset, &["p1", "a1"]).unwrap(); + assert_eq!( + fixture.write_with(&mut StdFs, b"bytes"), + Err(ProtectedError::NotAnOrdinaryRecord) + ); + assert_eq!(fixture.read(), Err(ProtectedError::NotAnOrdinaryRecord)); + assert_eq!(fs::read_dir(fixture.marker_dir()).unwrap().count(), 0); + assert!(!fixture.root_dir().join("catalog").exists()); +} diff --git a/docs/native/CORE-MIGRATION-LEDGER.md b/docs/native/CORE-MIGRATION-LEDGER.md index f851c53c9..9038a87ab 100644 --- a/docs/native/CORE-MIGRATION-LEDGER.md +++ b/docs/native/CORE-MIGRATION-LEDGER.md @@ -17,7 +17,7 @@ scope shifts — it is a living decision record, not a one-time snapshot. | 7 | `features/project/` domain logic | TS, `features/project/` (24 files, 2,114 lines) — real logic concentrated in `thunks/` + `projectSelectors.ts` (~450-500 lines); `reducers/` (11 files) is CRUD bookkeeping | High — Redux-store-shape/dispatch bound; `reducers/` stays TS-side permanently | Low | Medium (import/restore orchestration) | Low-medium | Medium (only the thunks/selectors subset) | Deferred | Candidate after the schema crate is proven; only thunks/selectors, never `reducers/` | Not started | | 8 | AI services | TS, `services/ai/` (44 files, 5,401 lines), mixed portability (retry/routing/error-taxonomy renderer-neutral vs. `computeShaderFactory.ts`/`webGpuDetectorService.ts`/`.wgsl` inherently WebGPU-coupled) | Mixed | Medium-high (API keys) | Low-medium | Medium | Uncertain — too large/mixed to assess narrowly | **Out of scope for all of Wave 2** | None proposed | None | | 9 | Project state-shape compatibility adapter | TS, `features/project/coreBoundaryAdapter.ts` at the Core boundary + Rust, `crates/worldscript-project` schema | High at the boundary — production Redux `EntityState` must be translated without importing Redux into Core | Low | High — ID/order preservation is part of project identity | Medium | High — every native renderer needs the same conversion contract | **2 — Wave 2 prerequisite before G1 evaluation** | **Current-production #553 closure complete; Rust Core authority switch not started (#836).** `IMPLEMENTATION_STARTED = YES`. Every current-production Project path is canonical and no-loss according to backend semantics (#553, PRs #773–#849): textual raw carrier and lexical tokens on the filesystem, structured-value semantics in IndexedDB. Persistence and admission: shared TS/Rust classification including the raw-token grammar; `LEGACY_TO_V1` admitted in memory and migrated durably on both backends (IndexedDB authority; filesystem under the project lock with a pre-migration snapshot, #849); the generation-fenced canonical IDB authority for web/PWA autosave, flush and manual save; the desktop filesystem writer as a preserve-first raw-carrier writeback under the project lock with a generation/incarnation fence. Egress: export and library backup (projects and snapshots), stripped to portable form. Snapshots: creation and restore, each admitted, with an exact restore carrier. Import: every modeled field admitted and projected, then an admitted-raw first save (#842, #848); a `null` tension score is admitted so the project remains loadable, omitted from the typed editor projection, and preserved in the canonical raw carrier. Export: every JSON surface, including Advanced import/export, through the canonical egress (#847). Replacement and authority: same-ID replacement writes a fresh canonical document, with carriers bound to target, epoch and authority; the desktop fails closed when filesystem storage is unavailable, with no IndexedDB fallback; an unloadable browser record is refused and kept. The closure guarantees no silent loss or replacement of stored data; it does not promise that every malformed shape boots into the editor (malformed manuscript-section hardening is #845). TypeScript remains the production Project authority; the renderer-neutral Rust Core authority switch is separate future work (#836). Normalizes array or Redux `EntityState` to renderer-neutral arrays and reconstructs the TS-side shape only at the integration boundary. The Rust verdict remains partial because unknown fields are not rejected (Rust is observation-only until #836); within the current TypeScript authority, every current-production ingress, writer, migration and egress preserves the authoritative canonical carrier according to backend semantics — the textual raw carrier and its lexical tokens where textual authority exists (filesystem), the stored structured value in IndexedDB (canonically serialized where text is needed, with no claim that original JSON text survives). Both required decisions (persisted version authority; a field-class-staged unknown-field policy, not one global policy) are resolved and maintainer-admitted in [`docs/native/PROJECT-CORE-COMPATIBILITY-CONTRACT.md`](PROJECT-CORE-COMPATIBILITY-CONTRACT.md), `PROPOSED = YES` / `ADMITTED = YES`. Issue #553's current-production implementation of that contract is complete; its terminal acceptance is QNB-99 (pending); the authority switch it gates is #836. | `tests/unit/features/project/coreBoundaryAdapter.test.ts` covers array and `EntityState` inputs, round-trip ID/order preservation, and rejection of duplicate IDs, missing references, and orphaned entities for both characters and worlds; `tests/unit/features/project/projectSchemaVersion.test.ts` and `crates/worldscript-project/tests/version_test.rs` cover classification/parity; the IDB load observation is covered by `tests/unit/services/storage/idbProjectStoreLoadStateObservation.test.ts`; the canonical parser/import/admission foundation is covered by `tests/unit/projectDocument.test.ts` and `tests/unit/projectImportSchema.test.ts`; the writeback overlay/verify/fence primitive by `tests/unit/services/projectDocumentWriteback.test.ts`; the IDB canonical admission/durable-commit boundary (against real fake-indexeddb, including a generation-conflict rejection, a §2.7 downgrade-contradiction, and an encrypted round trip) by `tests/unit/services/storage/idbProjectCanonicalAuthority.test.ts`; production routing/refusal-success coverage by `tests/unit/services/projectAutosavePersistence.test.ts`, `tests/unit/persistedStateFlush.test.ts`, and the listener/shortcut tests; the envelope fixture is accepted by Rust after migration and validation; the #553 current-production closure (a1–a11) by `tests/unit/services/projectCanonicalEgress.test.ts`, `tests/unit/libraryBackupService.test.ts`, `tests/unit/services/fs/fsStores.test.ts`, `tests/unit/services/projectAutosaveCanonicalWriter.test.ts`, `tests/unit/services/projectImportCarrier.test.ts`, `tests/unit/storageServiceDesktopAuthority.test.ts` and `tests/unit/malformedProjectBoot.test.ts` | -| 10 | R-15 protected desktop storage contract | **Contract `docs/native/R15-SECURE-STORAGE-CONTRACT.md` + headless Rust implementation (Gates 1a/1b/2 and Gate 3 slices 3A, 3B and 3C parts 1–3c-2b) in `crates/worldscript-secure-storage`, not production authority**; current desktop records remain TS/Tauri filesystem authority | High — future Core must serve Tauri and Qt without renderer-private crypto semantics | High | High — durability, migration, and identity binding protect user data | High | **Highest — cross-renderer security/durability contract** | **3 — S5-A, S5-B1, S5-B2, and S5-B3 all admitted; final cross-contract audit complete, S5_TERMINAL=YES (PR #584 merged `c24aa645`, post-merge CI/CD + CodeQL green); Gate 1a re-admitted by QNB-100 (2026-09-26) and implemented headless in `crates/worldscript-secure-storage`; Gate 1b decided 2026-09-26 (Option C: platform secure store primary, optional `WSS_ARGON2ID_V1` passphrase recovery); 1b-core landed (#850); 1b-platform delivered as small sequential slices — §8.2.2 item layout (#854), durable authority (#855), runtime key handles (#914), anchor transitions + `KeyProvider` (#915), OS secure-store adapter (§8.2.5, #916; evidence Linux `CI_ONLY`/`LOCAL_ONLY`, macOS/Windows `CI_ONLY`, no packaged evidence); Gate 2 (typed identity registry, identity-bound record codec and §10.4.1 record-class disposition) admitted and implemented headless (§20, #920); legacy source-locator mapping belongs to Gate 5; #361's shipped-helper gap closes only with Gate 7; Gate 3 slice 3A (durable staging and promotion, §9 steps 3–8) and slice 3B (the `record-commit` marker codec, §5.4, and the marker commit protocol with startup reconciliation, §9/§9.2) and slice 3C parts 1–3c-2b (the authority-root digests, §5.4, the record-catalog descriptors and pages, §5.5/§5.5.1, the root slot, pointer and key-epoch record encodings, §5.3.4, and the two-phase root commit with crash recovery and the trusted cold start including the key-epoch set check, §5.3.1, the persisted, root-verified record catalog with `list_records`, §5.5/§5.5.1, and the protected write and read paths committing every marker transition through the root, §9) implemented headless, with Gate 3 closure (#357 reconciliation and the asset-pair marker boundary) remaining (#921); the rest of Gate 3 and Gates 4–7 (including Gate 4 cross-process serialization) not admitted** | **S5_A_ADMITTED=YES / S5_B1_ADMITTED=YES / S5_B2_ADMITTED=YES / S5_B3_ADMITTED=YES / S5_IMPLEMENTATION_READY=NO / S5_TERMINAL=YES / R15_GATE1A=IMPLEMENTED_HEADLESS / R15_GATE1B=IMPLEMENTED_HEADLESS_AND_PLATFORM_ADAPTER / R15_GATE2=IMPLEMENTED_HEADLESS / R15_GATE3=SLICE_3C_PROTECTED_WRITE / PRODUCTION_AUTHORITY_SWITCH_ALLOWED=NO**; inventory, identity/AAD envelope, key epochs, fail-closed reads, durable replacement, crash-resumable migration, unified admission, race-free `AuthoritySnapshot` acquisition/lifetime (`docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md`), canonical migration source/payload evidence (`docs/native/r15/MIGRATION-SOURCE-EVIDENCE.md`), and the chunked large-object envelope (`docs/native/r15/CHUNKED-LARGE-OBJECT-ENVELOPE.md`) are all specified. No production authority switch or plaintext migration is claimed. | Final S5 cross-contract consistency audit (mutual reference integrity across all four documents) is complete — two mechanical citation-drift notes (a stale disposition-count note in §10.4.1, and S5-B3's mis-citation of S5-B1's migration-time mechanism for its own ordinary-write staging debris) and three substantive gaps were corrected: S5-B3's chunk-locator carried no operation/generation identity, so recovery could not distinguish a superseded attempt's orphaned chunk from the current one; §10.4.1's atomic-write-temporary-files carve-out contradicted its own "exactly one of three groups" exhaustiveness claim; and fixing that carve-out into an explicit `REFUSE_AUTHORITY_SWITCH` group in turn made Gate 7's class-level rule permanently unsatisfiable for that one class, fixed by making Gate 7 instance-aware. `S5_TERMINAL` is YES: PR #584 merged and its post-merge main CI (incl. CodeQL) was green. Gate 1a's headless vectors (contract header fixture, fixed-key AEAD for absent/present `project_id`, rule-D boundary, malformed-input and substitution tests, cross-checked against an independent implementation) now exist; the Gate 1b platform secure-store adapter exists with CI/local per-platform evidence (#916, contract §8.2.5); packaged secure-store evidence, Gate 4 cross-process serialization, per-record migration tests, packaged durability evidence, and explicit #357/#359/#360/#361 reconciliation are still required before the later implementation gates can close | +| 10 | R-15 protected desktop storage contract | **Contract `docs/native/R15-SECURE-STORAGE-CONTRACT.md` + headless Rust implementation (Gates 1a/1b/2 and Gate 3 slices 3A, 3B and 3C parts 1–3c-2b) in `crates/worldscript-secure-storage`, not production authority**; current desktop records remain TS/Tauri filesystem authority | High — future Core must serve Tauri and Qt without renderer-private crypto semantics | High | High — durability, migration, and identity binding protect user data | High | **Highest — cross-renderer security/durability contract** | **3 — S5-A, S5-B1, S5-B2, and S5-B3 all admitted; final cross-contract audit complete, S5_TERMINAL=YES (PR #584 merged `c24aa645`, post-merge CI/CD + CodeQL green); Gate 1a re-admitted by QNB-100 (2026-09-26) and implemented headless in `crates/worldscript-secure-storage`; Gate 1b decided 2026-09-26 (Option C: platform secure store primary, optional `WSS_ARGON2ID_V1` passphrase recovery); 1b-core landed (#850); 1b-platform delivered as small sequential slices — §8.2.2 item layout (#854), durable authority (#855), runtime key handles (#914), anchor transitions + `KeyProvider` (#915), OS secure-store adapter (§8.2.5, #916; evidence Linux `CI_ONLY`/`LOCAL_ONLY`, macOS/Windows `CI_ONLY`, no packaged evidence); Gate 2 (typed identity registry, identity-bound record codec and §10.4.1 record-class disposition) admitted and implemented headless (§20, #920); legacy source-locator mapping belongs to Gate 5; #361's shipped-helper gap closes only with Gate 7; Gate 3 slice 3A (durable staging and promotion, §9 steps 3–8) and slice 3B (the `record-commit` marker codec, §5.4, and the marker commit protocol with startup reconciliation, §9/§9.2) and slice 3C parts 1–3c-2b (the authority-root digests, §5.4, the record-catalog descriptors and pages, §5.5/§5.5.1, the root slot, pointer and key-epoch record encodings, §5.3.4, and the two-phase root commit with crash recovery and the trusted cold start including the key-epoch set check, §5.3.1, the persisted, root-verified record catalog with `list_records`, §5.5/§5.5.1, and the protected write and read paths committing every marker transition through the root, §9) implemented headless, so Gate 3 is implemented headless (#921; closure evidence and residual owners — asset-pair → Gate 5, key-epoch crash window and reclamation → Gate 4, power loss → Gate 6, deletion → #948, #357's legacy path → Gate 7 — in contract §20); Gates 4–7 (including Gate 4 cross-process serialization) not admitted** | **S5_A_ADMITTED=YES / S5_B1_ADMITTED=YES / S5_B2_ADMITTED=YES / S5_B3_ADMITTED=YES / S5_IMPLEMENTATION_READY=NO / S5_TERMINAL=YES / R15_GATE1A=IMPLEMENTED_HEADLESS / R15_GATE1B=IMPLEMENTED_HEADLESS_AND_PLATFORM_ADAPTER / R15_GATE2=IMPLEMENTED_HEADLESS / R15_GATE3=IMPLEMENTED_HEADLESS / PRODUCTION_AUTHORITY_SWITCH_ALLOWED=NO**; inventory, identity/AAD envelope, key epochs, fail-closed reads, durable replacement, crash-resumable migration, unified admission, race-free `AuthoritySnapshot` acquisition/lifetime (`docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md`), canonical migration source/payload evidence (`docs/native/r15/MIGRATION-SOURCE-EVIDENCE.md`), and the chunked large-object envelope (`docs/native/r15/CHUNKED-LARGE-OBJECT-ENVELOPE.md`) are all specified. No production authority switch or plaintext migration is claimed. | Final S5 cross-contract consistency audit (mutual reference integrity across all four documents) is complete — two mechanical citation-drift notes (a stale disposition-count note in §10.4.1, and S5-B3's mis-citation of S5-B1's migration-time mechanism for its own ordinary-write staging debris) and three substantive gaps were corrected: S5-B3's chunk-locator carried no operation/generation identity, so recovery could not distinguish a superseded attempt's orphaned chunk from the current one; §10.4.1's atomic-write-temporary-files carve-out contradicted its own "exactly one of three groups" exhaustiveness claim; and fixing that carve-out into an explicit `REFUSE_AUTHORITY_SWITCH` group in turn made Gate 7's class-level rule permanently unsatisfiable for that one class, fixed by making Gate 7 instance-aware. `S5_TERMINAL` is YES: PR #584 merged and its post-merge main CI (incl. CodeQL) was green. Gate 1a's headless vectors (contract header fixture, fixed-key AEAD for absent/present `project_id`, rule-D boundary, malformed-input and substitution tests, cross-checked against an independent implementation) now exist; the Gate 1b platform secure-store adapter exists with CI/local per-platform evidence (#916, contract §8.2.5); packaged secure-store evidence, Gate 4 cross-process serialization, per-record migration tests, packaged durability evidence, and explicit #357/#359/#360/#361 reconciliation are still required before the later implementation gates can close | ## Decisions this table records diff --git a/docs/native/R15-SECURE-STORAGE-CONTRACT.md b/docs/native/R15-SECURE-STORAGE-CONTRACT.md index aff724f2e..a6d7065ec 100644 --- a/docs/native/R15-SECURE-STORAGE-CONTRACT.md +++ b/docs/native/R15-SECURE-STORAGE-CONTRACT.md @@ -4,7 +4,7 @@ **Status:** S5-A — admitted R-15 secure-storage architecture baseline; production implementation not started. `S5_A_ADMITTED = YES`, `S5_IMPLEMENTATION_READY = NO`, `S5_TERMINAL = YES` (PR #584 merged as `c24aa645`; its post-merge main CI — CI Success and CodeQL — completed green, 19 success / 3 skipped), -`PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO`. `S5_B2_ADMITTED = YES` (`docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md` — race-free `AuthoritySnapshot` acquisition/lifetime/reclamation, §5.3.3). `S5_B1_ADMITTED = YES` (`docs/native/r15/MIGRATION-SOURCE-EVIDENCE.md` — canonical JSON encoding, packaged-IDB source evidence, per-class `canonical_destination_payload_bytes`/`source_value_digest`, atomic-write-temporary reconciliation, and identity-upgrade/recovery for unbound sources and legacy quarantine, §10.1.2, §10.1.3, §10.4.1). `S5_B3_ADMITTED = YES` (`docs/native/r15/CHUNKED-LARGE-OBJECT-ENVELOPE.md` — per-chunk-authenticated envelope and `chunk_set_digest` for records above the `64 MiB` whole-record limit, §6.1.2, §6.3, §13). All three S5 child contracts are admitted, and the final cross-contract consistency audit (S5-A/S5-B1/S5-B2/S5-B3 mutual reference integrity) is complete: five findings were made and corrected in this same change — two mechanical citation-drift notes (a stale "blocked pending S5-B1" disposition-count note in §10.4.1, and S5-B3's §6 crash-recovery paragraph citing S5-B1's migration-time reconciliation mechanism for an ordinary write's own orphaned staging chunk, where §9.2/§9 step 11's own ordinary-write staging-reconciliation rule actually applies) and three substantive gaps (S5-B3's chunk physical locator, §2, carried no operation/generation identity, so recovery could not distinguish a superseded attempt's orphaned chunk from the current attempt's — closed by giving each chunk's staging form the same `operation_id`/`target_generation` temp suffix §9 step 3 already defines for a whole record, and by making §6's recovery text check that exact suffix rather than the bare promoted-form locator; and §10.4.1's atomic-write-temporary-files carve-out was declared exempt from "exactly one of the three groups," directly contradicting that same exhaustiveness invariant — closed by making it an explicit fourth `REFUSE_AUTHORITY_SWITCH` group; that fix in turn made Gate 7's flat class-level rule ("blocked while any class is `REFUSE_AUTHORITY_SWITCH`") permanently unsatisfiable for this one class, since its class-level registry entry never changes even once every instance resolves — closed by making Gate 7's rule instance-aware, so only an *unresolved* `REFUSE_AUTHORITY_SWITCH` instance blocks it). No further inconsistency was found after these corrections. `S5_TERMINAL` and `S5_TERMINAL_R15_DESIGN_ADMITTED_MERGED_POSTMERGE_GREEN` are now YES, recorded in a dedicated follow-up after PR #584 merged and its post-merge main CI (including CodeQL) was confirmed green. **Gate 1a (§20) was re-admitted by the QNB-100 readiness verdict on 2026-09-26** (recorded on [#445](https://github.com/qnbs/WorldScript-Studio/issues/445#issuecomment-5847938516); no earlier Gate 1 admission is recorded). Gate 1a is the headless `WSR1` header and strict parser, the record-class registry, canonical AAD (§6.2), AES-256-GCM seal/open with a fail-closed OS nonce source, and the fixed-key/boundary/adversarial vectors §6.1 requires, in `crates/worldscript-secure-storage`. Gate 1b (the key-provider/KDF profile, §8.2/§8.2.1) was decided on 2026-09-26 as Option C; 1b-core landed in #850 and 1b-platform landed as small sequential slices (#854, #855, #914, #915, #916), ending with the OS secure-store adapter (§8.2.5). Status split: S5 design terminal/admitted = YES; Gate 1a = re-admitted and implemented headless; `S5_IMPLEMENTATION_READY = NO` for the R-15 program as a whole; Gate 1b = decided and implemented headless with the platform secure-store adapter (1b-core #850; 1b-platform #854, #855, #914, #915, #916); Gate 2 (typed identity registry, identity-bound record codec and record-class disposition, §20) = admitted and implemented headless; Gate 3 slice 3A (durable staging and promotion) = admitted and implemented headless; Gate 3 slice 3B (the `record-commit` marker codec, §5.4, and the marker commit protocol with startup reconciliation, §9/§9.2) = admitted and implemented headless; Gate 3 slice 3C parts 1, 2, 3a, 3b, 3c-1, 3c-2a and 3c-2b (the authority-root digests, §5.4, the record-catalog descriptors and pages, §5.5/§5.5.1, the root slot, pointer and key-epoch record encodings, §5.3.4, the two-phase root commit with crash recovery and the trusted cold start including the key-epoch set check, §5.3.1, the persisted, root-verified record catalog with `list_records`, §5.5/§5.5.1, and the protected write and read paths committing every marker transition through the root, §9) = admitted and implemented headless; the rest of Gate 3 and Gates 4–7 = not admitted; `PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO`. No current TypeScript/Tauri path reads or writes user data through this crate. +`PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO`. `S5_B2_ADMITTED = YES` (`docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md` — race-free `AuthoritySnapshot` acquisition/lifetime/reclamation, §5.3.3). `S5_B1_ADMITTED = YES` (`docs/native/r15/MIGRATION-SOURCE-EVIDENCE.md` — canonical JSON encoding, packaged-IDB source evidence, per-class `canonical_destination_payload_bytes`/`source_value_digest`, atomic-write-temporary reconciliation, and identity-upgrade/recovery for unbound sources and legacy quarantine, §10.1.2, §10.1.3, §10.4.1). `S5_B3_ADMITTED = YES` (`docs/native/r15/CHUNKED-LARGE-OBJECT-ENVELOPE.md` — per-chunk-authenticated envelope and `chunk_set_digest` for records above the `64 MiB` whole-record limit, §6.1.2, §6.3, §13). All three S5 child contracts are admitted, and the final cross-contract consistency audit (S5-A/S5-B1/S5-B2/S5-B3 mutual reference integrity) is complete: five findings were made and corrected in this same change — two mechanical citation-drift notes (a stale "blocked pending S5-B1" disposition-count note in §10.4.1, and S5-B3's §6 crash-recovery paragraph citing S5-B1's migration-time reconciliation mechanism for an ordinary write's own orphaned staging chunk, where §9.2/§9 step 11's own ordinary-write staging-reconciliation rule actually applies) and three substantive gaps (S5-B3's chunk physical locator, §2, carried no operation/generation identity, so recovery could not distinguish a superseded attempt's orphaned chunk from the current attempt's — closed by giving each chunk's staging form the same `operation_id`/`target_generation` temp suffix §9 step 3 already defines for a whole record, and by making §6's recovery text check that exact suffix rather than the bare promoted-form locator; and §10.4.1's atomic-write-temporary-files carve-out was declared exempt from "exactly one of the three groups," directly contradicting that same exhaustiveness invariant — closed by making it an explicit fourth `REFUSE_AUTHORITY_SWITCH` group; that fix in turn made Gate 7's flat class-level rule ("blocked while any class is `REFUSE_AUTHORITY_SWITCH`") permanently unsatisfiable for this one class, since its class-level registry entry never changes even once every instance resolves — closed by making Gate 7's rule instance-aware, so only an *unresolved* `REFUSE_AUTHORITY_SWITCH` instance blocks it). No further inconsistency was found after these corrections. `S5_TERMINAL` and `S5_TERMINAL_R15_DESIGN_ADMITTED_MERGED_POSTMERGE_GREEN` are now YES, recorded in a dedicated follow-up after PR #584 merged and its post-merge main CI (including CodeQL) was confirmed green. **Gate 1a (§20) was re-admitted by the QNB-100 readiness verdict on 2026-09-26** (recorded on [#445](https://github.com/qnbs/WorldScript-Studio/issues/445#issuecomment-5847938516); no earlier Gate 1 admission is recorded). Gate 1a is the headless `WSR1` header and strict parser, the record-class registry, canonical AAD (§6.2), AES-256-GCM seal/open with a fail-closed OS nonce source, and the fixed-key/boundary/adversarial vectors §6.1 requires, in `crates/worldscript-secure-storage`. Gate 1b (the key-provider/KDF profile, §8.2/§8.2.1) was decided on 2026-09-26 as Option C; 1b-core landed in #850 and 1b-platform landed as small sequential slices (#854, #855, #914, #915, #916), ending with the OS secure-store adapter (§8.2.5). Status split: S5 design terminal/admitted = YES; Gate 1a = re-admitted and implemented headless; `S5_IMPLEMENTATION_READY = NO` for the R-15 program as a whole; Gate 1b = decided and implemented headless with the platform secure-store adapter (1b-core #850; 1b-platform #854, #855, #914, #915, #916); Gate 2 (typed identity registry, identity-bound record codec and record-class disposition, §20) = admitted and implemented headless; Gate 3 slice 3A (durable staging and promotion) = admitted and implemented headless; Gate 3 slice 3B (the `record-commit` marker codec, §5.4, and the marker commit protocol with startup reconciliation, §9/§9.2) = admitted and implemented headless; Gate 3 slice 3C parts 1, 2, 3a, 3b, 3c-1, 3c-2a and 3c-2b (the authority-root digests, §5.4, the record-catalog descriptors and pages, §5.5/§5.5.1, the root slot, pointer and key-epoch record encodings, §5.3.4, the two-phase root commit with crash recovery and the trusted cold start including the key-epoch set check, §5.3.1, the persisted, root-verified record catalog with `list_records`, §5.5/§5.5.1, and the protected write and read paths committing every marker transition through the root, §9) = admitted and implemented headless, so Gate 3 = implemented headless (closure evidence and residual owners in §20); Gates 4–7 = not admitted; `PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO`. No current TypeScript/Tauri path reads or writes user data through this crate. **Baseline:** `main` at `7ce506ee771f6273e22c08ded049b48955cb40a5` @@ -3577,7 +3577,7 @@ R15_GATE_STATUS R15_GATE1A=IMPLEMENTED_HEADLESS R15_GATE1B=IMPLEMENTED_HEADLESS_AND_PLATFORM_ADAPTER R15_GATE2=IMPLEMENTED_HEADLESS -R15_GATE3=SLICE_3C_PROTECTED_WRITE +R15_GATE3=IMPLEMENTED_HEADLESS R15_GATE4=NOT_ADMITTED R15_GATE5=NOT_ADMITTED R15_GATE6=NOT_ADMITTED @@ -3882,8 +3882,32 @@ Later implementation may be admitted only in these bounded gates: interrupted replacement (old generation served, then `ACTIVE(old)` re-committed), a `PENDING` marker whose root never committed, a chain ahead of the root (not read until reconciled), an ahead generation whose file does not verify (not published), a chain no root ever named, and a - missing, replaced or earlier-deleted marker. Gate 3 closure — #357's reconciliation and the asset-pair - marker boundary — is the rest of Gate 3. + missing, replaced or earlier-deleted marker. + - **Gate 3 closure (#921)** — Gate 3's definition is met for ordinary records, headless and + `CI_ONLY` (Linux, macOS and Windows runners; injected faults, not power loss): + - *file sync* — `stage_and_promote` syncs every staged file before promotion + (`gate3_durable_test`); + - *atomic replacement* — every record, marker, catalog page and root slot is an immutable, + generation-addressed promotion that never overwrites bytes, and the root pointer is replaced + by write-sync-rename-sync (`gate3_durable_test`, `gate3c_root_commit_test`); + - *directory sync* — record, marker, catalog-shard, catalog, root, slot and pointer + directories are synced, and a platform that cannot confirm one yields + `COMMITTED_NOT_CONFIRMED_DURABLE`, never `DURABLE_COMMIT_SUCCESS` (`gate3_durable_test`, + `gate3b_commit_test`, `gate3c_protected_test`); + - *generation reconciliation* — marker-chain startup reconciliation (`gate3b_commit_test`), + root crash recovery and trusted cold start (`gate3c_root_commit_test`), catalog leftovers + (`gate3c_authority_test`) and per-record protected startup resolution + (`gate3c_protected_test`); + - *fault injection* — filesystem and secure-anchor faults at every step of each of those + tests. + Only ordinary records take the protected path; any other class is refused before anything is + written. Residuals, each with an owner: the `asset-pair` marker body and member commit + (§8.4.1) — Gate 5, before any asset pair migrates; the key-epoch record crash window, + `root_commit_mutex`, exclusive admission, reader pins and generation reclamation — Gate 4; + packaged physical power-loss qualification — Gate 6 (#924); record deletion transitions + (`DELETE_PENDING`/`TOMBSTONED`, §8.5) — #948, before Gate 7; and #357's legacy residual — the + current TypeScript/Tauri atomic-write path keeps its authority until Gate 7's switch replaces it + with this one (no standalone Tauri durability authority is added). 4. **Journal/admission:** implement enable/rotate/recovery state machines, exclusive migration admission, bounded inventory/checkpoints, and shutdown/cancellation behavior. 5. **Migration admission readiness and inventory-complete migration** (admission-readiness, not full @@ -3919,4 +3943,4 @@ complete merely because a design document exists. ## 21. S5 admission decision -This S5-A baseline, together with S5-B1/S5-B2/S5-B3, is admitted at the semantic level for everything each actually specifies (protected records/representations enumerated; logical identity, envelope, key/epoch, parse, failure, and downgrade semantics explicit; durable writes, generations/commit markers, admission, lock, recovery, and memory bounds defined; canonical migration-source/payload evidence, race-free `AuthoritySnapshot` lifetime, and the chunked large-object envelope all admitted above; Core-vs-platform responsibilities and headless tests explicit; #357/#359/#360/#361 have implementation owners and closure evidence) but is **not** implementation-ready: no production implementation exists for any of the four documents. The final cross-contract consistency audit across all four documents is complete: every cross-reference, shared formula (`source_value_digest`, the marker-body `is_chunked`/`chunk_count` extension, the §6.3 nonce/AAD wording), and status flag was checked for mutual agreement; two mechanical citation-drift notes and three substantive gaps (S5-B3's chunk-locator operation-identity binding; §10.4.1's disposition-registry exhaustiveness; and Gate 7's resulting class-level-vs-instance-level contradiction that the exhaustiveness fix itself introduced) were corrected in this same change (see the header status line above), and no further inconsistency was found. This is **`S5_A_ADMITTED / S5_B1_ADMITTED / S5_B2_ADMITTED / S5_B3_ADMITTED / CONTRACT_DEFINED / IMPLEMENTATION_NOT_STARTED`**, not `IMPLEMENTATION_READY`; `S5_TERMINAL` was declared YES in a dedicated follow-up after PR #584 merged with green post-merge main CI. Gate 1a is re-admitted (QNB-100) and implemented headless (see the header status line); Gate 1b is decided (Option C, §8.2/§8.2.1) and implemented headless with the platform secure-store adapter; Gate 2 (the typed identity registry, the identity-bound record codec and the record-class disposition), Gate 3 slice 3A (durable staging and promotion), slice 3B (the `record-commit` marker codec and the marker commit protocol with startup reconciliation) and slice 3C parts 1, 2, 3a, 3b, 3c-1, 3c-2a and 3c-2b (the authority-root digests, the record-catalog descriptors and pages, the root slot, pointer and key-epoch record encodings, the two-phase root commit with the key-epoch check, the persisted, root-verified record catalog, and the protected write and read paths through the root) are admitted and implemented headless, while the rest of Gate 3 and Gates 4–7 remain unadmitted, and no production authority switch is allowed. Current desktop filesystem authority remains unchanged and current user data is not retroactively encrypted by any of these documents. +This S5-A baseline, together with S5-B1/S5-B2/S5-B3, is admitted at the semantic level for everything each actually specifies (protected records/representations enumerated; logical identity, envelope, key/epoch, parse, failure, and downgrade semantics explicit; durable writes, generations/commit markers, admission, lock, recovery, and memory bounds defined; canonical migration-source/payload evidence, race-free `AuthoritySnapshot` lifetime, and the chunked large-object envelope all admitted above; Core-vs-platform responsibilities and headless tests explicit; #357/#359/#360/#361 have implementation owners and closure evidence) but is **not** implementation-ready: no production implementation exists for any of the four documents. The final cross-contract consistency audit across all four documents is complete: every cross-reference, shared formula (`source_value_digest`, the marker-body `is_chunked`/`chunk_count` extension, the §6.3 nonce/AAD wording), and status flag was checked for mutual agreement; two mechanical citation-drift notes and three substantive gaps (S5-B3's chunk-locator operation-identity binding; §10.4.1's disposition-registry exhaustiveness; and Gate 7's resulting class-level-vs-instance-level contradiction that the exhaustiveness fix itself introduced) were corrected in this same change (see the header status line above), and no further inconsistency was found. This is **`S5_A_ADMITTED / S5_B1_ADMITTED / S5_B2_ADMITTED / S5_B3_ADMITTED / CONTRACT_DEFINED / IMPLEMENTATION_NOT_STARTED`**, not `IMPLEMENTATION_READY`; `S5_TERMINAL` was declared YES in a dedicated follow-up after PR #584 merged with green post-merge main CI. Gate 1a is re-admitted (QNB-100) and implemented headless (see the header status line); Gate 1b is decided (Option C, §8.2/§8.2.1) and implemented headless with the platform secure-store adapter; Gate 2 (the typed identity registry, the identity-bound record codec and the record-class disposition), Gate 3 slice 3A (durable staging and promotion), slice 3B (the `record-commit` marker codec and the marker commit protocol with startup reconciliation) and slice 3C parts 1, 2, 3a, 3b, 3c-1, 3c-2a and 3c-2b (the authority-root digests, the record-catalog descriptors and pages, the root slot, pointer and key-epoch record encodings, the two-phase root commit with the key-epoch check, the persisted, root-verified record catalog, and the protected write and read paths through the root) are admitted and implemented headless, while Gate 3 is therefore implemented headless (closure evidence in §20), Gates 4–7 remain unadmitted, and no production authority switch is allowed. Current desktop filesystem authority remains unchanged and current user data is not retroactively encrypted by any of these documents. From d813427ebb5d8106c052d88e46088cff8e3e0989 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:37:33 +0200 Subject: [PATCH 2/2] docs(core): state the Gate 3 fault-injection boundaries exactly (#445) Review wave on #949: the closure evidence names the injected boundaries per suite instead of claiming every step, the CHANGELOG qualifies directory sync on Windows, and the asset-pair refusal test also covers reconcile_protected and an untouched record directory. --- CHANGELOG.md | 3 ++- .../tests/gate3c_protected_test.rs | 5 +++++ docs/native/R15-SECURE-STORAGE-CONTRACT.md | 10 ++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0af8b64cc..c158477df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **R-15 Gate 3 complete — crash-durable protected storage core (#445):** the protected-storage core now covers everything Gate 3 requires — synced files, never-overwritten generations, synced - directories, startup recovery and fault-injection tests on Linux, macOS and Windows — for ordinary + directories where the platform supports it (Windows reports them as not confirmed), startup + recovery and fault-injection tests on Linux, macOS and Windows — for ordinary records, and refuses any other record type before writing. What remains is assigned to later gates (asset pairs, power-loss qualification, record deletion, and switching the app over). Nothing reads or writes user data through it yet. PR #949. diff --git a/crates/worldscript-secure-storage/tests/gate3c_protected_test.rs b/crates/worldscript-secure-storage/tests/gate3c_protected_test.rs index d17a292ee..f94f2d449 100644 --- a/crates/worldscript-secure-storage/tests/gate3c_protected_test.rs +++ b/crates/worldscript-secure-storage/tests/gate3c_protected_test.rs @@ -462,6 +462,11 @@ fn an_asset_pair_member_is_refused_before_anything_is_written() { Err(ProtectedError::NotAnOrdinaryRecord) ); assert_eq!(fixture.read(), Err(ProtectedError::NotAnOrdinaryRecord)); + assert_eq!( + fixture.try_reconcile(), + Err(ProtectedError::NotAnOrdinaryRecord) + ); assert_eq!(fs::read_dir(fixture.marker_dir()).unwrap().count(), 0); + assert_eq!(fs::read_dir(fixture.record_dir()).unwrap().count(), 0); assert!(!fixture.root_dir().join("catalog").exists()); } diff --git a/docs/native/R15-SECURE-STORAGE-CONTRACT.md b/docs/native/R15-SECURE-STORAGE-CONTRACT.md index a6d7065ec..c4ea06eef 100644 --- a/docs/native/R15-SECURE-STORAGE-CONTRACT.md +++ b/docs/native/R15-SECURE-STORAGE-CONTRACT.md @@ -3898,8 +3898,14 @@ Later implementation may be admitted only in these bounded gates: root crash recovery and trusted cold start (`gate3c_root_commit_test`), catalog leftovers (`gate3c_authority_test`) and per-record protected startup resolution (`gate3c_protected_test`); - - *fault injection* — filesystem and secure-anchor faults at every step of each of those - tests. + - *fault injection* — at selected boundaries, each named by its test: file creation, partial + write, file sync, read-back error or corruption, promotion link, removal and directory sync + (`gate3_durable_test`); marker and record operations including directory sync + (`gate3b_commit_test`); file creation in a slot directory and the pointer rename + (`gate3c_root_commit_test`); file creation in the record directory + (`gate3c_protected_test`); and secure-anchor faults at the prepare and commit windows of + §5.3.1's crash table (`gate3c_root_commit_test`, `gate3c_authority_test`, + `gate3c_protected_test`) — the durable and marker-chain suites inject no anchor faults. Only ordinary records take the protected path; any other class is refused before anything is written. Residuals, each with an owner: the `asset-pair` marker body and member commit (§8.4.1) — Gate 5, before any asset pair migrates; the key-epoch record crash window,