FW Lite plugins MVP: resource-backed storage, hardened API v1, research-backed trust UX - #2432
FW Lite plugins MVP: resource-backed storage, hardened API v1, research-backed trust UX#2432myieye wants to merge 17 commits into
Conversation
Plugins are single-HTML-file extensions stored as CRDT entities (manager-only writes, never synced to FLEx), run in an opaque-origin sandboxed iframe with a postMessage RPC bridge. API v1: reads + per-operation user-approved createEntry/updateEntry, per-plugin storage, offline-by-default CSP with a declarative internet permission, per-content-hash run consent. Includes a generated per-project AI prompt for authoring plugins, three example plugins, and Playwright/unit/backend test coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New bundled examples (lazy-loaded so they stay out of the main chunk): Dictionary Preview (typeset printable book page), Lexicon Galaxy (dictionary as an interactive night sky), Orthography Change Machine (spelling-reform rules with per-entry approval queue), Word Harvest Bingo (live workshop scoreboard), Sentence Sprint (example-sentence gap-filling queue with streaks). Host features: export/import/duplicate plugins, iframe fullscreen toggle, one-shot 'Open in plugin' entry launch context (fwlite.context.entryId). Structured filters gain missingGlossWs, missingExampleWs and missingPartOfSpeech (mirroring the task-view gridify expressions); the AI prompt documents them and canonicalizes the palette/dark-mode pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add plugin Description (CRDT migration + validator + API surface), broaden the plugin API/SDK, and add many new example plugins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t of the DB Plugin entities now carry only metadata (name, description, extracted manifest, file reference, size); the HTML uploads as an immutable content-named media file, giving offline-pending upload, download on demand, hg send/receive sync, and FLEx visibility for free. Reverts the SignalR message-size bump. Adds the edit permission, host capability gating for comments/history, compare-and-swap updateEntry semantics, write-trust (Always allow) with hash pinning, and the security fixes from the sandbox review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018yusi3xgjHHaA9w18itSeD
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018yusi3xgjHHaA9w18itSeD
The write-approval dialog keeps complete recursive coverage but renders the app's field names (Sense 1 Gloss (en)) instead of developer paths. Fixes from review: $state.raw for postMessage-bound state, host attach/detach instance capture, plugin identity keying so list refetches don't tear down a running plugin, launch context via reactive location, validator null guard, update-path manifest test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018yusi3xgjHHaA9w18itSeD
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018yusi3xgjHHaA9w18itSeD
This reverts commit a509dd0.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…-review-y9lx01 # Conflicts: # frontend/viewer/src/locales/en.po # frontend/viewer/src/locales/es.po # frontend/viewer/src/locales/fr.po # frontend/viewer/src/locales/id.po # frontend/viewer/src/locales/ko.po # frontend/viewer/src/locales/ms.po # frontend/viewer/src/locales/sw.po # frontend/viewer/src/locales/vi.po
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018yusi3xgjHHaA9w18itSeD
|
The latest updates on your projects. Learn more about Argos notifications ↗︎ Awaiting the start of a new Argos build… |
|
The 4 Argos diffs are the expected ones: all four project-view snapshots (default + entry-selected, light + dark) include the sidebar, which gains the new Plugins nav item in the demo project. No other visual changes. Generated by Claude Code |
The upload can run long after SaveFile (pending resources flush when back online), so the subfolder rides in the file's local cache path and is derived at upload time. Once the media-manager branch lands Harmony resource metadata, the path carrier can be dropped in favor of the LcmFileMetadata field this adds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018yusi3xgjHHaA9w18itSeD
|
Interaction notes: plugins × media manager (#2358) — from a review of both branches, for whoever merges second:
Also: the Argos e2e diffs are the same expected change as the default build — snapshots that include the sidebar now show the Plugins nav item. Generated by Claude Code |
| <iframe | ||
| bind:this={iframeElement} | ||
| title={plugin.name} | ||
| {srcdoc} |
There was a problem hiding this comment.
this results in using a bunch of extra memory. Ideally we would load the doc like this:
// Point to a blank slate first
iframe.src = 'about:blank';
iframe.onload = () => {
const doc = iframe.contentDocument;
doc.open();
doc.write(largeHtmlString);
doc.close();
// largeHtmlString can now be safely GC'd if no other references exist
};most importantly though, we don't want to store the html string anywhere, we want the GC to be able to reclaim that memory.
There was a problem hiding this comment.
Fixed in 89f0d287 — with one deviation from the suggested mechanism: document.write needs iframe.contentDocument, and the sandbox deliberately omits allow-same-origin, so the plugin frame is opaque-origin and the parent can't touch its document. The equivalent that keeps the sandbox is a Blob URL: the composed document goes into a Blob the iframe loads via src, so neither the raw HTML nor the srcdoc string is retained anywhere — state now holds only {manifest, hash, docUrl}, the strings are GC-able as soon as the load scope ends, and the URL is revoked when the state is replaced or the view unmounts. Reload reuses the same Blob; the on-disk resource cache stays the source of truth. Verified the navigation-teardown load counter doesn't false-positive on src-based loading (all 6 core flows + the 27-example sweep pass).
Generated by Claude Code
There was a problem hiding this comment.
Keeps multi-MB plugin HTML out of the retained state and the DOM attribute so it can be GC'd once the document is loaded; the on-disk resource cache stays the source of truth. srcdoc-style document.write isn't available here because the opaque-origin sandbox blocks contentDocument access. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018yusi3xgjHHaA9w18itSeD
Builds on the plugins MVP branch (
claude/nice-swirles-1986fb) and reworks it into its shippable shape. A plugin remains one self-contained HTML file running in an opaque-origin sandboxed iframe against a small, stablewindow.fwliteAPI.Persistence. Plugin HTML no longer lives as a text column in the CRDT. The
Pluginentity carries only metadata (name, description, declared manifest, file size,MediaUri); the HTML is an immutable, content-hash-named media resource on the same path audio/pictures use — offline-pending upload, download on demand, synced through hg send/receive underLinkedFiles/Plugins/, visible (inert) to FLEx. Edits upload a new file and repoint the entity, so cached/synced copies can never go stale. Since nothing shipped, the two plugin migrations are squashed into one and the change classes redefined; the 3 MB SignalR message-size bump is reverted because HTML no longer travels inline. The manifest (fwlite-plugin-permissions/-contexts/-requiresmetas) is extracted into the entity at save time so lists and the entry menu never need the file, while consent and the sandbox always re-parse the actual HTML.API v1 hardening. A compile-time-checked interface + arg-decoder table replaces the string-switch dispatch. Writes now require a declared
editpermission — a read-only plugin can never even pop a write dialog. Comments/history become capability-gated areas a host may not support (a future FieldWorks host won't), advertised in the init message and declarable as requirements so hosts can warn before launch.updateEntrygets compare-and-swap semantics (newconflicterror code):beforemust match the entry's current state, which makes the approval dialog provably show the write's real effect — previously a plugin could fabricatebeforeto disguise what it was overwriting.Trust UX (per permission-fatigue research: VS Code workspace trust, Android/iOS runtime permissions, Office trusted documents, NN/g on dialog habituation): the write dialog gains Always allow for this plugin, pinned to a content hash per project + device; trusted writes surface as ambient notifications instead of dialogs; the grant survives your own local edits but downgrades to ask-again when changed code arrives via sync; revocation is one tap from the plugin card or the run toolbar. Creating a plugin yourself counts as run consent. Write summaries are now complete recursive diffs (a hand-picked field list could be smuggled past), rendered with the app's own field names ("Sense 1 Gloss (en)"), bidi/control characters stripped, no silent truncation.
Sandbox honesty. A frame can always navigate itself (no CSP directive or sandbox flag stops it), so the offline guarantee was overclaimed. The consent card now says "not allowed to use the internet" rather than "no internet access", and the run view detects a plugin leaving its document, stops it, and tells the user.
Reviewer notes / follow-ups
LinkedFiles/Plugins/<fileId>/plugin-<hash>.html. Since deferred (pending) uploads can't carry metadata on current Harmony, the subfolder rides in the file's local cache path and is derived at upload time; once the media-manager branch (Media manager #2358, Harmony resource metadata) lands, that carrier can be dropped in favor of the newLcmFileMetadata.LinkedFilesSubfolderfield.Tested:
LcmCrdt.Testsplugin + media-subfolder + serialization/snapshot suites,dotnet ef migrations has-pending-model-changesclean, 62 plugin vitest tests, svelte-check/eslint clean, 6 core Playwright flows, and all 27 example plugins run against the demo project (migrated into the newtests/uisuite after merging develop).(Screenshots live in commit a509dd0, which is reverted on the branch so they don't ship.)
🤖 Generated with Claude Code
https://claude.ai/code/session_018yusi3xgjHHaA9w18itSeD