Repository navigation
[1.29.2] β 2026-10-22
-
Interim maintenance release between v1.29.1 and v1.30. Highlights, each detailed below:
- Settings that were stored but never applied now work, and dead controls are gone (#818).
- The production web app ships its self-hosted DuckDB-WASM files (#1124).
- The page paints before startup finishes, and a stalled start becomes visible (#1134).
- Font and app license texts ship with every build (#1136).
- The Gemini key leaves the request URL (#1135).
- Desktop and container publishing waits for an exact-commit security audit (#911).
- The local model list offers only models the browser runtime can load (#743).
- The desktop third-party notices and SBOM are complete again under pnpm 12 (#871).
v1.29.2 adds no new storage format and makes no encrypted desktop project storage claim; that work (R-15) belongs to v1.30.
-
Fix (offline translations after an update): right after a service-worker update, the app could lose its translations when it went offline before re-fetching them. The new version's dynamic cache was still empty, and the locale handler never looked at the bundles already precached in the static cache. It now falls back to that precached copy (PR #1141).
-
Fix (local AI models): Settings offered three WebLLM models that the bundled WebLLM runtime cannot load: Gemma 3 1B, Gemma 3 4B and Llama 3.3 70B (PR #1142, #743). They are not in its model list, so downloading one failed and generation quietly fell back to the small ONNX model. The three entries are removed, and devices with little GPU memory are now recommended Llama 3.2 1B instead of Gemma 3 1B. A selection stored before this change is kept as it is and keeps its local fallback. A test now checks every offered model against the installed runtime's model list. The defect dates back to v1.5; the release checks for v1.29.0 and v1.29.1 counted only changed AI source files and did not check that the offered models exist.
-
Fix (desktop license notices): Since the move to pnpm 12 (PR #1110), the third-party notices and CycloneDX SBOM in the desktop installers left out about 60 shipped JavaScript packages (PR #1143, #871). These included the local AI runtimes WebLLM, Transformers.js and ONNX Runtime Web, and the optional dependencies of jsPDF and the collaboration transport. pnpm 12's
licenses list --proddropsoptionalDependenciesand the dependencies of linked workspace packages such as@domain/ai-core.scripts/generate-third-party-inventory.mjsnow takes the shipped set frompnpm-lock.yaml(dependencies and optional dependencies of the app and its linked workspace packages, transitively) and takes only the license metadata frompnpm -r licenses list. The Linux inventory has 666 components again (v1.29.1: 665). The differences are real dependency updates. No published release was affected: v1.29.1 was built with pnpm 11. The release check caught this by comparing the candidate's SBOM with the published one. -
Fix (startup): The page no longer stays blank while startup waits (PR #1134).
index.htmlnow paints a static boot shell inside#rootbefore any script runs, and React replaces it on mount. If the app has not mounted after 15 seconds (for example because another tab still holds an older IndexedDB connection, now logged throughonblocked), a notice explains the likely cause and offers Reload, and the stall is logged as an error. Both notices show English at once and switch to the active language from the runtime locale bundles (new keyserror.startup.slowStartanderror.startup.criticalin all 19 locales). The production smoke test now waits for the real app and rejects the boot shell and startup notices, so a bundle that never mounts can no longer pass it. The startup error and the slow-start notice are built with DOM APIs; the old Reload button used an inlineonclickthat thescript-src 'self'CSP blocked, so it never worked. Lighthouse's recurringNO_FCPfailures (a run where the app never painted within 90 s) can no longer abort the gate, because the shell always paints; a boot that still hangs now shows the notice and logs the error instead of a blank page. Why those runs did not mount is not yet proven. -
Compliance (font licenses): The SIL Open Font License texts of the ten bundled
@fontsourcefonts and the app's MIT license are now produced at build time byscripts/copy-third-party-licenses.mjsintopublic/licenses/(PR #1136, #818). This runs inpredev,prebuildand the Vercel edge build (RUNTIME_ASSET_SCRIPTS), so the texts ship with every build from the pinned packages. The font list lives inservices/thirdPartyNotices.ts, and a test keeps it equal to the fontsindex.tsximports. Eleven copies of these files had been committed by accident with #1124; they are removed from version control, andpublic/licenses/is ignored likepublic/duckdb/. -
Security (release publication): Publishing now requires an exact-ref security audit of the commit being published (PR #1137, #911). Tauri
release(GitHub Release assets and updaterlatest.json) and GHCRbuild-push(version,major.minorandlatesttags) each need asecurity-auditjob in the same workflow. That job validatesgithub.sha, checks out exactly that commit, provesHEADmatches it, and runs the pinned OSV scanner withsrc-tauri/osv-scanner.tomloverpnpm-lock.yaml,src-tauri/Cargo.lockandcrates/Cargo.lock. The publish jobs run only when that audit succeeded for the same SHA; a finding, scanner error, skip, cancellation, timeout or SHA mismatch leaves them unrun, for tag pushes and manual dispatches alike.scripts/release-security-gate.mjsand its unit tests reject any weakening, including a new write-privileged job outside the gate. The dispatch-onlyrelease-gate-proof.ymldemonstrates the gate without publishing anything. -
Security (AI connection test): The Gemini connection test under Settings β AI sends the API key in Google's
x-goog-api-keyheader instead of the?key=query string, so the key no longer appears in the request URL (PR #1135, #1129). In the Local and Eco AI modes, the test for any cloud provider is now refused before a key is read or a request is made ("The current AI mode allows only local models", new keysettings.ai.testError.aiModeBlockedin all 19 locales), matching the AI mode as the single cloud authority (#1122). Local providers stay testable. -
Docs: The in-app help (19 locales) now names the real Early Access Features section instead of "Experimental flags", says which features are on by default, drops switches that no longer exist (cross-project search, Plot Board), describes the flag reset correctly, and lists the actual appearance options instead of non-existent specialty themes (PR #1123).
-
Privacy: The AI execution mode is now the single switch for cloud AI (PR #1122, #818). "Local storage only" (on by default) blocked cloud AI on top of the mode, and "EU data residency" only blocked OpenAI and Grok without controlling where data is processed; both toggles are removed. A one-time migration keeps existing installs as they were (a local-only choice becomes local mode); fresh installs start in hybrid. A remote LanguageTool server is used only when you enable it and enter that server.
-
Maintenance (CI): Lighthouse CI waits up to 90 s for first paint and 120 s for load, instead of the defaults (30 s and 45 s), in both the mobile and desktop configs (PR #1133). One run that errors aborts the whole
lhci autorun. On 2026-10-21 a cold first run after the visual-regression phase failed the required Browser Quality job three times withNO_FCP, although the app paints. -
Settings: New Restore default settings under Settings β Data & Backups (PR #1121, #818). It returns every setting to its default without deleting projects, snapshots or API keys, and keeps the UI language, the AI execution mode, privacy choices and collaboration servers, so a reset never silently re-enables cloud AI. Until now the only way back was a factory reset that deletes everything.
-
Settings: Theme Customization under Settings β Appearance gets a card with a reset per color and a reset of all customization back to the selected theme (PR #1120, #818). Since custom colors became active there was no way to undo them. The card also warns about an explicit text color below WCAG 4.5:1, bounds the custom CSS editor, and its hints say that high contrast pauses custom colors (19 locales).
-
Maintenance (Vercel builds): Vercel Previews now build on demand (PR #1130).
vercel.jsonignoreCommandrunsscripts/vercel-ignore-build.mjs: Production always builds, and a Preview builds on a branch's first deployment, when the commit message contains[preview], or when a deployment input changed:vercel.json,vite.config.*,package.json,packages/*/package.json,patches/**,pnpm-lock.yaml,pnpm-workspace.yaml,.npmrc,.nvmrc, the edge build scripts (build-edge,runtime-asset-steps,sync-deploy-base,resolve-deploy-base,dependency-state,copy-*),config/resolveViteBase.ts, the ignore step itself, orapi/**. Unknown state builds. This saves build minutes; correction waves and the strict merge cascade had driven about 75 builds in one day. A skipped build still counts as a canceled deployment. The nightly record pruner now keeps the newest three Vercel deployment records per environment instead of ten, plusmainand every open PR head. -
Fix (web): The production web app (Vercel) did not ship the self-hosted DuckDB-WASM files, so the analytics store could not start; requests for them returned the app page instead (PR #1124). The edge build now produces these assets like the regular build does. The desktop app and GitHub Pages were not affected.
-
Settings: Spell Check under Settings β Advanced Editor was stored but never applied; it is replaced by an opt-in Browser Spell Check (off by default, with a note that some browsers' enhanced spell check sends text to their provider) (PR #1118, #818). Auto-Complete Suggestions and Grammar Check, which had no implementation, are removed; grammar checking remains available through LanguageTool under Integrations.
-
Settings: The Word Count Display, Reading Time Estimate and Writing Statistics toggles under Settings β Advanced Editor now work (PR #1117, #818). The editor badge shows the word count and a reading-time estimate according to their toggles, and the inspector's statistics card follows Writing Statistics.
-
Settings: The Top P and Rate Limit sliders under Settings β Advanced AI are removed; their values were stored but never sent to a provider or used by any limiter (PR #1119, #818). The unused
frequencyPenalty,presencePenaltyandcustomPromptsfields are gone too. Temperature and Max Tokens stay. -
Settings: Theme Customization now works (PR #1116, #702, #818). Its colors and custom CSS were saved but never applied; they now map onto the design tokens (base theme β appearance preset β customization β accessibility; high contrast, in the app or from the system setting, pauses the custom colors while custom CSS stays active; labels on fixed status fills keep their own color). Custom CSS runs in one app-owned stylesheet with
@importand remoteurl()stripped; the stored settings keep the original source and any unknown fields unchanged. Button text on a custom primary color and text on a custom background without a text color are picked to reach at least 4.5:1 contrast. The never-applied old defaults are treated as unset, so existing installs do not change. New installs use the plain writing surface (#701); an explicit textured choice is kept. -
Docs: WorldScript has no user accounts, by design. The README and the in-app help (19 locales) now say so plainly instead of "no account required" (PR #1114). The help no longer describes a Cloud Sync feature that does not exist (article removed, 2947 β 2945 keys) or lists "cloud sync providers". The feature-flag article names all 8 opt-in flags instead of stale counts. The broken DeepWiki and i18n README badges are fixed.
-
Maintenance (Dependabot):
vite-plugin-pwa1.3.0 β 2.0.0 (PR #1115, successor of #1109). The major version only raises the Node floor to 20.19 (the repo requires β₯ 22.19) and allows@vite-pwa/assets-generator^2 as a peer; the plugin source is unchanged between the two releases and Workbox stays at 7.4.1. -
Maintenance (Dependabot):
google/osv-scanner-action2.6.0 β 2.7.0 andossf/scorecard-action2.4.4 β 2.5.0 (actions-minor-patchgroup; PR #1113, successor of #1107), with the OSV pin inworkflowPolicy.test.tsmoved to match. OSV 2.7.0 scans the same 1673 npm, 583src-tauriand 175 Core Cargo packages as 2.6.0. -
Maintenance (Dependabot):
tokio1.53.2 β 1.53.3 insrc-tauri(cargo-minor-patchgroup; PR #1112, successor of #1105). Patch release (Rust 1.71, below the 1.90 floor); lockfile only. -
Toolchain: Move the pinned pnpm from 11.28.5 to 12.13.0 (PR #1110, QNB-55):
packageManager,engines.pnpm, thepnpm/setupsteps inci.ymland the shared setup action, and the docs that state the pin. 12.13.0 is the newest release past the 7-dayminimumReleaseAge.pnpm-lock.yamlstays unchanged:pmOnFail: ignorestops pnpm 12 from adding its package-manager document at the top of the lockfile, because GitHub's dependency graph reads only the first YAML document and would otherwise lose every npm dependency (dependabot-core#15904).dependency-state.mjs reconcile(Vercel's install command and the local reconcile path) now refuses to install when the running pnpm differs from the pin; CI installs the exact pin throughpnpm/setup; and Vercel builds use Corepack (ENABLE_EXPERIMENTAL_COREPACK=1) to run exactly that pin. The supply-chain settings (minimumReleaseAge,strictDepBuilds/allowBuilds,verifyDepsBeforeRun,engineStrict, frozen lockfile) were re-proved under pnpm 12, which also rejects unknownpnpm-workspace.yamlsettings. The general CI and the shared setup action install pnpm 12.13.0 throughpnpm/setup; the reviewer trust workflow installs whatever the base branch'spackageManagerpins (#1111), so it moves to 12.13.0 once this lands. -
CI: Restore the required Reviewer Governance Trust Boundary check (PR #1111). Since #1101 it failed on every PR because
pnpm/setuprefuses aversioninput (11.22.0) that differs from the basepackageManagerpin (11.28.5). The trust workflow now has noversioninput, so the trusted base's own pin selects pnpm, and the workflow policy test keeps it that way. -
Toolchain: Update the pinned pnpm from 11.22.0 to 11.28.5 (PR #1101):
packageManager,engines.pnpm, thepnpm/setupsteps inci.ymland the shared setup action, and the docs that state the pin. 11.28.6 is newer but still inside the 7-day minimum release age. The lockfile is unchanged; the frozen reconcile accepts it as is.reviewer-governance-trust.ymlis a protected trust-boundary workflow and keepspnpm/setup11.22.0 until a base-owned transition authorizes the change; pnpm followspackageManagerand runs 11.28.5 there too. -
Maintenance (Dependabot): Monthly, grouped version updates (PR #1100). Minor and patch updates are grouped per ecosystem:
npm-minor-patch,cargo-minor-patch,core-minor-patch,actions-minor-patch, next to the existingreact,dev-tooling,tauri-deps(now minor/patch) andcodeql-actiongroups. Majors that are not ignored (the Stryker majors are) stay separate, except in the lockstepreactandcodeql-actiongroups, andvite,@huggingface/transformersandzodkeep their own PRs. Minor/major updates of the documented Core MSRV pins incrates/are ignored; patches still arrive. Security updates are unaffected by the schedule. The triage runbook now describes the signed-successor flow it already used, and the inventories indocs/DEPENDABOT-TRIAGE.mdanddocs/CI.mdmatch the new configuration. -
Toolchain: Declare
src-taurirust-version = "1.90"and build it with exactly that toolchain in the Tauri Rust Gate (cargo +1.90 check --locked --all-targets) (PR #1099, #572). The declared 1.77.2 no longer held: 82 of 581 locked crates.io packages already required a newer Rust, up to 1.89 (notify-rust4.18 viatauri-plugin-notification), and no CI job builtsrc-tauriwith its declared floor. The current Tauri plugin lines need 1.90. The Rust Core (crates/) keeps 1.77.2 and its existing exact build. The higher floor activates Clippy's MSRV-gated suggestions; the one hit (unnecessary_map_orintask_supervisor.rs) now usesOption::is_none_orwith unchanged behaviour. The updater stays on 2.12.x until the Tauri plugin group is admitted. -
Security: Update
rustls-webpki0.103.15 β 0.103.16,webpki-roots1.0.7 β 1.0.10 andwebpki-root-certs1.0.7 β 1.0.10 in the Tauri lockfile (PR #1103) for RUSTSEC-2026-0382 (certificate verification may accept distrusted intermediates) and RUSTSEC-2026-0381 / -0383 (bundled roots trusted distrusted emSign intermediates). The advisories were published on 2026-10-19/20 and turned the required OSV scan red onmain(475da803, run 39486423119). The fixed versions declare Rust 1.71; lockfile only, with no other package changed. -
Security: Pin
follow-redirectsto exactly 1.16.1 through an override (PR #1102) for GHSA-8r9p-f939-6h3c and GHSA-q3jh-wrc6-jgq8 (both High, CVSS 8.2, published 2026-10-20). In 1.16.0, credential headers could survive cross-origin redirects. The package is transitive viaaxiosandhttp-proxy, whose ranges already allow 1.16.1 (published 2026-10-01, outside the 7-day release-age floor). Before this override, the required OSV scan reported both advisories on every PR head; with it, the scan passes with no findings. The lockfile change isfollow-redirects1.16.0 β 1.16.1 only. -
Dependencies: Update
vitefrom 8.2.1 to 8.3.4 (PR #1098; Dependabot source PR #1054). Releases 8.3.0β8.3.4 list features, bug fixes and Rolldown dependency updates, no breaking change. Because the production build uses Rolldown, the required Build jobβs productionsmoke:prodcheck (the real bundle must mount in headless Chromium) is the release check for this bump, asdocs/DEPENDABOT-TRIAGE.mdrequires. -
Dependencies (CI): Update the
github/codeql-actiongroup from 4.38.2 to 4.38.3 (init,autobuild,analyze,upload-sarif) incodeql.ymlandscorecard.yml(PR #1097; Dependabot source PR #1084). Patch release; all four subpaths move to the same pinned commit together, asdocs/DEPENDABOT-TRIAGE.mdrequires. -
Dependencies (CI): Update
actions/download-artifactfrom 8.0.1 to 8.0.2 inci.yml,mutation.ymlandtauri-build.yml(PR #1077; Dependabot source PR #1051). Patch release; everyuses:reference moves to the same pinned commit. -
Dependencies (CI): Update
actions/upload-artifactfrom 7.0.1 to 7.0.2 in every workflow that uses it (PR #1096; Dependabot source PR #1049). Patch release; everyuses:reference moves to the same pinned commit, so no workflow keeps the older version. -
Dependencies: Update the dev-tooling group (PR #1095; Dependabot source PR #1083):
@playwright/test1.63.0 β 1.64.0, Storybook 10.6.0 β 10.6.2 (core, addon-a11y, builder-vite, react, react-vite),@storybook/test-runner0.24.5 β 0.26.0,vitestand@vitest/coverage-v85.0.2 β 5.0.4. Theplaywright/playwright-coreoverrides inpnpm-workspace.yamlmove to 1.64.0 with it, so the runner and the core stay one version (they had pinned 1.61.0). Playwright 1.64 now forwardsscreenfrom device descriptors, so emulatedwindow.screenand media queries follow the device. Upstream marked@storybook/test-runneras no longer officially supported in 0.26 (#610 there); a later move to the Storybook Vitest addon is the replacement path. -
Maintenance (deployment records): The daily
prune-deployments.ymlrun now also bounds the GitHub deployment records that Vercel creates (PR #1094). It keeps the newest ten perProduction/Previewenvironment, the record of the currentmaincommit and the Preview records of open PR heads;github-pagesrecords stay with the existing Pages pruner. The step runs inside the already-allowlisted jobs. The dispatched plan reads only. The scheduled job writes only for the trusted workflow on the commit that is stillmain; before each candidate it re-reads the record,mainand the open PR heads, requires a recognized and then proveninactivestatus before the DELETE, keeps the shared rate reserve, and deletes at most 40 records per pass. Newscripts/prune-vercel-deployment-records.mjswith anode:testsuite (12 cases) run through Vitest; documented indocs/PAGES-DEPLOYMENT-RECORD-PRUNING.md. -
Dependencies: Update @huggingface/transformers from 4.2.0 to 4.3.1 in
packages/ai-core(PR #1093; Dependabot source PR #1055). Thedist/transformers.web.jsentry used by thevite.config.tsalias and theexportsmap are unchanged, and thevendor-*chunks key on our own source paths. It brings onnxruntime-web 1.31-dev and onnxruntime-node 1.30.0;adm-zip >=0.6.1andallowBuilds: onnxruntime-node=falsestill apply. Release notes list no breaking change. -
Maintenance (Vercel quota): Turn off Git-triggered Vercel deployments for
dependabot/**anddeptrain/**branches throughvercel.jsongit.deploymentEnabled(PR #1092). In the 24 hours before 2026-10-20 10:55 UTC, 38 of the project's 47 deployments came from these branches (30 Dependabot rebases, 8 cohort successors) on the Hobby plan. AnignoreCommandwas not used because canceled builds still count as deployments. Dependency PRs are qualified by the required GitHub Actions checks; Production builds ofmainand Previews of other branches are unchanged. Documented indocs/VERCEL-PREVIEW-RETENTION-POLICY.md. -
Dependencies: Update @google/genai from 2.22.0 to 2.28.0 (PR #1091; Dependabot source PR #1059, which by then proposed 2.29.0). 2.28.0 is the last release before 2.29.0 made automatic continuation the default for
generateContent/generateContentStream; that path has defects fixed only in 2.30.0, which is still inside the 7-day minimum-release-age. The version is pinned exactly so no lockfile re-resolution can move to 2.29.x. Releases 2.23β2.28 list features and bug fixes only; lockfile and manifest only. -
Dependencies: Update ai (Vercel AI SDK) from 7.0.109 to 7.0.138 (PR #1090; Dependabot source PR #1057, rebased by Dependabot after the cohort freeze; same 7.0 patch line). Manifest range
^7.0.130; lockfile and manifest only. -
Dependencies: Update @ai-sdk/google from 4.0.85 to 4.0.90 (PR #1089; Dependabot source PR #1058). Patch releases; lockfile and manifest only.
-
Dependencies: Update yjs from 13.6.32 to 13.6.33 (PR #1088; Dependabot source PR #1060). Patch release; lockfile and manifest only.
-
Dependencies: Update serde_json from 1.0.151 to 1.0.154 in the Core lockfile (PR #1087; Dependabot source PR #1081). serde_json 1.0.154 declares Rust 1.71, inside the Core floor 1.77.2. No manifest or source changes.
-
Dependencies: Update serde_json from 1.0.151 to 1.0.154 in the Tauri lockfile (PR #1086; Dependabot source PR #1080). serde_json 1.0.154 declares Rust 1.71, inside the 1.77.2 floor. No manifest or source changes.
-
Maintenance: Require the Vercel retention operator to run Node 22.19.0 or later in the 22.x line before source checks or provider/child-process access; the application's Node 22/24 CI matrix is unchanged. Issue #627. PR #1078.
-
Security (desktop updater): Build
tauri-plugin-updaterwithout itszipfeature (PR #1079, part of #911). The feature only unpacks the v1.zip/.tar.gzupdater bundles; withcreateUpdaterArtifacts: truethe release ships the raw-setup.exe/.msi/.AppImage, and the macOS.app.tar.gzpath does not depend on it.zip 4.6.1(RUSTSEC-2026-0367/-0368/-0369) and itsarbitraryhelpers leavesrc-tauri/Cargo.lock. No version change; Rust 1.77.2 stays the declared floor. -
Security (desktop): Remove the unused optional
rust-computefeature and itscandle-core/candle-nndependencies (PR #1079, part of #911). The feature was off by default, no Rust code was gated on it, and no build enabled it. The 64 packages of that subtree leavesrc-tauri/Cargo.lock, among themzip 8.6.0(RUSTSEC-2026-0367/-0368/-0369),safetensors,tokenizers,gemmandpulp; no package is added or changes version. -
Security (OSV policy): Accept RUSTSEC-2026-0370/-0371/-0372 (
zvariant,zbus_names,zvariant_derive) insrc-tauri/osv-scanner.tomluntil 2026-11-30 (PR #1079, part of #911). They are "unmaintained" notices without CVE, CVSS or fixed version: zbus 6.0 absorbed the three crates. The 5.x copies are Linux-only through the Tauri notification and single-instance plugins, whose every published line requireszbus ^5; the 4.x copies are a never-compiled weak keyring edge incrates/Cargo.lockthat cannot be dropped without downgrading the Secret Service session toPlain. The RUSTSEC-2024-0436 (paste) entry is removed because no scanned lockfile containspasteany more. -
Dependencies: Update Tokio from 1.53.1 to 1.53.2 in the Tauri lockfile (PR #1076; Dependabot source PR #1044). No manifest or source changes.
-
R-15 Gate 4: removal on Windows,
test-supportonly (slice H1a-2, part 4 of 4). The object is opened through the handle-relative lookup with delete access and read-only sharing (no handle that opens the file can write to it while the removal holds it, with the exception of a writable mapped view that existed before, see below; a writer that already has it open makes the open failBusy), authenticated on that handle, and then given the POSIX delete disposition on the same handle: noFILE_FLAG_DELETE_ON_CLOSE, no fallback, a read-only file is refused. One newunsafecall (the disposition, one fixed structure, layout pinned by a test); a counter-proof shows what write sharing would have let through. The cases ofremove.rsnow run on Windows too, every code that means "no POSIX delete" (includingERROR_INVALID_FUNCTION) isUnsupported, and a directory is looked at again right before the disposition so that one converted into a junction meanwhile is left alone (Redirected; narrows the window, does not close it). A writable memory-mapped view that another process created earlier and keeps after closing its handle is not stopped by the sharing rule (same-user residual, stated, not closed); on Unix, which has no sharing mode, any same-user writer can change the bytes between the authentication and the removal, which was not stated before: the removal binds the object, not its bytes. No new package. Issue #359. PR #1075. -
R-15 Gate 4: removal of a directory on Linux, the recovery of leftovers and the crash cases on Unix,
test-supportonly (slice H1a-2, part 3).remove_empty_dirmoves the directory through the same quarantine directory and checks the link count of the held directory afterward; macOS reports no usable signal for a removed directory that is still open (hosted probe: link count andF_GETPATHunchanged), so there an empty directory answersUnsupportedbefore anything is moved;remove_leftover_filetakes a leftover.h1q-*directory by its generated name only, authenticates the file in it again and removes it only if accepted. A simulated crash, an entry stranded by a blocked put-back and a foreign object left by a refused put-back are covered; the held directory is synced for what the unlink changed in it, like any removal; a non-empty directory swapped in the last window isDisturbed, notNotEmpty. The recovery also removes an empty directory left by a crashed directory removal (Linux; it holds nothing, so it is not authenticated), and syncs the parent when it removes the emptied quarantine directory, whatever the removal came to. No newunsafe, no new package. Issue #359. PR #1074. -
R-15 Gate 4: hardening of the Unix file removal against a same-user actor in the quarantine directory,
test-supportonly (slice H1a-2, part 2b of 5). The parent is synced after a rollback, a stranded entry, a put-back and a vanished name, and the quarantine directory an entry moved through is synced too (a sync of it that fails is an error after a put-back, a kept entry or an entry an actor removed again, and makes a removalNotConfirmed, never another outcome); the quarantine directory, also when its setup fails, is removed by its name only after a check that the name still leads to the directory that was created (the check narrows the race and does not close it, sincermdirtakes a name: a lost race removes at most an empty directory); an entry, a foreign one included, is reported as retained only if the recorded name still leads to its directory and the entry in it was looked at and is the one that was seen, otherwiseDisturbed(a look at a name, so it detects after the fact and does not prevent; a foreign entry is not opened, so nothing holds its identity unique and a reused identity is not noticed); a quarantine directory that cannot be opened is refused. Tests force each case, including the quarantine renamed away and the put-back blocked. No newunsafe, no new package. Issue #359. PR #1073. -
R-15 Gate 4: removal of a file on Unix,
test-supportonly (slice H1a-2, part 2 of 4).PinnedDir::remove_authenticated_filemoves the entry into a quarantine directory withRENAME_NOREPLACE, identifies the open object there and only then unlinks it; anything else is put back, a failure after the move rolls back or reportsStranded, and the parent is synced. The one window no Unix primitive closes (a same-user actor in the quarantine directory after the last identification) is forced in a test and reported asDisturbed, detected after the fact and not prevented. The rustixprocessfeature is added forgeteuid; no new package, no newunsafe. Directories, leftovers and Windows follow. Issue #359. PR #1069. -
R-15 Gate 4: the API and the isolation of the bound removal,
test-supportonly (slice H1a-2, part 1 of 4).PinnedDir::remove_authenticated_file,remove_empty_dirandremove_leftover_filewithRemovalOutcome,RemovalStageandObserverexist only with the test-only feature, have no caller and, in this part, answerUnsupportedand remove nothing; the platform implementations follow in three stacked parts. A scan of the sources and a compile of a dependent with and without the feature keep the primitives out of every other build.PinnedDir::sync(no deletion) is added. The contract states what no Unix primitive can do (unlink "this inode": a same-user actor in the quarantine directory can win one race, which is detected after the fact asDisturbed, not prevented) and leaves the decision about that residual to the maintainer before any sweep uses it. No newunsafe, no new dependency feature, no new package. Issue #359. PR #1068. -
R-15 Gate 4: handle-relative pinned I/O (slice H1a-1). A new parallel module
pinnedopens one directory with no-follow semantics, resolves it once to a link-free path, and reaches everything below it through a directory that is held open (PinnedDir,PinnedFile): bounded listing, bounded reading from the start of the file, identity and hard-link count; a link, junction or reparse point is refused at every component below the root, and a name must be exactly one ordinary component (on Windows also no stream, wildcard or device name). Unix uses*atcalls on directory descriptors and checks the root's path without following a link; Windows pins a directory with a handle that does not share delete and opens children (NtCreateFilerelative to the handle), listings (the handle's directory information) and entry kinds relative to it, so a directory converted into a reparse point in place leads nowhere. It changes nothing in the file system, has no caller and does not changeDurableFs; the removal primitives follow in H1a-2 behindtest-support. Localunsafefor the relative open, the listing and the final path, and one typed handle-query helper inroot_lock(anunsafe traitwith two implementations); theWdk_*features of thewindows-sysalready locked, no new package. Issue #359. PR #1067. -
R-15 Gate 4: takeover with a trusted clock observation (slice Q1). The mutation-capable takeover no longer takes a timestamp:
JournalTakeoverCommitcarries aLeaseClock, which is sealed (it cannot be implemented outside the crate; the one production clock isSystemLeaseClock, the UTC wall clock of the operating system),commit_journal_takeoverreads it once while the root-commit lock is held into an opaqueTrustedLeaseTimeObservation(no constructor from a number), andpublish_takeover_fencedtakes that observation. An unavailable or unset (zero) clock refuses before a journal or authority byte is written. The predicate now refuses a takeover by the committed lease owner itself (that is a renewal, which needs no clock) and a committed lease whose fields contradict each other. The OS wall clock is not attested and no protection against a compromised clock is claimed. Decision Q1-D1 mirrored on #359, with the one deviation from its letter (the clock reader lives sealed in Core) flagged for the owner. Issue #359. PR #1066. -
R-15 Gate 4: inherit unchanged pages in a streamed capture (slice I1b).
StreamedCapture::push_page_inheritingkeeps the stored predecessor's page of the same index when the pushed entries are exactly its entries (the predecessor's page is read back and authenticated first, one page at a time): the kept page takes the predecessor's reference and generation, nothing is staged for it, and its entries still go into the inventory digest, so the successor equals whatcapture_inventorybuilds from the same pages. A page that differs or lies beyond the predecessor's pages is staged as before. The promotion stores a kept page from the predecessor's set directory under the new set's own directory, and the root-boundbegin_inheriting_streamed_captureverifies the stored set once and keeps unchanged pages on every push;begin_streamed_captureis unchanged. A predecessor of another manifest or a foreign key is refused before anything is created. Stored-set reclamation is the next slice. Issue #359. PR #1065. -
R-15 Gate 4: inherit unchanged inventory pages (slice I1a).
promote_inventory_set_inheriting_fencedlets a capture keep a page of the stored predecessor instead of rewriting it, but only if the page's reference (index, generation, entry count and the content digest of its exact envelope bytes) equals the reference of the same index in aVerifiedInventoryof exactly the committed manifest the set is written under: a re-sealed copy of the same entries, a changed page that claims an older generation, a page beyond the predecessor's page count and a predecessor of another manifest are refused before the first byte. An inherited page is stored again under the new set's own directory, so each stored set stays self-contained. The root-boundcommit_inventory_capture(and the exclusive final capture) verifies the stored set the root names when, and only when, a page keeps an earlier generation; the strictpromote_inventory_set_fencedand the streamed path keep their rule (streamed inheritance and stored-set reclamation are the next slices). Issue #359. PR #1064. -
R-15 Gate 4: reclaim abandoned inventory staging (slice R1).
reclaim_abandoned_inventory(compiled only with the test-onlytest-supportfeature until handle-relative I/O closes the check-then-use windows of its path-based removal; no production build contains the sweep, andtests/feature_isolation_test.rskeeps it so) reads the binding, the manifest and the journal key from the committed root, takes only the owner's fence, and removes the pending directories (inventory/pending-<revision>-<random>) of a revision the journal has committed past: a promotion needs the next revision and the committed revision only grows. RevisionR + 1and above is kept (a live attempt, a retry or a prepared root may own it), as are stored page sets (reclaiming them waits for the inheritance slice, which breaks the generation-based proof), anything outside the exact name grammar, every link or junction (nothing is read or unlinked through one:DurableFs::entry_kind) and any file that does not open as this operation's page; a directory is validated completely before its first removal and files are re-authenticated just before they are unlinked. The root commit event is held from the read of the binding to the last removal, and a former owner, a wrong fence and a journal inRECOVERY_REQUIREDremove nothing.DurableFs::remove_dirandentry_kindare provided methods that default toUnsupported, so an adapter without them is refused before anything is deleted;StdFsremoves only empty directories. No caller schedules the sweep and the path-based directory and root identity stay the H1 residual, whose acceptance criteria the Gate 4D evidence lists. Issue #359. PR #1063. -
Security: update rustls to the OSV-fixed release 0.23.46 (RUSTSEC-2026-0346 to -0349), which the required OSV scan on
mainreported after PR #1061 had passed.rustlsis a direct dependency oftauri-plugin-updater2.12.0,reqwest0.12.28 and 0.13.4,hyper-rustls0.27.9,tokio-rustls0.26.4,quinn0.11.9,quinn-proto0.11.18 andrustls-platform-verifier0.7.0 (version-exactcargo metadataresolve nodes); the lockfile-only update changes no manifest, feature, or toolchain pin. PR #1062. -
R-15 Gate 4E: the
PREPARE/DONEwindow (W1).ProtectedStorage::with_journal_locatorhands the storage the locator of its journal (configuration only). With one, the ordinary operations (write, reconcile, list, read) over a bound root take one finite root event under the shared admission they already hold, authenticate the journal through the locator and are admitted only if its phase isPREPAREorDONEand its binding is exactly the binding of the snapshot root; writes and reconciles repeat that verdict right before their first root commit. Everything else keeps the E1 refusal (MigrationRequired) and writes nothing: no locator, a missing, misplaced, symlinked or wrong-scope location, a journal that does not authenticate, any other phase, a moved binding. A busy root event is the errorProtected(RootBusy)to retry (never the pre-observationNone); an admission fault stays an admission error; a prepared root left by an interrupted catalog commit is recovered by the write and the window is judged again. The journal owner's progress checkpoints hold no admission, so the ordinary catalog commits copy the committed binding forward and two tests land a checkpoint between two exact reads and a sweep lands one at every sixth read of a write: it is neither lost nor written over. Reads and lists follow the same two phases (decision recorded on GH #359). No producer of a binding, no E3 beyond the final capture, no Gate 5β7. Issue #359. PR #1061. -
CI / Pages: admit the Pages record pruner's destructive writes.
DESTRUCTIVE_WRITES_ADMITTEDistrueand the pruning job holdsdeployments: write, but only a scheduled run onmainwrites (at most ten records,workflow_dispatchstays a read-only dry run) and the workflow stays disabled until the operator enables it after a clean live dry run.funds()now treats a missing or malformed rate allowance as unfunded, so a response, or an error that ends a read, without a usable header cannot leave a stale budget behind, the reservation before the inactivation now includes thePOSTitself, and every deletion re-checks the budget after the inactivation settles and before theDELETE. A candidate that changed or stopped being eligible mid-run stops the run instead of being skipped.workflow_dispatchnow runs a separate read-onlyplanjob, so only the scheduled job holdsdeployments: write. The writer-fence section of the pruning contract describes what a delete can still reach (one round trip after the last revalidation, bounded to a record that was a candidate at that read) and the fail-closed list. Only positively proven owngithub-pagesrecords are touched; the newest three, the current and rollback publication and the evidence holds are never candidates. Issue #1028. PR #1041. -
R-15 Gate 4E: the final inventory capture is now exclusive by type.
commit_final_inventory_captureandcommit_final_streamed_inventory_capturepublish the capture that runs inADMIT(final_inventory_captured) and take the installation's exclusive admission by mutable borrow and the root from it, so a shared guard does not compile and no other root or retargeted alias can be given. The ordinarycommit_inventory_captureandcommit_streamed_inventory_capturenow publish only the preliminary inventory and refuse the final one (FinalCaptureNotExclusive); the final commits refuse a preliminary one (NotAFinalCapture), each before the file system is touched. No window, no new producer of a binding. Issue #359. PR #1038. -
R-15 Gate 4E: add the writer journal locator.
JournalLocatornames the journal directory of one installation (its scope and one directory name of a fixed grammar, no path, no search) andauthenticate_journalresolves it under a root event of either admission mode: the locator's scope must be the anchor's, the location must be a real directory (not a symlink) strictly below the admitted installation and outside the authority root, opened as that entry itself without following a symlink, pinned and unchanged after the reads, and the committed root, binding, manifest and key route are authenticated by the existing keyless read. Any failure is a typed refusal that writes nothing and reads no other directory. The result borrows the root event and so ends with the admission. The directory pinning of the conversion gate moves into a sharedplacementmodule with unchanged behaviour. Nothing consumes the locator yet: the E1 barrier, thePREPARE/DONEwindows and the E3 capture barrier are unchanged. Issue #359. PR #1037. -
Documentation: correct the merge runbooks to the repository's branch-retention setting. A merge no longer deletes its head branch (
delete_branch_on_mergeisfalsesince 2026-10-13), so the protected squash command drops--delete-branchand the dependent-PR check guards stacked PRs instead (no hosted CI offmain, squash rewrites ancestry) rather than branch deletion. Deleting a merged branch becomes a manual step after a census of dependent PRs, evidence naming its tip and local worktrees.PR-CI-MERGE-WORKFLOW.md, the CodeAnt and DeepSource review loops and the worktree housekeeping guide no longer state that GitHub auto-deletes head branches. PR #1036. -
R-15 Gate 4E: the recovery of a prepared root that changes the live-migration binding now needs the exclusive admission.
recover_rootrefuses to complete such a root (typed, before any sync or write; the preparation stays) and the ordinary operation that ran it is refused withRecoveryPending; the newrecover_root_exclusivecompletes it, andbegin_conversionresolves a pending preparation through it before it reads the journal (it now takes the provider by mutable reference). A prepared root that keeps the binding still completes under a shared admission. The classification compares the authenticated committed and prepared roots; no journal locator, no E3 writer and noPREPARE/DONEwindow. Issue #359. PR #1035. -
R-15 Gate 4E: a journal phase change is now published only by
commit_phase_transition, which takes the installation's exclusive admission by type. The progress checkpoint, the binding advance and the journal-level checkpoint publish refuse a successor that changes the phase, and the transition refuses one that keeps it, each before a journal byte is written. The conversion driver's own phase changes use the new commit and behave as before. The raw root commit is no longer exported by the production crate, only with the test-onlytest-supportfeature, so it cannot be used to name a phase change. The capture, takeover and renewal writers, the writer journal locator and the PREPARE/DONE windows are unchanged and stay closed. Issue #359. PR #1033, which was closed unmerged: its six commits landed through PR #1035. -
Operations: add a versioned Vercel Preview retention runner for the post-merge
reconciliation.pnpm run vercel:retention:dry-runis read-only and
pnpm run vercel:retention:reconcileruns the full pass, both without arguments. A pass
proves the exact-main gates and Production, classifies every preview conservatively,
re-reads the provider and revalidates each ID before deleting it, never retries a write,
and ends with a post-proof. A mutating pass runs only from a source equal to the reviewed
origin/main. Issue #627. PR #1032. -
Operations: supersede the release-batched Vercel Preview retention record with
ordinary per-merge retention. A dated Active mode section in the retention policy names
the local Claude Code CLI session as the destructive writer under the maintainer decision
recorded in GH #627, keeps the #1020 record as history, and requires every deletion to
name one exact deployment ID revalidated against live data. AGENTS.md and the
deployment, merge and Cursor Cloud runbooks point to it. PR #1031. -
CI / Pages: reconcile owned deployment records safely, keeping the newest three
plus current publication, rollback and evidence holds. Daily bounded reconciliation
replaces broad CI-completion triggers; manual dispatch is read-only. Both publication
slots are proven against the triggering run's actor before any candidate is classified,
so a forged status can neither consume the rollback slot nor vouch for a record.
Failed inactivation stops deletion, and every mutation revalidates the candidate, both
slots and trusted-main authority within a measured per-run request budget.
Destructive writes stay closed in code and in the workflow token until the owner admits a
writer-fence protocol, so even an enabled schedule only reports. Issue #1028. PR #1029. -
R-15 E2 preparation: lifecycle transitions authenticate the committed migration binding before catalog pages and refuse with
RecoveryPendingeven when those pages are missing or corrupt. Existing root-error, locked-provider and unbound-catalog behavior is preserved. The contract records the owner-approved locator, phase-barrier and error-priority direction; PREPARE/DONE windows remain closed. PR #1027. -
Security: update quinn-proto to the OSV-fixed release 0.11.18 (RUSTSEC-2026-0338 to -0342). The crate is a transitive dependency through
tauri-plugin-http,reqwest0.12 andquinn; the lockfile-only update changes no manifest, feature, or toolchain pin and unblocks the required OSV scan. PR #1030. -
R-15 Gate 4E evidence: coordination-resource preservation checks inspect links without following them and reject nonempty files, FIFOs, sockets, and observation errors. Fixture-scoped regressions cover missing/empty resources and link variants. The ordinary-operation barrier is unchanged. Clean successor to PR #1021. PR #1026.
-
Docs: a release tag that is not an ancestor of the checked history now fails as one tag-boundary class instead of a list of disjoint subjects. Real undocumented subjects still fail after a correct ancestor tag. A history read that fails after that ancestor check fails closed instead of passing as empty history. PR #1023.
-
Operations: destructive Vercel Preview retention for source merges may now
wait until the next sanctioned release reconciliation, owned by Codex CLI in
the local VS Code/Ubuntu environment. Cursor Cloud still proves exact-main
CI/CD, CodeQL, Production READY, canonical HTTP, and alias sanity after every
merge, and it does not delete deployments or publish a release. Production,
rollback, active-PR previews, and unknown metadata stay protected. Gate 7
and the storage-authority switch stay maintainer-gated. Deferral is capped
at 14 days and 24 non-protected Preview deployments; either cap, or provider
resource pressure, requires one exceptional local Codex CLI pass and stops
dependent repository mutation until that pass is recorded. That pass does
not restore per-merge destructive cleanup. The cycle ends only after the
sanctioned release pass. The mode is inert until this policy is onmain.
Introducing record: #1020. -
R-15 Gate 4E: the ordinary-operation barrier of a bound root is now pinned with evidence. With an unlocked
provider, a loadable catalog and no interrupted root commit, a committed root that binds a live migration already
madeProtectedStoragerefuse every ordinary operation (write, reconcile, list, read, lock, unlock, shutdown) in
every phase, from the committed root and so across a crash. Eighteen new cases show it with the installation tree
unchanged apart from the two coordination resources, a binding that names no journal, a cold start, a provider that
starts locked, a lost catalog page, an interrupted root commit and unbound controls; the refusal codes in states
they do not pin are stated as such. No behaviour changed. The barrier is stricter than the contract'sPREPARE
window, which stays a later step. PR #1019. -
R-15 Gate 4D: every
DurableFsadapter must now bound its reads.read_at_mostandlist_dir_at_most
lose their default bodies (which loaded the whole file or directory before applying the limit) and become
required trait methods, so an adapter cannot compile without stating how it bounds the allocation; a doctest
triple pins the rule. Test doubles use twotest-support-only helpers that keep their former behaviour.
Behaviour ofStdFsand of every journal read is unchanged. PR #1018. -
R-15 Gate 4D: the conversion session iterates the inventory and leaves
CONVERT(part c-2 of the
conversion driver).ConversionSession::convert_nextconverts up to a batch of entries of the page the
cursor points into through a caller-supplied, idempotentEntryStep, records the page-local cursor and
reportsMoreorDone; a step failure records no checkpoint (what the step had already converted is converted again, so it must be idempotent) and a restart resumes at the persisted cursor.
finish_convertmovesCONVERTtoVERIFYonly after the session saw the end of the last page. The
caller owns the loop and the lease clock. PR #1017. -
R-15 Gate 4D: the conversion session reads the inventory it converts and the cursor becomes page-local
(part c-1 of the conversion driver).ConversionSession::verify_inventoryauthenticates the whole page set
once against the committed root (keylessly, one page in memory at a time) andpagereads one authenticated
page;advance_cursorneeds the verified set and refuses an entry index outside the selected page, where it
used to check the total only. The contract now says whatcursor_entry_indexmeans: the index within its
page of the next entry to process. The cursor-driven iteration is the next part. PR #1016. -
R-15 Gate 4D: the conversion session enters
CONVERTand moves the checkpoint cursor (part b-2 of the
owner's steps through the gate).ConversionSession::enter_convert(idempotent: a session already in
CONVERTwrites nothing) andadvance_cursor(forward only, inside the inventory, inCONVERTonly) share
one private step withrenew_lease, which now commits throughcommit_lease_renewal_heldor the new
commit_journal_checkpoint_held, and every outcome after a landed commit is followed by the admission
check. The cursor-driven iteration is the next part. PR #1015. -
R-15 Gate 4D: the conversion session renews its lease (part b-1 of the owner's steps through the
gate).ConversionSession::renew_leasebuilds the renewal from the session's own snapshot (renewed_lease),
takes the root event through the held admission, commits it with the key route and active epoch the
committed root names, reads the committed journal back under that root event and returns the commit; it is
the first caller of the D3b renewal. The journal directory must lie below the admitted installation and is
pinned for the session, and every file operation goes through canonical paths; the root settles what a
step did, a failed step keeps the snapshot, a retry adopts what was written, a differing candidate of a
crashed attempt is moved aside, and an unsettled step spends the session. Entering
CONVERTand moving the cursor are the next part. PR #1014. -
R-15 Gate 4D: the entry gate of the exclusive conversion driver (part a).
begin_conversiontakes the
installation's exclusive admission by mutable borrow, derives the root layout from the scope that admission
was acquired for, re-reads the committed binding, the registry-routed journal key and the root-named
manifest, and returns aConversionSessiononly for the owner of the lease while the operation is inADMIT
orCONVERTwith the final inventory captured. It reads no clock, holds no key and writes nothing; moving
the journal under the gate is the next part. PR #1013. -
R-15 Gate 4D: stage a streamed inventory without handling a journal key (stage two, part b, of the
streaming capture).begin_streamed_captureroutes the journal key from the authenticated root and
registry, reads the binding and the committed manifest from the root and returns aStagingSessionthat
stages page by page under that key and one journal directory value;commit_streamed_inventory_captureno
longer takes a committed manifest or a journal directory, promoting into the directory the capture was
staged in and loading the manifest from the root under the lock. PR #1012. -
R-15 Gate 4D: commit a staged inventory (stage two, part a, of the streaming capture).
commit_streamed_inventory_capturepromotes the pages thatStreamedCapturestaged into the digest
directory of their successor one page at a time (each page confirmed against its reference, its entries
absorbed into the inventory digest, an identical page already there adopted), publishes the successor and
advances the root binding, in the order and under the guarantees ofcommit_inventory_capture, which now
shares one core with it. The staged files are removed only after the root has committed, so a retry after a
failure still has them; a staged page found wrong stops the commit before any manifest is published. The
key-routing staging session is the next part. PR #1011. -
R-15 Gate 4D: stage a captured inventory one page at a time (stage one of the streaming capture).
StreamedCapturetakes the entries page by page, assigns the page index and generation, checks that they
are valid and strictly ascending across pages, seals each page once and stages its envelope in a private
inventory/pending-*directory (never authority, never read by a reader), keeping one page and one
reference per page in memory; the total is announced up front because the inventory digest commits to it
first.finishbuilds the successor with the same function ascapture_inventory, and
load_staged_pageconfirms a staged page against its reference, a missing one being a broken attempt and
not a recovery state of the journal. A capture is bound to its journal directory and to the key that opened
its manifest, and a failure the caller can handle removes the pages staged so far. Promotion under the root
lock and the composed commit are the next stage. PR #1010. -
R-15 Gate 4D: the journal owner can renew its own lease (maintainer decision C, the wiring half).
commit_lease_renewalpublishes a manifest that moves the lease expiry strictly forward and changes nothing
else (publish_renewal_fenced, a publish of its own so the ordinary checkpoint keeps refusing lease changes)
and advances the root binding to it as a renewal, under the root lock and the routed journal key, in the
takeover's order. It reads no clock: the fence settles whether a lapsed lease may still be renewed. A stale or
foreign owner, an expiry that does not move forward and any other changed field are refused before a journal
write; a retry after a failed root commit adopts its own identical candidate. PR #1009. -
R-15 Gate 4D: constrain the cursor and the lease across successive journal manifests (maintainer decision C).
The successor relation now requires a forward phase change to enter the new phase at cursor(0, 0)
(CursorNotReset;transition_phaseproduces it), enteringRECOVERY_REQUIREDto keep the last cursor, and
every ordinary successor (checkpoint, phase change, recovery entry, inventory capture) to leave the lease
untouched, so the owner and the fence change only by takeover.assert_renewal_successoris the predicate of
the separate same-owner renewal (the expiry moves strictly forward, nothing else changes); wiring it into a
journal-owner operation is the next slice. A lease can no longer appear through a checkpoint: its first owner
is the bootstrap manifest or a takeover. PR #1008. -
R-15 Gate 4D: state and enforce how a journal's target epoch relates to its source. The contract
(Β§10.1.1, Β§10.4): a rotation'starget_epochis above itssource_epoch(Β§8.3 item 2), and an envelope or
schema migration keeps the key epoch (target_epoch == source_epoch; creating a newer epoch, with the durable
target verifier that needs, is a rotation).journal_envelope_epoch, which the manifest validation already
calls, enforces it, so aROTATEfrom 5 to 3 or from 2 to 2 and anENVELOPE_MIGRATIONfrom 2 to 3 or
from 3 to 2 neither encode nor decode (InvalidCounter). This closes the pre-caller
validation criterion before any caller writes a rotation journal. PR #1007. -
R-15 Gate 4D: use the key route in the journal-owner operations and drop the caller-supplied journal key.
commit_journal_checkpoint,commit_journal_takeover,commit_inventory_captureandadvance_live_migration
resolve the journal key throughresolve_journal_keywhile they hold the root lock, right after the committed
binding is read and before any journal write;JournalSourceis now{ dir, operation }. An epoch that cannot
be routed (Revoked, unregistered, not the committed registry) refuses all four operations
(AuthorityError::JournalRoute) with nothing written, and a route to a key the journal was not sealed under is
refused by the authenticated manifest load. This closes the D2b criteria (source-key retention, manifest
preflight, registry route and its wiring). PR #1006. -
R-15 Gate 4D: add the registry-resolved key route of a bound migration journal.
resolve_journal_key
reads the root-named manifest generation, requires its digest to match the committed binding, takes the
header epoch the root vouches for, looks that epoch up in the authenticated key-epoch registry
(Prepared,ActiveandRetiredRecoveryOnlyare usable;Revokedor absent fail closed) and resolves the
record's route to the key. The binding and the registry are read from the committed root inside the call
(the registry must hash to the root'skey_epoch_set_digest, so a removed or uncommitted generation is
refused), never taken from the caller. It never reads the root's active epoch to choose the key, so a
journal still bound after cutover resolves under its source epoch, and it refuses before any mutation.MemoryKeyProvider::import_epoch_key
gives test fixtures a key of known material. Nothing calls the route yet; wiring it into the composed
journal operations and dropping the caller-supplied key is the next slice. PR #1005. -
R-15 Gate 4D: compare a manifest's journal envelope epoch with trusted authority before the journal key is
used.JournalManifest::opentakes the epoch the caller trusts (ManifestRead) and refuses a different header
epoch asKeyEpochMismatchbefore any key use, instead of as an authentication failure afterwards; the epoch
derived from the authenticated body must still agree. The promote readback and candidate adoption pass the epoch
of the manifest they hold,load_manifest_generationtakes an explicit expected epoch, and
load_authoritative_manifestjudges the exact bytes against the root-bound digest before opening them and takes
the epoch from the header the root vouches for. A bad root-named file now reports the authority mismatch. The
registry-resolved journal key route stays a later slice. PR #1004. -
R-15 Gate 4D: refuse revoking a key epoch while a live migration is bound.
write_key_epochreads the
authenticated committed root under theroot_commit_mutexit already holds and refuses aRevoked
generation (RootStoreError::RevocationWhileMigrationBound) before creating anything when the root binds a
live migration, so the epoch the bound journal is sealed under stays resolvable until the binding is cleared.
The rule covers every new revocation because the binding does not name the journal's epoch; other statuses,
and any status without a bound migration, are unaffected. The registry-resolved journal key route and the
manifest pre-key epoch comparison stay later slices. PR #1003. -
R-15 Gate 4D: seal the migration journal under an operation-stable key epoch instead of a hard-coded
1.journal_envelope_epochderives it from the authenticated manifest:ENABLEuses the target epoch
(and must be exactly the first-time0 β 1of the contract),ROTATEandENVELOPE_MIGRATIONthe source
epoch, in every phase, so the epoch no longer depends on the authority root's active epoch that moves at
cutover. The manifest codec, the durable promotion, the page store and the page reader all use that one
derivation and refuse a manifest or page sealed under another epoch (KeyEpochMismatch) before a byte is
written or accepted; the reader and the page store compare the epoch before the key is used, and the single-page
promote seals only for the manifest the root names, so a same-owner copy with other epochs cannot choose it. Registry resolution of the journal key,
the refusal to revoke the source epoch of a bound journal and the lease/cursor phase rules stay later
slices. PR #1002. -
R-15 Gate 4D: persist the proof of the final inventory capture. The journal manifest gains an
authenticatedfinal_inventory_capturedflag (one strict byte afterjournal_page_set_digest):
false beforeADMIT, set only by the capture that runs insideADMITin the same successor that
binds the final inventory digests, never cleared, required fromCONVERTon.ADMIT β CONVERT
is refused without it (FinalInventoryNotCaptured) by the transition constructor, the successor
relation and the root, a capture is refused once the final inventory is captured, and a progress
checkpoint, phase transition or takeover cannot change it. After a crash a journal can therefore
tell a preliminary inventory carried forward from a final one captured behind the write barrier.
The unreleased manifest format is amended in place (no release contains the codec, the crate is
not a dependency of the desktop app), so the format version stays 1. The conversion driver, the
journal envelope epoch and the lease/cursor phase rules stay later slices. PR #1001. -
R-15 Gate 4D: add the composed capture commit.
commit_inventory_captureruns under one
root_commit_mutex: it stores the captured page set, publishes the capture manifest and advances
the root binding as a capture, so the manifest is never written before its pages are durable and
the root never names a manifest whose pages are missing; a retry after a failure adopts what is
durable and writes only what is missing. The root now accepts a change of the inventory fields
only through a capture (assert_capture_successor) and requires every other checkpoint,
includingadvance_live_migration, to leave them alone (assert_progress_successor,
FrozenFieldChanged); before this the successor relation froze them only fromCONVERT.
Inheriting unchanged pages, streaming capture and conversion stay later slices. PR #1000. -
R-15 Gate 4D: add the verified reader of a stored inventory page set.
verify_stored_inventory
loads the root-named manifest itself, resolves each page's generation from its digest-keyed
directory only as a hint, listed with a bound (DurableFs::list_dir_at_most; a missing page, a
missing directory or mixed generations isRecoveryRequired), reads each page bounded, opens it as its own identity at the journal key
epoch, streams the inventory digest one page at a time and keeps only the page references, then
confirms the page-set and inventory digests against the manifest.load_inventory_pagereads one
page by its authenticated reference and refuses a file that changed after verification. The
reader creates nothing and takes no lock. Inheriting unchanged pages in the store, the
semantic-open recovery mapping and the composed capture commit stay later slices. PR #999. -
R-15 Gate 4D: add the inventory page store. The pages of a captured inventory live under a
directory keyed by the manifest's authenticated page-set digest
(inventory/<hex digest>/page-<index>/), so a discarded capture attempt can never be mistaken for
the page set a manifest names and no directory listing is authoritative.seal_inventory_pages
seals each page once, andpromote_inventory_set_fencedwrites the whole captured set for the
committed owner only. The committed manifest must be the generation the root binding names (read
back and compared), and the set is first proven against the manifest successor: the successor is
a valid, encodable capture of the open inventory, the pages are non-empty, indexed0..nand
carry exactly the new revision as their generation (every page is rewritten until a verified
reader can prove an unchanged page against the predecessor's page references), the page-set and
inventory digests match, and each envelope opens as its own page under the journal key epoch.
Nothing is created when the set is refused. The directory chain up to the journal directory is
synced, a failed sync being reported as already promoted, and a retry of a set that failed
partway adopts the identical pages already on disk and continues with the first missing one.
Reading the pages back, the composed commit under the root lock and a streaming capture for very
large inventories stay later slices. PR #998. -
R-15 Gate 4D: add
capture_inventory, the manifest-side constructor of the Β§10.3 inventory
capture. From sealed pages it builds the successor that carries the page set, the entry count and
the inventory digest, inDISCOVER(preliminary) orADMIT(final) while no conversion has run
(not inPREPARE, where ordinary writes are still admitted). Pages must be indexed from 0 without gap or duplicate, hold globally ascending valid
entries and carry a generation no newer than the new revision, so an unchanged page keeps the
generation that names it; the result is re-verified and checked as a valid successor. Pure: no
I/O. The physical page layout, promotion and reading, and the conversion itself, stay later
slices. PR #997. -
R-15 Gate 4D: the composed journal commits can replace a stale candidate. The new
CandidateConflictpolicy (defaultRefuse, the behaviour so far) letscommit_journal_checkpoint
andcommit_journal_takeovermove a durable next-revision candidate that differs from their
manifest (a different manifest, an unopenable file or an oversized one) aside with its bytes
preserved, then publish their own, after every authority, lease and successor check; an identical
candidate is still adopted. This resolves the takeover liveness limit: a new claim after the first
claim's lease expired is no longer refused forever. Safe because reaching the publish proves that
no prepared root names the candidate. PR #996. -
R-15 Gate 4D: add
commit_journal_takeover, the owner takeover (Β§10.1). Under one root lock
it reads the committed binding, publishes a new owner's claim and advances the binding to the new
fencing generation. The committed lease must be expired at the caller-supplied clock (or absent),
and the claim carries fence + 1 and revision + 1, holds a lease that outlivesnow, and changes
ownership only: the phase, cursor, inventory, target key and recovery reason stay the committed
ones. A retry after a failed root commit adopts its own identical claim. The commit evidence
records the new fence, so the former owner's checkpoint is refused afterwards. Nothing calls it
yet; progress by the new owner is an ordinary checkpoint, and bind, clear and the discard half of
R2B stay later slices. PR #995. -
R-15 Gate 4D: the journal checkpoint publishes only a valid successor of the manifest the root
names.publish_manifest_fencedloads that generation (exact path, bounded, authenticated against
the binding digest) andassert_manifest_successorrefuses, before any read of the candidate or
any write, a manifest that skips or repeats a revision, changes the operation, type, epochs or
fence, jumps or reverses a phase, leaves a terminal phase, regresses the cursor within a phase,
rewrites the target key on or afterADMITor the inventory on or afterCONVERT, or changes the recovery
reason other than by entering recovery, or carries a cursor outside its own inventory.
advance_live_migrationenforces the same relation, so a generation promoted directly cannot
become authoritative. The journal manifest loaders now use the bounded read as well. Lease fields and the cursor across a phase change stay unconstrained until their slices.
PR #994. -
R-15 Gate 4B operations integration harness:
gate4b_operations_testno longer assumes a
nonblocking storage operation resolves immediately after a guard is released.Ok(None)is legal
until a forked probe child execs and closes its inheritedflockdescriptor, so the lock,
unlock, shutdown, snapshot and reconcile calls that expect a result now go through a bounded
poll_admittedhelper, while the tests that assertOk(None)because they hold admission still
call the operation directly. Closes the recurringboth_root_contention_windows_preserve_intents_and_refuse_clean_drainflake, the same
family as the write helper from PR #962. PR #993. -
R-15 Gate 4D:
commit_journal_checkpointnow adopts an identical candidate on retry. After a
failed root commit the new manifest revision stays durable as an unadopted candidate; the retry
reads that exact generation under the same fence and authority check and, if it authenticates,
carries the key epoch a promote pins and decodes to the manifest being published, advances the
binding to its digest without writing a journal byte or leaving staging residue. A different,
unopenable or oversized candidate is refused asGenerationExistsand left untouched; the
candidate is read through the new boundedDurableFs::read_at_most, so a crafted file cannot
exhaust memory. Discarding such a candidate, a successor-relation guard,
bind, clear and takeover stay later slices. PR #992. -
R-15 Gate 4D: add
commit_journal_checkpoint, the journal owner's checkpoint. Under one root
lock it reads the committed live-migration binding from the root, publishes the owner's next
manifest revision against that binding (so a stale owner is refused before any journal write) and
advances the binding to the published generation. A key route or epoch change, a missing binding
and a wrong manifest or fence write nothing; a failure after the publish leaves the new revision
as an unadopted candidate and the root naming the previous one. Nothing calls it yet; adopting or
discarding a candidate on retry (R2B), bind, clear and takeover stay later slices. PR #991. -
R-15 Gate 4D: add
advance_live_migration, which commits a root whose live-migration
binding is the journal owner's next revision. Under the root lock it requires the advance to be
the CAS successor of the committed binding (same operation and fence, revision + 1) and the
named manifest generation to be durable, authenticated and equal to the binding's digest, and it
syncs the journal directory before the root is published. It writes no journal byte and nothing
in the crate calls it yet; bind, clear, takeover and the promote-plus-advance coordinator stay
later slices. PR #989. -
Dependencies: raise the
sharpoverride floor from 0.35.4 to 0.35.5 for GHSA-wq5f-xc86-pv6w
(a librsvg memory-safety vulnerability that OSV published on 2026-10-06 and that fails the
Security Audit on every PR) and cap it below 1.x. The fix has been on npm since 2026-09-27, so
the 7-day release-age floor is already met. The change is thepnpm-workspace.yamloverride plus
the lockfile:sharpand its@img/sharp-*/ libvips binaries move to 0.35.5 / 1.3.4 for
@huggingface/transformersandminiflare; nopackage.jsonor source change. PR #990. -
R-15 Gate 4D: refuse a stale caller's journal promote before any I/O.
promote_manifest_fencedandpromote_page_fencednow take the root's committed
LiveMigrationand admit only that binding's owner at its next manifest revision, or its
current manifest for a page. A durabler+1does not maker+2admissible. No root read or
write, no deletion, nokey_epochchange; a stale copy of the binding and the cross-process
CAS stay open. PR #988. -
Dependencies: align
@ai-sdk/google4.0.76β4.0.85 and@ai-sdk/openai4.0.53β4.0.80
on one shared floor,@ai-sdk/provider4.0.19 and@ai-sdk/provider-utils5.0.51.
undicistays 7.29.1, which already satisfies^7.29.0and the workspace override
>=7.29.1 <8.@ai-sdk/react,ai, and@ai-sdk/gatewaykeep their existing pins.
Supersedes Dependabot #979 and #981. PR #987. -
R-15 Gate 4D: resume from the journal manifest generation named by the committed
rootLiveMigration. A newer durable generation is retained and not adopted.
Only a missing copy of that exact generation is recovery-required. Root binding
advance, R2B, and R4 stay later slices. PR #967. -
Dependencies: move
worldscript-secure-storageOS entropy fromgetrandom0.2.17 to 0.3.4.
The 0.3 line renames that call tofill. An entropy-source failure still maps to
RandomnessUnavailable, with no fallback PRNG. Rust 1.77.2 and the other RustCrypto
crates stay unchanged. Supersedes Dependabot #883. -
Dependencies: pair
@tauri-apps/plugin-updaterandtauri-plugin-updateron 2.12.0.
Updater 2.13 requires Rust 1.90, above the desktop crate's Rust 1.77.2, so the
one-sided npm 2.13 bump is not the admitted pair. Node runtime, updater pubkey,
and endpoints are unchanged. PR #901. -
Dependencies: bump devDependency
@types/nodefrom 25.9.2 to 26.6.3.
Runtimeengines.nodestays>=22.19.0. Lockfile updates are the types
package and its peer-resolution keys. Supersedes Dependabot #891. PR #964. -
R-15 Gate 4B operations harness: nonblocking admission that must become
available after a holder is released now uses a bounded retry. Direct
try_acquireremains where the test proves the guard is unavailable now.
Production admission semantics are unchanged. PR #963. -
CI: pin
pnpm/setupv3.0.0 in.github/actions/setup/action.ymland the
workflow-policyjob, withnode-version-file: falseso v3 does not install Node
from.nvmrcahead ofactions/setup-node. The base-owned
reviewer-governance-trust.ymlstays on the admitted v2.1.0 pin until a
protected-transition predecessor authorizes the exact byte change. Supersedes
the incomplete Dependabot pin in #893. PR #963. -
Vercel retention: ordinary mode still requires a full authenticated Preview
reconciliation after every successful merge tomainbefore dependent mutation.
An explicitly declared bounded dependency/toolchain maintenance train may defer
only Preview artifact housekeeping, while exact-main CI/CD, CodeQL, Production
READY, canonical HTTP, and alias/promotion/rollback checks stay mandatory after
every train merge. The train is fail-closed at 12 merges or 24 hours measured
from the UTC instantMERGE_1is recorded, and one
full reconciliation is required before non-train mutation resumes. The
introducing transition lets #963 be that train's first merge when the durable
record is complete before merge; a later pull request cannot reuse the
bootstrap, and a failed resulting-main gate returns the train to ordinary
reconciliation. PR #963. -
R-15 Gate 4B operations integration harness: poll nonblocking admitted writes (
write_poll/
write_until_admitted) so legal shared-admissionOk(None)is not treated as a committed write in
gate4b_operations_test. PR #962. -
R-15 Gate 4D B1
with_fencemutex proof closure (#359 / #922): add deterministic unit test
with_fence_holds_mutex_during_closureso mutex retention is proven through the public fenced entrypoint,
not only viaJournalDurableGuardacquisition. PR #961. -
R-15 Gate 4D slice B1 journal durable API hardening (#359 / #922): crate-private unfenced
promote_manifest/promote_page; public mutation only throughpromote_manifest_fencedand
promote_page_fenced; deterministicwith_fenceexclusion test; post-promotion error provenance
documented. PR #960. -
R-15 Gate 4D slice B journal durable promotion (#359 / #922): headless Core now durably stages and
promotes migration journal manifest and page generations via pre-sealed envelopes (including bootstrap
revision0throughJournalManifest::sealwithout re-enteringseal_record), plus an in-process
with_fencemutex boundary that rejects stale fence tokens before I/O. RootLiveMigrationbinding
updates and restart reconciliation remain deferred (Slice B2). Production authority is unchanged.
PR #959. -
R-15 Gate 4D migration execution state (#957 / #359 / #922): headless Core now implements Β§10.3
journal phase ordering, fence/revision checks, authoritative manifest revision selection against
the root live-migration binding, checkpoint revision bumps, and terminal recovery/done transitions.
Durable journal I/O, record conversion, and root binding updates remain later 4D slices. Production
authority is unchanged. PR #957. -
R-15 Gate 4C journal codec (#445 / #922): headless Core now implements the Β§10.1 migration
journal manifest body,inventory_digest,journal_page_set_digest, canonical paged inventory
page bodies, and manifest/page envelope seal-open helpers with fail-closed malformed-input refusal.
No durable journal I/O, live-migration root binding, or enable/rotate state machine is included.
Production authority is unchanged. PR #956. -
Cursor Cloud Agent tooling (QNB-193): document Cloud bootstrap, PR/review lifecycle, and
resulting-main verification semantics; addpnpm run cursor:cloud-doctorfor non-secret Git vs
ghdiagnostics. PR #955. -
R-15 Gate 4B read/snapshot admission (#445): shared admission now covers authenticated
snapshot/key capture and the complete protected-read handoff; retained views authenticate their
exact immutable epoch evidence. Locked, migrating and misrouted reads fail closed without legacy
plaintext fallback. This supersedes the read half of #952 only; mutation/root-recovery/lifecycle
closure still owns #360. Production authority is unchanged. PR #953. -
R-15 Gate 4B mutation, root-recovery and lifecycle (#445): protected writes, lock/unlock,
shutdown, prepared-root step-F recovery, and namespace-scoped snapshot retention now share the
same admission ordering as read/snapshot capture; Successor A session-binding and rebind semantics
are preserved. This supersedes the mutation half of frozen #952 only. Production authority is
unchanged. PR #954. -
R-15 Gate 4B foundation β shared operations and exclusive transitions (#445): the headless
core now has kernel-backed admission across processes, automatic crash release and typed guards
that bind the installation and authority-root directories. Root events stay beneath the same
admission. Protected-operation and lock/unlock integration follow in the next 4B slice; #360
remains open. No production authority switch is enabled. PR #951. -
R-15 Gate 4, slice 4A β one root writer at a time, across processes (#445): every change to
the protected-storage root of trust now runs under an operating-system lock, so two app windows
or processes can never commit from the same starting point, and a process that crashes while
holding it never leaves it stuck. Nothing reads or writes user data through it yet. PR #950. -
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 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. -
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
only the version the root names, an interrupted write is finished or undone on startup from
authenticated evidence, and an interrupted first write disappears from the record list. Nothing
reads or writes user data through it yet. PR #947. -
R-15 Gate 3, slice 3C part 3c-2a β the stored record catalog (#445): the protected-storage
core now stores the record catalog pages next to the root of trust, commits catalog changes
together with a new root, and lists stored records only from pages that match what that root
names β a tampered, missing or replayed page stops instead of being trusted, and pages left by an
interrupted change are set aside. Nothing reads or writes user data through it yet. PR #946. -
R-15 Gate 3, slice 3C part 3c-1 β key epochs checked against the root (#445): the
protected-storage core now stores its key-epoch records and, both when committing a new root of
trust and when starting up, requires that the root names exactly those records and that its
active key epoch is the one bound to the key it is protected with. PR #945. -
R-15 Gate 3, slice 3C part 3b β committing the root of trust (#445): the protected-storage
core can now commit a new root of trust in the order the storage contract requires, finish or
cleanly abandon a commit that was interrupted at any step, and on startup trust only the root the
secure key store names β repairing a stale pointer instead of following it. Nothing reads or
writes user data through it yet. PR #944. -
R-15 Gate 3, slice 3C part 3a β root and key-epoch records (#445): the storage contract now
fixes the exact byte formats for the stored root of trust, the small pointer to it, and the key
epoch records. The protected-storage core can seal, strictly check and open the root and key-epoch
records, and encode and strictly check the (unencrypted) pointer. Nothing is
committed through them yet; the commit sequence and its crash recovery follow. PR #943. -
R-15 Gate 3, slice 3C part 2 β record catalog pages (#445): the storage contract now fixes
how the authenticated record catalog is split into pages, and the protected-storage core can build,
strictly check and seal those pages. Listing stored records will rely on this catalog instead of
trusting folder contents. Nothing reads or writes user data through it yet. PR #942. -
R-15 Gate 3, slice 3C part 1 β authority-root digests (#445): the protected-storage core
can now compute the digests that tie the whole committed storage state together β every record's
current commit marker, the record catalog, the key epochs and who committed the state β exactly as
the storage contract specifies, with independently checked test vectors. Nothing is persisted or
committed through it yet; the catalog and the root commit follow. PR #941. -
R-15 Gate 3, slice 3B part 2 β commit protocol and startup recovery (#445): the
protected-storage core can now commit a new version of a record through its commit markers and
recover deterministically at startup after an interrupted write: it finishes the write when the
new version is intact and provably belongs to it; when the evidence shows it is not, it keeps the
previous version (or, for a record's very first write, leaves no version) and sets the rejected
bytes aside without deleting them; and when a read or sync fails, it decides nothing until retried. Nothing reads or writes user data through it yet, and the final
authority-root commit follows in slice 3C. PR #940. -
R-15 Gate 3, slice 3B part 1 β commit-marker codec (#445): the protected-storage core can
now encode, strictly decode and seal the commit marker that records which version of a record is
current, exactly as the storage contract specifies, with byte-level test vectors. Nothing reads
or writes user data through it yet; the commit write protocol and startup recovery follow in the
next slice. PR #937. -
Security: basic-ftp override floor raised (#938): a fresh OSV finding (GHSA-c475-qrg2-pj4r,
fixed in basic-ftp 6.2.1) in the Lighthouse CI tooling is resolved by raising the dependency
floor. It affects development tooling only, not the shipped app. PR #939. -
Docs truth: stricter R-15 gate status check (#935): each line in the status block must now be
a single gate entry, and the block ends only at a closing fence on its own line. Code examples
containing a literal comment marker no longer hide the text after them, comments spanning lines no
longer split a phrase, lists such as "Gates 4 and 3 slice 3B" are checked item by item, and an entry
with several problems reports each of them. PR #936. -
Docs truth: R-15 gate status is now checked automatically (#933): the storage contract carries
one machine-readable status line per encryption gate, and the documentation check fails when the
migration ledger disagrees with it or when current text calls an already-implemented gate "not
admitted", a contradiction reviewers had to catch by hand in four consecutive pull requests. The
README test-metrics synchronizer also covers one more line it previously missed. PR #934. -
R-15 Gate 3, slice 3A β durable staging and promotion (#445): the protected-storage core can
now write a new encrypted version of a record next to the previous one: it seals the record in
memory, writes only the encrypted bytes to a temporary staging file, flushes it to disk, checks
it, then links it into place under a new version name without ever replacing an existing
version, checks it again, and flushes the folder on Linux and macOS (on Windows the folder flush
is reported as unconfirmed rather than assumed). The previous version is never touched and no
plaintext is written; every failed step is reported with the files it left, and a staging file
that fails its check is kept for later recovery. Tests inject failures at each file-system step
on Linux, macOS and Windows; real power-loss evidence and deciding which version counts come
later. Nothing in the app uses this yet, and the production authority switch stays off. PR #930. -
R-15 Gate 2 closure β record-class disposition (#445): every protected record class now has
exactly one recorded disposition from the storage contract, and the record codec only seals and
opens classes that are meant to become protected records. API credentials and the browser
storage key-derivation salt and passphrase check keep their own separate protection and are
refused, so they never end up in an ordinary protected record; a class without a recorded
disposition is refused too. Nothing in the app uses this yet, and the production authority
switch stays off. PR #929. -
R-15 Gate 2, slice A β identity-bound record codec (#445): a protected record can now be
sealed and opened through its typed identity, so its authenticated data always comes from the
contract identity rather than hand-assembled fields. Moving encrypted data to another record,
project, installation or record class, changing any authenticated byte, or using a wrong key is
refused, and malformed or legacy bytes are rejected before decryption. Asset pairs derive their
members from the same ID parts, keeping IDs that contain:exact. Nothing in the app uses this
yet, and the production authority switch stays off. PR #928. -
R-15 Gate 2, slice 1 β typed record-identity registry (#445): every protected record class
now has a typed identity built only from that class's registered template, so a record addressed
through it always carries the contract's logical ID and project binding in the authenticated
data. Existing project and record IDs keep their exact spelling even when they contain:,
and an index version must be one the current format supports. Malformed parts (an empty part,
control characters, a non-canonical number, or a malformed installation ID) are refused, never
repaired. Tests prove that data sealed under one identity never opens under another. Nothing in
the app uses this yet, and the production authority switch stays off. PR #917. -
Security: axios and DOMPurify override floors raised (#918): fresh OSV findings on the
locked versions are fixed. The development-only axios (pulled in bywait-on) now resolves to
1.20.0 for 12 advisories on 1.19.0, and the shipped HTML sanitizer DOMPurify moves to 3.4.16
for GHSA-p98j-92pf-mc4p. Both fixed releases are older than the 7-day release-age window, so no
exception was added, and no other dependency changes. PR #919. -
R-15 Gate 1b-platform, slice D β OS secure-store adapter (#445): behind the opt-in
platform-keystorefeature, the protected-storage core can now keep its anchor, key index and
keys in the real operating-system store: the macOS Keychain, the Windows Credential Manager, and
on Linux the Secret Service only (never the non-persistent kernel keyring, and never a silent
in-memory fallback). A missing, locked or unreachable store refuses protected mode instead of
using a weaker store. A new CI job runs the full key lifecycle against the real store on Linux,
macOS and Windows, proves the refusal on Linux without a Secret Service, and builds at the minimum
Rust version; its Linux helper keeps the throwaway keyring in a private temporary home. Nothing in
the app uses this yet, and the production authority switch stays off. PR #916. -
R-15 Gate 1b-platform, slice C2 β durable anchor transitions and the full key provider
(#445): the secure-store authority now applies the two-phase root-anchor transitions
(prepare, commit, abort) as one validated read, the exact pure transition, one replacement and an
exact read-back. Prepare only accepts a root key the store issued and can resolve; an exact
commit replay and an abort with nothing prepared write nothing; conflicts and refused writes leave
the stored anchor unchanged.SecureStoreRuntimeimplements the completeKeyProvider: after its
own confirmed commit of a root it prepared itself an unlocked session rebinds to it, while a root
committed elsewhere (even one that keeps the same key, or one the session merely replays) still
needs a newunlock, a changed root whose key
is missing isLockedfor a stale session rather thanKeyLost, and an epoch key another writer
provisioned is not adopted withoutunlock. No platform adapter, app wiring, or production authority
switch is included. PR #915. -
R-15 Gate 1b-platform, slice C1 β runtime key handles (#445):
SecureStoreRuntimeadds the
runtime half of the key-provider contract over the durable authority from slice B.unlockloads
key handles only after a complete authority read and grants nothing if that read fails, the key
index changes meanwhile, or the final state check reports a lost or inconsistent root.
resolve/resolve_refanswerLockedwhile locked, resolve only a route the current index
issues (never a search), and check the stored item on every call, so a replaced key is
RecoveryRequiredand a deleted oneKeyLost, even if it was already missing atunlock. Every
resolution is refused once the committed root key is lost or replaced, while a lost key of a
merely prepared root transition is not reported as root loss; an unlock is bound to the exact
committed root it saw, so an anchor that loses that root is a rollback (RecoveryRequired) and
one that names another root needs a newunlock(Locked).lockdrops and zeroizes every
handle. It only reads the secure store: no anchor transitions, platform adapter, app wiring, or
production authority switch. PR #914. -
Post-release v1.29.1 truth sync: removed the release-candidate markers from README.md and
CHANGELOG.md now that thev1.29.1tag and GitHub Release are published, and recorded the real
release evidence in AUDIT.md, TODO.md anddocs/RELEASE-V1.29.1-EVIDENCE.md(see## [1.29.1]
below and #872).