feat: runtime plugins for real — load from outside the binary, proven by a genuine plugin (goal 0249) - #515
Merged
Merged
Conversation
… in the plugin's hand internal/services/pluginsvc scans <data dir>/plugins/<id>/ for the converged manifest (id-equals-folder, enumerated capabilities, fail-closed on unknown ones), serves each valid plugin's js/css/json over the asset middleware (traversal-guarded, invalid plugins never serve), and carries the capability seam: RequestGuardedAction refuses an undeclared kind before any rule runs, evaluates a declared one through the guardrail core per action, and on approval performs the primitive itself (open-url, http/https only) -- the plugin never holds it. The frontend loads plugins BEFORE the app module graph evaluates (main.tsx dynamic-imports App and the aux windows), so the tool-list/ command-table snapshots include runtime registrations with zero late-registration machinery; a loadGate tripwire in atlasTools.ts makes a boot-order regression loud. Plugins hold ONE frozen api (src/plugins/hostApi.ts): registerCanvasObject (the ADR-0046 object concept as the contribution -- renderFace is a framework-agnostic DOM callback the host wraps in the one React mount), registerCommand (palette, never default-bound), and requestGuardedAction. The noun registry gains a third-party path (open string kinds; the built-in literal unions and their identity agreement check keep guarding built-ins), ObjectSource grows its url arm, placement is ONE generic branch (atlasThirdPartyPlacement.ts), and SetBoardObjectPayload is the new content-plane write door (undo round-trip tested, classified in the completeness table). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012im1JxQQV2ahnXzZDdVmZq
…n end to end
examples/plugins/mill-bookmark is the reference plugin: plain ESM, no
build step, copied-folder install. It contributes a Bookmark canvas
object on the object contract (source: url -- the ObjectSource arm's
first declarer), edits its address inline through the host's
content-plane door, and its Open button never touches a browser --
it requests the guarded open-url action.
Closing the gap that made the capability model unusable: a parked
guarded action now RENDERS in the Review queue
(ReviewGuardedActions.tsx; PendingGuardedActions/ResolveGuardedAction
are Wails-bound and the park/resolve emit the existing
pending-changed event) -- the render-alongside half docs/adr/0047 §5's
park always promised. Tray buttons fall back to a tool's own declared
label when no locale key exists (a plugin has no bundle to key into).
runtime-plugins.spec.ts proves the shipping artifact, not a stand-in:
the server boots with MILL_PLUGINS_DIR at examples/plugins, and the
suite drives dropped-folder -> tray entry -> placement -> plugin
renderFace -> payload edit -> reload persistence, then the guarded
loop: ask parks, Review lists it naming plugin:mill-bookmark, approve
wakes the blocked caller ("Opened."), deny reaches it as not-allowed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012im1JxQQV2ahnXzZDdVmZq
…, and the real SDK boundary Settings > Extensions gains an Installed plugins section: every folder in the plugins directory with its manifest metadata (name, version, author, description, declared capabilities), the same enable/disable switch built-ins carry, a load-error state naming the exact problem (no switch pretending a broken plugin could run), Open plugins folder, and Reload (plugins load at app start). Plugin tools are excluded from the compiled-in rows -- one richer row, never two. Docs: userdocs/reference/install-a-plugin.md is the user-facing story (install, capabilities, the activate contract, the shipping Bookmark example); extending-the-canvas.md's "no out-of-tree mechanism yet" paragraph -- now false -- points at it and reframes the compiled-in door honestly. The dependency-cruiser boundary now guards a REAL path: src/plugins/sdk.ts (what a plugin sees) may import nothing at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012im1JxQQV2ahnXzZDdVmZq
alicoding
enabled auto-merge (squash)
August 29, 2026 07:03
Resolves the two overlaps by taking main's structure wholesale and re-applying this branch's deltas in their new homes: the third-party noun path re-lands on main's split registry (its content half now lives in atlasBoardObjectContent.ts, widened to open string kinds there); the installed-plugins mount and third-party row exclusion re-land on the treatment's per-section ExtensionsSection; plugin nouns declare nounName (the treatment's noun-title convention). This branch's own gesture-types extraction is dropped -- main's split already brought the registry under the file limit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012im1JxQQV2ahnXzZDdVmZq
…all-story case The merge auto-resolved the bindings hub by taking main's regen, dropping PendingGuardedActions/ResolveGuardedAction -- restored the documented way (regenerate on rebase, never hand-merge). The Extensions install-story e2e case (broken-plugin fixture, per-test copy of examples/plugins) also lands here; it had missed the earlier commit's staged paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012im1JxQQV2ahnXzZDdVmZq
…ontract without the side effects The dynamic-import boot (two commits back) had two measured costs: the App chunk's CSS re-entered the cascade after mill-tokens.css (breaking the load-order tie its header documents -- the teal accent test caught it), and first-paint moved behind the plugin await, so a keypress racing the shell landed on nothing. This restores the fully static module graph (CSS order and chunking exactly as before) and instead makes the three module-scope registry snapshots LAZY (shared/lazySnapshot.ts): ATLAS_TOOLS, COMMANDS, and the Extensions rows materialize on first ACCESS -- always a render- or event-time read, after plugin activation -- with the boot tripwire moved into the builder so a premature materialization stays loud. The specs that pressed a hotkey the instant goto resolved worked only by accident of synchronous eval; the shell now paints after a short async boot, so their first press waits for the painted nav -- the honest user-primitive precondition, applied to all five sites as one class. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012im1JxQQV2ahnXzZDdVmZq
…g Open The Enter commit's payload write re-renders the plugin face, replacing its DOM; on a slower runner the Open click could land on the doomed pre-commit button, whose handler then wrote status into a detached element. Waiting for the commit's own observable (the derived title) means the click always hits the current elements. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012im1JxQQV2ahnXzZDdVmZq
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.
Un-gates ADR-0047 §4's out-of-tree loader (owner-directed). Orchestrator-authored end to end per the #514 law. The audit found 8 oversold claims first ("do not trust the pattern"); every one is now closed:
The kernel (S1):
internal/services/pluginsvc: scans<data dir>/plugins/<id>/for the converged manifest (id-equals-folder, enumerated capabilities, fail-closed on unknown ones), serves plugin js/css/json over the asset middleware (traversal-guarded; invalid plugins never serve).RequestGuardedActionrefuses an undeclared kind before any rule runs, evaluates declared ones per action through feat: the guardrail gains its public request-an-action entry (ADR-0047 §5) #508's rule core, and on approval Mill performs the primitive itself (open-url, http/https only) — the plugin never holds it.activate(api):registerCanvasObject(the ADR-0046 object concept —renderFace(el, ctx)is a framework-agnostic DOM callback, no build step for authors),registerCommand, payload writes + guarded actions host-mediated.ObjectSourcegrows itsurlarm;SetBoardObjectPayloadis the new content-plane door (undo round-trip tested, completeness-classified).The proof (S3):
examples/plugins/mill-bookmark— plain ESM, copy the folder, reload, a Bookmark object exists. Also closed on the way: parked guarded actions now render in Review with approve/deny (ADR-0047 §5's "render alongside" half had never shipped — a plugin's ask was invisible and unapprovable).runtime-plugins.spec.ts(3/3) boots against a copy of examples/plugins and drives: copied folder → tray → placement → plugin render → payload edit → reload persistence → guarded ask → Review row namingplugin:mill-bookmark→ approve wakes the caller ("Opened.") → deny reaches it ("Not allowed").The install story (S2): Settings → Extensions gains Installed plugins — manifest metadata, declared capabilities, the same switch, error rows naming the exact problem, Open plugins folder + Reload. Live-verified + e2e-pinned including a broken-plugin fixture.
Records (S4): ADR-0047 amendment, SPEC §9.5,
userdocs/reference/install-a-plugin.md, the extending doc's now-false "no out-of-tree mechanism" corrected, and the cruiser boundary finally guards a real path (src/plugins/sdk.tsimports nothing).Proof of green: pluginsvc + atlassvc Go tests, vitest 121/121, runtime-plugins 3/3, full suite 560 passed.
🤖 Generated with Claude Code
https://claude.ai/code/session_012im1JxQQV2ahnXzZDdVmZq