Skip to content

Enforce Viewer access and protect local text edits - #1046

Merged
callumalpass merged 11 commits into
mainfrom
fix/viewer-write-refusal
Oct 29, 2026
Merged

callumalpass merged 11 commits into
mainfrom
fix/viewer-write-refusal

Conversation

@callumalpass

@callumalpass callumalpass commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Viewers could make locally visible Editor changes and disk edits that stayed pending and could sync after an upgrade. The Editor shaped controls from the local Host grant, which reported Owner, although the SDK already exposes the collection membership role. Local Host submission and disk capture also bypassed that role boundary.

Evidence: R3-4, R3-4b.

Changes:

  • Editor uses the SDK membership snapshot and current native status to make Viewer controls read-only, show a specific permission message, and refuse stale mutation actions before local capture. Type updates follow the same boundary. Live permission updates preserve the open workspace and drafts; saves require no membership network request. An observed Connect downgrade takes effect while signed policy catches up; the SDK keeps its latest session grant, including during metadata omission in recovery. Native grants then control live upgrades.
  • API 1.3 adds optional existing GrantInfo to session status, so SDK clients receive current signed role-effective permissions without reconnecting. Old consumers ignore the optional key, old producers may omit it, and API 1.0 remains supported. Synced Host grants reflect known membership; existing pre-bootstrap/offline capture remains unchanged.
  • New Viewer disk changes to text records and definitions use existing durable read_only holds before candidate catalog validation. Files stay held across upgrade/restart and require an explicit submit or discard choice. Moves are represented as a source deletion and destination change in one hold transaction, preserving both paths for discard. Submitting checks original content and current path and uses canonical validation; intervening synced edits or renames produce a conflict and retain the hold. Only the local Host can discard these local files, including a new file with no synced counterpart. Existing conflict holds retain their original evidence. Renamed and recreated files keep independent holds. A conflicting held note can explicitly keep both versions; status and hold notifications remain current.

Rebase and SDK size follow-up:

  • Rebased onto 8ef11cde454f6fc45a3e86f42c328fca328551b4, including Fix daemon sync progress and connection status #1042 sync status/progress, Prepare collection mode controller and service readiness #1033 collection-mode readiness, and fix: propagate deleted collections to joined computers #1041 collection-deletion propagation. The last rebase resolved only adjacent desktop tests and SDK test imports; a range comparison confirms the Viewer production patches replayed unchanged. Ordinary edits keep the bounded daemon queue and indexed pending lookup. Viewer and held-path text observations are partitioned before catalog preparation and become durable holds before the scan returns, preventing an upgrade between scan and drain from silently submitting them.
  • Hold commits share one hold snapshot and catalog, with no per-file full-hold reads. Commits are bounded by 64 observation acknowledgements or about 256 KiB of hold text; move halves stay atomic. Attachments remain outside the text-hold batch. Existing deferred-token ownership handles partial failures. A 193-file regression checks four commits, one notification, constant hold reads, and protection after upgrade.
  • read_only is registered in the user-facing problem catalog. Late receipt/await regressions isolate the receipt from valid automatic status pushes, retaining the existing retired-session guard. Stable hello identity is preserved through direct grant field comparisons, including UUID-bearing grants.
  • Viewer-only action/copy trims preserve shared hold text and the guidance to ask the owner for edit access. These safe trims still narrowly exceed the old 196 KiB root-entry regression tripwire. The root ceiling is 197 KiB, with a measured-size comment; the thin client remains at 68 KiB. This follows the size script's measured-size-plus-margin policy for standalone apps and avoids weakening handover correctness or actionable error guidance. The commit records the same justification.

Validation on final rebased source:

  • rcargo build --locked -p mdbn-daemon passed. All 45 replica disk tests passed on the final runtime, including Fix daemon sync progress and connection status #1042's scan/batching regressions and Viewer explicit-choice holds. Replica all-target Clippy with warnings denied and runtime architecture (27 crates) passed.
  • The full SDK check ran through rpnpm under Node 24.13.1 using fresh WASM built by rcargo from the exact final runtime: all 167 Vitest files passed (2,111 tests; one Git-history-dependent test skipped because the builder omits .git), plus all 20 Node tests. SDK typecheck and build passed.
  • npm run size passed: root SDK 196.1 KiB gzip / 757.6 KiB raw against the justified 197 KiB ceiling; thin client 67.3 KiB gzip against the unchanged 68 KiB ceiling.
  • ci:local --node passed build, typechecks, tests, generated/release checks, dependency audit, and package audit. This includes all 888 Editor tests. Its only failing step was architecture, because the remote runner omits Git metadata; the final local architecture check passes. The fresh desktop renderer build and all 19 isolated mocked Chromium UX tests passed, including both the deleted-collection/skipped-file notice and explicit Viewer discard confirmation.
  • SDK/client API inventories were regenerated after the last source conflict with no diff. Final local architecture, changelog (37 fragments), runtime formatting, and whitespace checks passed. Both validation snapshots match all 2,678 tracked final runtime files.
  • The broader root ci:local --rust attempt on the preceding rebase encountered Rust 1.99 Clippy warnings in unchanged files and a linker failure reporting failed to write to an output file. Disk full? / Bus error. Root Rust sources and its CI harness are identical across the final rebase. A separate root workspace/all-target Clippy run with CI's Rust 1.94 and warnings denied passed. These limitations are not clean-main rerun claims.
  • The local E2E entry remains unqualified: the earlier pnpm e2e --no-prepare attempt stopped at the missing local client dist/crypto-entry.js prerequisite. No production or personal daemon/desktop was used. Full E2E/artifact qualification is still a release prerequisite.
  • Independent Opus source reviews were completed and material findings corrected. The final recheck found no remaining correctness bugs in Viewer batching/recovery/catalog handling or SDK grant identity/hold presentation. It was source-only; execution evidence is listed separately above.

Follow-ups / release prerequisites:

  • Publish and repin the SDK/native artifact used by Editor. Editor currently pins beta.141; the complete live native role/status behavior, including upgrade from a Viewer Host hello, requires the new runtime and codec. This PR does not replace production trust assets or claim qualification of the pinned runtime.
  • Extend durable explicit-choice protection to unuploaded binary/oversized attachments and attachment replacements of text holds. These need actual attachment descriptors/storage; this patch does not invent them.
  • Reconcile existing pending backlogs from older clients and writes captured before a downgrade reaches the local signed policy. This PR prevents new captures under a confirmed Viewer role; it does not rewrite pending journals or claim to fix ambiguous prior effects.
  • KeepBoth applies to local notes; helper/Desktop action metadata does not yet distinguish definitions or local deletions, where that action returns a specific refusal. Copy conflicting definition text before discard and reapply it as a deliberate edit. Moves preserve source/destination bytes as separate holds; submitting both halves remains two explicit choices. Submitting a move as deletion plus creation does not preserve the note record ID. Identity-preserving held moves need original-path provenance in the hold representation and are a follow-up.

@callumalpass
callumalpass marked this pull request as ready for review October 29, 2026 14:48
@callumalpass
callumalpass added this pull request to the merge queue Oct 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 29, 2026
@callumalpass
callumalpass force-pushed the fix/viewer-write-refusal branch from 8ab0f32 to 6e95e95 Compare October 29, 2026 16:49
Partition refused definitions before catalog preparation, share hold lookups, and bound hold commits by count and text bytes. Keep ordinary edits in the daemon queue and retain original observation ownership on failures.

Keep stable grant identity and actionable shared error guidance. Safe Viewer-only copy and action-loop trims still leave the application SDK entry slightly above its 196 KiB regression tripwire. Set the measured-size ceiling to 197 KiB rather than weakening copy or handover correctness; the thin client remains capped at 68 KiB.
@callumalpass
callumalpass force-pushed the fix/viewer-write-refusal branch from dd37121 to 76d642d Compare October 29, 2026 18:42
@callumalpass
callumalpass added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit b816171 Oct 29, 2026
49 checks passed
@callumalpass
callumalpass deleted the fix/viewer-write-refusal branch October 29, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant