codex: sync origin/main -> tinyland/main (repo-roam + per-device crypto; deny.toml + Taskfile union, 2026-06-08)#68
Merged
Jess Sullivan (Jesssullivan) merged 36 commits intoJun 9, 2026
Conversation
Operator-facing expand->contract migration sequence for moving the fleet from shared-master-key wrapping to per-device age/X25519 FileKey wraps. Doc-only acceptance artifact for the TIN-1417 design recon; lands no code and flips no fleet config. Captures the verified 7-step sequence (B1 FileProvider parity -> B3 restore dual-write + roll-call gate -> canary both directions -> sign devices.json -> tcfs key rotate -> keychain hardening -> strict flag flip LAST), per-step rollback, the honest revocation claim boundary, the ratified seven questions, and residual risks. Grounded in and cites docs/ops/per-device-crypto-identity-design-2026-05-18.md.
…mode
Replace the shipped-but-unused `crypto.per_device_wrapping: bool` with a
tri-state `crypto.wrap_mode { Master, Dual, PerDevice }` (default Master),
forming an EXPAND/CONTRACT migration ladder for per-device file-key wrapping.
- config.rs: define WrapMode (serde snake_case master|dual|per_device, default
Master via #[default]); hand-written CryptoConfig Deserialize keeps the legacy
`per_device_wrapping` key as an alias (true -> Dual, false/absent -> Master);
an explicit `wrap_mode` wins. Only `wrap_mode` is serialized going forward.
- engine.rs write path: branch on EncryptionContext.wrap_mode — Master emits the
master wrap only (byte-identical to today, v2); Dual emits BOTH master +
per-device wraps (v2, back-compatible); PerDevice emits per-device wraps ONLY,
drops the master wrap, and bumps the manifest to v3. Dual/PerDevice with no
recipients fail CLOSED.
- engine.rs read switch: version gate accepts v1/v2 and (crypto) v3; a binary
without per-device support rejects v3 fail-closed; v3 with no wrapped_file_keys
is rejected; unknown future versions rejected.
- Roll-call gate: DeviceRegistry::roll_call() + RollCall::all_capable(); the
daemon/CLI/FP build_encryption_context REFUSE PerDevice (dropping the master
wrap) until every active, non-revoked device has a real age recipient,
otherwise downgrade to Dual and warn loudly.
- Rewire all per_device_wrapping readers to wrap_mode: tcfsd grpc.rs, tcfs-cli
build_encryption_context, FP device_ctx/grpc_backend, and the FP config emitter
(emits canonical wrap_mode; FP reader consumes wrap_mode with the legacy alias).
Default remains Master, so existing fleets are unaffected until the migration is
explicitly enabled. Tests cover each mode's manifest shape + version, the legacy
alias mapping, v3 fail-closed reads, the roll-call downgrade, and the
byte-identical Master default.
…evice) Update the held migration plan to the ratified tri-state model, replacing the prior two-bool per_device_wrapping (expand) + per_device_wrap_strict (contract) framing. - New "control surface" section: crypto.wrap_mode = master | dual | per_device (default master), with a table mapping each mode to the manifest shape, manifest version, and migration phase. - Expand = dual (v2, both wraps); contract = per_device (v3, per-device-only, master wrap dropped). The v3-on-contract fail-closed rule is stated explicitly; dual stays v2 (back-compatible). - New "roll-call code gate" section: the daemon/CLI/FileProvider refuse to contract to per_device until every active device is per-device-capable, otherwise downgrade to dual and warn (never silently drop the master wrap). Grounded in DeviceRegistry::per_device_roll_call_ready and the EncryptionContext.with_wrap_mode plumbing. - Migration steps and per-step rollback re-expressed against wrap_mode; Step 7 is the per_device contract flip after a green roll-call. - Back-compat documented: legacy per_device_wrapping true -> dual. - Keeps the honest claim boundary, the ratified seven questions, and the residual risks; adds a residual-risk note on the v3 manifest namespace shared with symlink manifests (disambiguated by kind: symlink).
… a per-device identity
The engine read switch took the per-device branch whenever wrapped_file_keys
was non-empty and errored ('no age identity') if the context had no device
identity, never falling back to a present master wrap. A Dual manifest (v2,
carrying BOTH encrypted_file_key and wrapped_file_keys) read by a Master-mode /
no-identity context therefore failed closed instead of decrypting via the master
wrap, nullifying Dual's rollback/recovery rationale and breaking the live
daemon/FileProvider read path whenever a Master device reads peer-written Dual
content.
Attempt the per-device unwrap first; on any failure or unavailability (no ctx,
no identity, or no stanza addressing this device) fall back to the master wrap
ONLY when encrypted_file_key is actually present. A PerDevice/v3 manifest
(encrypted_file_key = None) stays strictly fail-closed and surfaces the
per-device error. Master writes remain byte-identical.
Add a positive test (Dual write, master-only read -> byte-exact plaintext) and
keep the v3 negative (master-only read of a per-device manifest stays
fail-closed).
…de-enum DRAFT: TIN-1417 tri-state crypto.wrap_mode migration (agent-drafted, needs review)
…-crypto-migration-v2 DRAFT: TIN-1417 migration doc — tri-state crypto.wrap_mode (supersedes Jesssullivan#490) (agent-drafted, needs review)
…ig (sandbox read path) The macOS FileProvider .appex cannot fs-read ~/.config/tcfs under the sandbox, so the per-device wrap read path (device registry + device-<id>.age) was unreachable in-sandbox. This mirrors the existing master_key_base64 Keychain inlining for the per-device material. Rust read-side (TESTABLE, full gate green): - crates/tcfs-file-provider/src/device_ctx.rs: build_encryption_context now resolves recipients and this device's secret INLINED-FIRST, preferring config values (device_recipients / device_recipients_all_capable / device_secret) over the on-disk registry/secret read. The fs read remains the fallback for the non-sandboxed daemon/CLI and tests. Every inlined recipient is re-validated with is_real_age_public_key; the PerDevice roll-call gate is driven by the host-supplied all_capable signal on the inlined path. wrap_mode default (master) stays byte-identical. - Added 5 tests: inlined-preferred-without-fs, all_capable=false downgrade to Dual, fs fallback when not inlined, mixed inlined-recipients + fs-secret, and inlined-secret-without-recipients fail-safe to master-only. Swift host (DRAFT, UNVERIFIED — cannot build/test in sandbox; needs real-device Xcode build + FileProvider QA): - swift/fileprovider/Sources/HostApp/HostApp.swift: configForKeychain now also inlines the active per-device recipients and this device's age secret into the Keychain config copy, INERT when wrap_mode=master. Mirrors the master-key inlining pattern exactly. Gate (Rust only): cargo fmt --check CLEAN; clippy (grpc + default) no NEW warnings (pre-existing ensure_master_decryptable dead-code warning only); cargo test -p tcfs-file-provider --features grpc all pass.
The device registry doubles as the per-device age recipient set, but it was plain serde_json with no signature: anyone with direct object-store write to meta_prefix/tcfs-meta/devices.json could inject a hostile recipient, un-revoke a device, or drop one, defeating per-device revocation (SEV-1). signing_key_hash is only a self-fingerprint, not a signature; the module doc-comment falsely claimed the registry was "signed by the master identity". Fix: - DeviceIdentity gains revoked_at, enrolled_by, signing_pubkey (all optional, back-compat deser; revoke() now stamps revoked_at). - DeviceRegistry carries an Ed25519 signature envelope (registry_signature, signer_pubkey, sig_alg) over a canonical, order-independent serialization of the device list. The signing key is HKDF-SHA256-derived from the master key (domain tcfs-device-registry-signing-v1) so any master-holder can sign/verify with no new key distribution. - sign()/verify_signature() + load_verified()/save_signed() and remote variants. verify_signature binds to the master-derived signer first (a sig from any other key is rejected), then verifies; a tampered list is a hard error; an unsigned legacy registry is RegistryTrust::UnsignedLegacy (bounded migration window, loud warn). - Recipient-set builders (tcfsd grpc.rs, tcfs-cli main.rs, file-provider device_ctx.rs build_encryption_context) now load_verified and REFUSE to build a per-device recipient set from an unsigned/tampered registry — they fall back to the shared master wrap (fail-closed), never wrapping to an unverified recipient. - Writers sign: tcfs init, device enroll, device revoke, device-id backfill, remote sync; the daemon re-signs a first-run/unsigned registry once the master key is available. - Fixed the false doc-comment. wrap_mode stays master by default; the Master path returns before any verify, so default behavior is byte-identical and an unsigned registry serializes without the new fields. Tests (tcfs-secrets + readers): tampered devices.json (inject recipient / un-revoke / flip field) fails verification on load; a registry signed by an unrelated key is rejected; forged signer pubkey rejected; recipient-set builders refuse an unverified registry; sign->serialize->load->verify round-trip; legacy unsigned registry loads with a warn; back-compat serialization guard.
The enroll --sync-remote path discarded the RegistryTrust returned by load_remote_verified, so an UnsignedLegacy (signature-stripped) remote was merged unconditionally and then re-signed locally with the real master key. An attacker with object-store write access could strip the signature and inject a hostile recipient, laundering it into a validly-signed registry. Fixes: - Bind the remote trust on the merge path via enforce_remote_merge_trust: refuse to merge an UnsignedLegacy remote unless the operator explicitly passes --accept-unsigned-remote (loud warning + re-sign). A signature- present-but-invalid remote stays hard-rejected by load_remote_verified. - Correct the overstated comment at the load_remote_verified call site. - Harden verify_signature to verify_strict() (rejects malleable/small-order signatures); drop now-unused Verifier import. - Document the UnsignedLegacy migration window with a dated hard-reject criterion (TIN-1417 B5, 2026-09-01) on both RegistryTrust::UnsignedLegacy and enforce_remote_merge_trust. Tests: unsigned_remote_with_injected_recipient_is_refused_not_laundered reproduces the full attack end-to-end against an in-memory object store and proves the merge is refused (with a counterfactual showing the laundering would otherwise succeed); plus accept_unsigned_remote_flag_allows_merge and signed_remote_passes_merge_gate. Existing B4 tests stay green.
… in Rust, correct trust-scope docs, bech32-decode Swift validation device_ctx.rs: - Correct false doc claims that inlined recipients are 'already-VERIFIED'. The Swift-read registry is NOT signature-verified (that is B4, separate/ unmerged). Comments now state recipients are re-validated for age-key WELL-FORMEDNESS only; registry AUTHENTICITY is out of scope (pending B4). - Hardening (availability): do NOT trust the host-provided all_capable boolean. Swift isRealAgePublicKey was a prefix/length heuristic; an over-count could set all_capable=true and let Rust drop the master wrap prematurely -> device lockout. all_capable is now RE-DERIVED in Rust from its OWN re-validated recipient set (true iff every inlined entry parsed as a real age::x25519::Recipient). - Tests: replace the host-boolean-trusting downgrade test with two re-derivation guards: host all_capable=true + a malformed recipient re-derives FALSE in Rust (-> Dual, no lockout); host all_capable=false is overridden to PerDevice when recipients are well-formed. HostApp.swift (DRAFT, not buildable here): - Harden isRealAgePublicKey toward a real bech32 decode (HRP=age, charset, BCH checksum, 32-byte X25519 payload) instead of prefix+length. Still needs a real-device Xcode build + FileProvider QA; does not claim Swift compiles.
…ed-registry DRAFT: TIN-1417 B4 sign the device registry (Ed25519, master-derived) (agent-drafted, needs review)
…n-inlining DRAFT: TIN-1417 inline per-device recipients + age secret into Keychain config (agent-drafted, needs review)
proc-macro-error2 is an unmaintained-advisory (not a vulnerability), pulled as a build-time proc-macro dep of age via i18n-embed-fl (age 0.11.2 -> i18n-embed-fl 0.9.4 -> proc-macro-error2). No runtime or security impact; not removable without churning the age dependency. Ignored with justification, consistent with the existing bincode/rand transitive-advisory ignores. Fixes the persistent cargo-deny red on main.
…y-proc-macro-error2 fix(fileprovider): restore cargo-deny and grpc-only build gates
…jects Track A roam enrollment: ~/.claude/projects is a disjoint 733MB tree outside the single TCFS sync_root, so it is enrolled via a scheduled 'tcfs reconcile --path --prefix --execute' unit rather than a sync_root change. Spec includes paste-ready nix for tummycrypt.nix (extraReconcileRoots option + tcfsReconcileScript builder sourcing creds like daemonWrapper + Darwin launchd agent and Linux systemd oneshot+timer), per-host enablement for macbook-neo and honey on the shared agent/claude-projects prefix, the bounded-subset-first rollout with TCFS_UPLOAD_ASSUME_FRESH_PREFIX guidance, per-cycle log/deny-set/ SHA256-after-decrypt verification, and explicit operator nix-switch steps. Doc only; no code or wrap_mode change. Engine reuses the daemon's master wrap and fail-closed deny-set; reconcile is not FileProvider-gated.
The legacy direct/uniffi FileProvider backends only perform master-key unwrapping (no per-device age identity). Their fail-closed guard rejected ANY manifest carrying wrapped_file_keys, which also locked out Dual (v2) manifests that carry BOTH a master encrypted_file_key AND per-device wraps. Mirror the engine read switch (crates/tcfs-sync/src/engine.rs ~:2395): reject only when wrapped_file_keys is non-empty AND encrypted_file_key is None (PerDevice/v3). When the master wrap is present (Dual/v2), fall back to it. PerDevice/v3 (encrypted_file_key=None) stays strictly fail-closed — never materialize raw ciphertext. wrap_mode=master stays byte-identical. - device_ctx.rs: ensure_master_decryptable now permits Dual, rejects PerDevice/v3 (used by direct.rs fetch + fetch_with_progress). - uniffi_bridge.rs: same condition applied inline to hydrate_file and hydrate_file_with_progress (preserving the Decryption error variant). - Tests: Dual reads via master wrap on direct + uniffi backends; PerDevice/v3 fails closed on both; master-only unchanged.
…-read-fallback DRAFT: TIN-1898 FP direct/uniffi backends read Dual/v2 via master-wrap fallback (agent-drafted, needs review)
…e-projects-enrollment DRAFT docs(ops): roam-enroll ~/.claude/projects via scheduled reconcile unit (agent-drafted, needs review)
Add a NEW scoped command `tcfs key rotate <prefix> [--rotate-keys] [--resume]` (separate from the master `rotate-key`). For every manifest under <prefix> it decrypts the current FileKey (master or per-device per the wrap shape), generates a fresh random FileKey, re-encrypts each chunk under it (new BLAKE3 content addresses), re-wraps to the CURRENT (post-revocation) recipient set resolved from the verified device registry honoring wrap_mode, publishes the new manifest, and GCs orphaned old chunks only after publish via the existing reference-safe cleanup_orphaned_chunks sweep. Resumable via .rotate-state.json. Make recipient-set removal the default for `tcfs device revoke` (cheap/immediate) with a loud forward-secrecy warning, and gate FileKey rotation behind `key rotate --rotate-keys` with a projected-bytes confirmation prompt. Rename the misleading skipped_plaintext_manifests counter: split into skipped_keyless_manifests (genuinely plaintext) and skipped_per_device_manifests (per-device-only, which the master rotate cannot re-wrap) so the master rotate no longer silently skips per-device manifests as if plaintext.
Reviewer must-fixes for PR Jesssullivan#504 (scoped per-device FileKey rotation): 1. cmd_key_rotate previously printed the forward-secrecy reassurance UNCONDITIONALLY. Under the DEFAULT WrapMode::Master (and Dual) the re-keyed FileKey is re-wrapped to the UNCHANGED shared master key, so a revoked master-key holder STILL decrypts the re-keyed content -- the message was false and dangerous. Gate the reassurance on wrap_mode == PerDevice && !device_recipients.is_empty() via the new rotation_grants_forward_secrecy / forward_secrecy_summary_lines helpers; Master/Dual now print a LOUD warning that no per-device forward secrecy was gained. Adds 4 tests covering the Master, Dual, PerDevice, and empty-recipient PerDevice messaging paths. 2. Orphan-chunk GC defaulted to grace=0 (immediate), which could 404 a concurrent reader holding an old chunk address in a multi-writer fleet. Default to the configured orphan_chunk_cleanup_grace_secs; add a --gc-immediate flag for grace=0. GC stays reference-safe (only chunks unreferenced by ANY live manifest are eligible). Surface a deferred-within-grace count so deferred orphans are not mistaken for lost. 3. Dry-run wording no longer claims re-chunking: rotation re-encrypts per the existing index (file_hash/file_id stay valid).
DRAFT: TIN-1899 scoped per-device key rotation for forward secrecy (agent-drafted, needs review)
Adds the one missing precision layer the large-workdir research identified for Gate G5 / TIN-1620 (one expendable live repo): a git-aware dev-env fingerprint that asserts a roamed repo is byte-and-semantically identical across hosts, with no mid-reconcile .git corruption. This is a tighter assertion layered ON TOP of the existing QA-matrix rows (T2/T3 exact bytes, T8/T9 + M3/M6 peer-edit rehydrate, T10/T11 + M5/M5-R conflict/keep-both, T12 symlink, T13 modes), not a new matrix and not a new sync engine. scripts/repo-roam-fingerprint.sh: capture/compare/seed-canary/self-test. Capture records git status --porcelain=v2 --branch, HEAD+branch+symbolic-ref, refs, staged vs unstaged diff hashes, index blob shas, untracked, stash, reflog tip, git fsck --full, and a sorted working-file sha256 manifest honoring the fail-closed reconcile deny-set (.env*/secret/live-WAL recorded DENIED, never hashed). compare exits nonzero on ANY difference and requires fsck clean on both sides. seed-canary builds a throwaway repo (feature branch + staged + unstaged + untracked + stash + exec script + symlink) and refuses $HOME / ~/git / fs-root. The tool plugs into the existing canary/evidence scaffold (git-repo-canary.sh -> home-canary-linux-xr-shadow.sh for shadow/push, git-repo-restore-proof.sh for rollback, the neo-honey / TIN-1620 flip-flop harnesses for cross-host lifecycle) and emits one new dev-env-fingerprint/ subtree plus one dev-env-zero-diff gate line; it does not duplicate any of them. docs/ops/repo-roam-test-plan-2026-06-08.md: the canary runbook, mapping each step (R0-R5) to the EXISTING T/M rows + the TIN-1620 G5 acceptance, marking LIVE-fleet steps as out of scope for this PR. Documents the Facet-4 enrollment fact (.git-as-files is config-scoped via a per-repo -c config with sync_git_dirs=true + git_sync_mode="raw"; ~/git enrollment works with NO Rust change; the global flip is forbidden for blast radius), the Facet-5 zero-diff caveats (mtime/index trap needing a git update-index --refresh mitigation; symlink drop in the reconcile collect path), and the Facet-6 raw-mode .git corruption gate (G5-git-5 expected-fail until conflict resolution is .git-aware). Flags the .git-as-files vs git-bundle tension and the unlisted stash assertion as operator-review items. Taskfile: lazy:dev-env-fingerprint + lazy:test-dev-env-fingerprint, mirroring the existing inventory/canary task surface. shellcheck-clean; self-test (disposable /tmp seed -> capture -> capture -> compare, plus drift-detection negative control) and the regression suite both pass. No live fleet touched.
Three must-fixes for the dev-env zero-diff fingerprint (PR Jesssullivan#507): 1. capture is now genuinely read-only. Drop the `git write-tree` call from head.env — it wrote tree objects into <repo>/.git/objects and touched the index, breaking the read-only contract that the live R0 step relies on when pointing capture at real expendable repos. The staged/index identity is already captured by `git ls-files -s` (index-blobs.txt) + the `git diff --cached` hash (diff-cached.sha256), so write-tree was redundant. Header + runbook read-only claims corrected to be TRUE. 2. Tighten the fsck corruption grep to genuine signals only: ^error: / ^fatal: / invalid sha1 pointer / broken link. Drop the broad `missing` / `dangling.*commit` matches that false-positive on healthy repos with gc'd / expired reflogs (a lone `dangling commit <sha>` notice was wrongly flipping fsck=dirty). 3. Make the [PR] vs [LIVE] boundary unmissable in the runbook: a green self-test only proves the assertion engine is internally consistent on one host — it is NOT proof of flip-flop zero-diff in either direction or of live .git corruption catching. Those are delegated to the [LIVE] R2/R3/R5 steps and the Facet-6 harness (PR Jesssullivan#506).
…0 T13-Z)
Two blockers for zero-diff repo roam, where a tree must roam neo<->honey
with `git status` staying clean.
BLOCKER A — preserve source mtime on restore. A fresh restore otherwise
stamps "now" onto every file, smudging .git/index's stat cache and making
`git status` report spurious-dirty. Add an OPTIONAL `mtime: Option<(i64,u32)>`
(unix secs + subsec nanos) to SyncManifest, captured at upload from the same
std::fs::Metadata that already yields file_mode, and re-applied via
utimensat (cfg(unix)) after the atomic rename and BEFORE make_sync_state_full
re-stats, on both the empty-file and chunked-file restore paths.
Back-compat: the field is #[serde(default, skip_serializing_if = ...)], so old
manifests deserialize to None (restore unchanged) and an mtime:None manifest
serializes byte-identically to a pre-field one. Manifests are content-addressed
by the file's BLAKE3 hash (manifests/{file_hash}); the manifest's serialized
bytes are never hashed for addressing/dedup, so the field cannot cause the
fleet to re-upload or re-address existing content.
BLOCKER B — opt tracked symlinks in. collect_local_set now sets
preserve_symlinks:true (follow_symlinks stays false) and folds the collected
symlinks into the reconcile map, and the push path dispatches symlinks to the
first-class symlink-manifest uploader instead of dereferencing them. The
restore side + deny-set guard already handle symlinks.
Tests: mtime round-trips (empty + chunked); old manifest w/o mtime restores
with today's behavior and serializes without the key; symlink round-trips
through reconcile push->restore with its target intact.
…rprint TIN-1620: dev-env zero-diff fingerprint for the repo-roam ladder
…1620)
compare_both_exist hashed symlinks via hash_file, which dereferenced the
link and hashed the target's content, then tried to parse the stored
SymlinkManifest as a SyncManifest. That parse failed, so a tracked symlink
present on both local and remote was classified Push{NewLocal} every cycle
and never reached UpToDate — a symlink-bearing repo re-pushed forever.
Add a symlink-aware branch: detect the local symlink with symlink_metadata
(no follow), read its target, and compare on symlink identity using the
exact scheme the push path writes — engine::symlink_manifest_hash(target),
the same hash stored in the manifest hash, the remote index entry, and the
local sync-state blake3. The remote manifest is parsed as SymlinkManifest
(the type upload_symlink_with_device serialized) and fed through
compare_clocks, reusing the regular-file UpToDate/Push/Pull/Conflict logic:
identical targets short-circuit to UpToDate, divergent targets fall to the
vector-clock decision. Fails closed to a conservative re-push if the remote
manifest is not a parseable symlink manifest.
Regular-file path is unchanged. symlink_manifest_hash and
read_symlink_target_text are promoted to pub(crate) so reconcile reuses the
push path's identity rather than reinventing it.
Tests: reconcile_tracked_symlink_converges_up_to_date asserts a second
reconcile of a pushed symlink plans UpToDate (fails as Push{NewLocal}
before the fix); reconcile_changed_symlink_target_is_not_up_to_date asserts
a repointed symlink surfaces as Conflict, so the fix is non-vacuous.
…user-story-test-plan-20260608 docs: define git roam daily-driver acceptance
…-fingerprint-denyset-20260608 test: harden repo-roam fingerprint deny-set
…fidelity DRAFT: reconcile restore fidelity (mtime preserve + symlink opt-in) for zero-diff roam — TIN-1620 T13-Z (agent-drafted, needs review)
…n-20260608 # Conflicts: # Taskfile.yaml # deny.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Syncs
Jesssullivan/tummycryptorigin/main(c8a9714) ontotinyland-inc/main(cf886d9), carrying this session's repo-roam + per-device-crypto work that the lab flake builds from.What comes over (23 commits origin had, tinyland lacked)
crypto.wrap_mode(master|dual|per_device), B4 Ed25519-signed device registry, keychain inliningtcfs key rotate(forward secrecy) + revoke UX~/.claude/projectsroam-enrollment runbookConflict resolution (union — no coverage/feature dropped)
deny.toml— identical advisory ID set on both sides (RUSTSEC-2025-0141,-2026-0097,-2026-0173); kept origin's comment prose. No advisory coverage changed.Taskfile.yaml(lazy:check) — UNION: keeps origin'sgit-roam-daily-driver-harness+dev-env-fingerprinttooling and tinyland'stest-bazel-macos-package-contract.sh. Both script files are present in the merged tree;task --list-allparses.Preserves all 41 tinyland-only Darwin/Bazel release-packaging commits (non-
.gitdisjoint paths, no conflict). Non-destructive merge, no force-push.This sync is the gate for bumping the lab
flake.locktummycryptinput (currently stale ate53bc72) to deploy the repo-roam fixes to neo+honey.