Skip to content

FW Lite plugins MVP: resource-backed storage, hardened API v1, research-backed trust UX - #2432

Draft
myieye wants to merge 17 commits into
developfrom
claude/plugins-mvp-review-y9lx01
Draft

FW Lite plugins MVP: resource-backed storage, hardened API v1, research-backed trust UX#2432
myieye wants to merge 17 commits into
developfrom
claude/plugins-mvp-review-y9lx01

Conversation

@myieye

@myieye myieye commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

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, stable window.fwlite API.

Persistence. Plugin HTML no longer lives as a text column in the CRDT. The Plugin entity 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 under LinkedFiles/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 / -requires metas) 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 edit permission — 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. updateEntry gets compare-and-swap semantics (new conflict error code): before must match the entry's current state, which makes the approval dialog provably show the write's real effect — previously a plugin could fabricate before to 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

  • Plugin files upload into 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 new LcmFileMetadata.LinkedFilesSubfolder field.
  • Old plugin resource files are never deleted (orphaned versions accumulate in LinkedFiles, same as replaced media) — GC is a follow-up.
  • Trust/consent grants are deliberately device-local localStorage (a teammate's click must not authorize code on your machine).
  • Serialization of the redefined change classes is covered by regenerated regression snapshots.
  • Interaction notes with the media-manager PR are in a comment below.

Tested: LcmCrdt.Tests plugin + media-subfolder + serialization/snapshot suites, dotnet ef migrations has-pending-model-changes clean, 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 new tests/ui suite after merging develop).

Plugins list Plugin cards with capability badges Consent First-run orientation card
Write confirm Write approval with human-readable diff + Always allow Trusted Trusted plugin: toolbar indicator + ambient notification
AI prompt AI prompt generator Gallery Example gallery
Stats Dictionary Stats example Galaxy Lexicon Galaxy example

(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

myieye and others added 10 commits July 7, 2026 22:44
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
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
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5fbb218a-b358-43e6-97d6-9387e240767a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/plugins-mvp-review-y9lx01

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the 💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related label Jul 10, 2026
claude added 2 commits July 10, 2026 23:12
…-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
@argos-ci

argos-ci Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Awaiting the start of a new Argos build…

@myieye

myieye commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

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
@myieye

myieye commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

Interaction notes: plugins × media manager (#2358) — from a review of both branches, for whoever merges second:

  • Guaranteed conflicts in LcmMediaService.SaveFile/UploadResource, IMediaServerClient (this PR adds linkedFilesSubfolderOverride to UploadFile; Media manager #2358 makes the interface internal, adds GetFileMetadata, changes MediaUploadFileResponse), and LcmFileMetadata (Media manager #2358 adds SizeInBytes, this PR adds LinkedFilesSubfolder) — union all three.
  • Simplification after Media manager #2358: its Harmony rc.211 bump persists resource metadata and passes it to UploadResource even for pending uploads — so the cache-path carrier for the subfolder in this PR's LcmMediaService can be deleted and metadata.LinkedFilesSubfolder passed directly. It also makes the subfolder CRDT-synced, giving the media manager a clean key to group or hide plugin files (today they'd show as opaque plugin-<hash>.html rows with no preview — worth a "Show app files" toggle or a Plugins group there).
  • Merge landmine: Media manager #2358's UploadResource reconstructs LcmFileMetadata positionally from the server response — LinkedFilesSubfolder would be silently dropped; use a with {} copy instead.
  • When delete lands in the media manager (primitives exist, UI doesn't yet): deleting a plugin's backing file would dangle the Plugin entity's fileUri — that flow needs a referenced-by check or should exclude the Plugins/ subfolder.

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}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: the commit is eecd1c3 (not 89f0d287).


Generated by Claude Code

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
Comment thread frontend/viewer/src/lib/plugins/plugin-srcdoc.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants