feat: Chamber Plugin SPI for onboarding override#388
Open
patschmittdev wants to merge 7 commits into
Open
Conversation
Introduce a generic Service Provider Interface so a separate, trusted TypeScript package can override Chamber's base Genesis/onboarding flow without any enterprise-specific code living in Chamber. Contract (types only): @chamber/plugin-api exposes ChamberRendererPlugin, OnboardingProvider/OnboardingProps, ChamberMainPlugin, MainPluginContext, and defineRendererPlugin/defineMainPlugin helpers. Renderer path (build-time): a Vite virtual module `virtual:chamber-plugin` resolves to a no-op plugin by default, or to the entry named by CHAMBER_PLUGIN_RENDERER for enterprise builds. ChamberPluginProvider exposes the active plugin via useChamberPlugin(); GenesisGate renders `plugin.onboarding ?? GenesisFlow`, preserving default behavior. Main path (runtime): PluginHost dynamic-imports an optional trusted plugin named by config.chamberPlugin or CHAMBER_PLUGIN, calls registerMain(ctx) once after services/IPC are wired, and logs-and-swallows any failure so a broken plugin never blocks boot. AppConfig.chamberPlugin is normalized by ConfigService. Both paths default to no-op, so the base Chamber build is unchanged. Tests: plugin-api contract (expectTypeOf), Vite resolver, PluginHost load/no-op/failure isolation, GenesisGate fallback + plugin-override, ConfigService chamberPlugin normalization. typecheck, eslint, and deps:check are clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend the Chamber Plugin SPI so a plugin onboarding surface can create a mind
and seed an onboarding document into it, without touching Electron APIs.
- @chamber/plugin-api: OnboardingProps gains a typed `createMind` capability
plus OnboardingMindRequest ({ templateId, marketplaceId?, seedDocument? }) and
OnboardingMindResult. The built-in GenesisFlow remains a valid fallback (it
ignores the extra prop), so default onboarding is unchanged.
- New path-safe capability: seedOnboardingDocument writes a document to a fixed,
Chamber-owned path (.chamber/soul-code.md) inside a mind. The caller supplies
only content (no path-traversal surface); escape + symlink checks mirror
MindProfileService, and the write is atomic. Exposed via genesis:seedDocument
IPC + preload, gated through MindManager mind-path resolution.
- GenesisGate implements createMind: install the template, optionally seed the
document, then select the new mind (mirrors the built-in post-create flow).
Chamber owns all Electron access; the plugin only describes intent.
Tests: plugin-api contract for createMind/request/result; seedOnboardingDocument
unit tests (write, overwrite, empty, oversize, missing dir, symlink reject);
GenesisGate test that createMind installs + seeds + selects; ipc-channels
assertion for the new channel. Browser API + test mocks gain seedDocument.
Validated: tsc --noEmit, eslint, deps:check, and targeted vitest all green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Node's ESM dynamic import() rejects raw absolute paths, notably Windows
drive-letter paths like 'C:\foo\bar.js' (parsed as a 'c:' protocol). The
PluginHost docstring promises "package name or absolute path", but the
implementation silently no-op'd absolute paths because the importer's
throw was log-and-swallowed by the resilience contract.
This is the path enterprise consumers will most often use when
configuring 'chamberPlugin' to a fully-qualified local install location.
The renderer override surface was unaffected (Vite has its own
resolution); only the main-side dynamic import was broken.
Fix: normalize via pathToFileURL when path.isAbsolute(target) is true.
Bare package specifiers ('@scope/pkg', 'pkg') pass through unchanged so
npm resolution still works. Added two unit tests covering both branches.
Discovered via live end-to-end verification on Windows where a raw
absolute path in CHAMBER_PLUGIN silently bypassed registerMain. With the
fix, the same path correctly loads the plugin and invokes registerMain
with the expected MainPluginContext.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eed path Address two blocker findings from a Clean-Architecture audit of the Plugin SPI before it goes into public Chamber. Both are about contract honesty, not mechanics; no runtime behavior changes except the seed file name. H1 -- trusted-plugin framing, not a security boundary. The contract comments described the narrow handoff as a "security boundary" and said the plugin "never touches Electron APIs directly". Neither seam is sandboxed: a renderer plugin is bundled into the renderer and shares window.electronAPI; a main plugin is dynamic-imported with full Node/Electron privileges. Reword main-plugin.ts, PluginHost.ts, and renderer-plugin.ts to state the real controls plainly -- opt-in loading plus a default-narrow handoff, not containment. Keep the one genuinely isolated bit accurate: the canvas HTML served by serveOnboardingCanvas runs at a separate origin. H2 -- remove the leaked enterprise concept from the generic SPI. The seed destination was hardcoded to .chamber/soul-code.md and the docs used "Soul Code" / "enterprise agent" examples, even though nothing in Chamber (or the enterprise variant, plugin, or Pulse mind) reads that file. Rename the constant to the neutral .chamber/onboarding.md, document that Chamber writes but never reads it (a plugin's own template/agent consumes it), and drop the enterprise wording from the contract and the genesis IPC comment. Neutralize the document content in the tests. Verified: typecheck clean; eslint clean on changed files; the plugin-api contract, PluginHost, seedOnboardingDocument, and GenesisGate plugin tests pass (29 tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… state consistent
createMind previously returned early when seedOnboardingDocument failed,
without dispatching SET_MINDS / SET_ACTIVE_MIND. But the mind was already
created and set active in the main process, so the renderer store and main
diverged: a mind existed and was active in main yet was invisible in the UI.
Treat mind creation as the atomic deliverable and document seeding as
best-effort enrichment (seedDocument is already optional). On seed failure
createMind now still syncs the renderer to the main process and returns
{ success: true, mindId, seedError }. success:false is reserved for "no mind
was created" (the gate stays open). This also removes the prior "mindId on a
failure result" ambiguity.
Adds OnboardingMindResult.seedError to the public contract (optional, only
present on success) and locks it with a type-level test. Adds GenesisGate
tests for both failure paths: template-install failure (no sync, gate stays
open) and seed failure (mind kept + selected, renderer synced, seedError
surfaced).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add ai-docs/plugin-spi.md covering both seams (renderer virtual module plus CHAMBER_PLUGIN_RENDERER, main PluginHost plus chamberPlugin / CHAMBER_PLUGIN), the capabilities and the createMind result contract, the trusted-plugin model (opt-in plus a default-narrow handoff, not a sandbox), the .chamber/onboarding.md write-but-not-read seed behavior, and minimal renderer/main authoring examples. Make the @chamber/plugin-api consumption decision explicit in its package.json description: a workspace-internal, types-only contract resolved in source (matching @chamber/shared and @chamber/client), not published to npm. Add a CHANGELOG entry under Unreleased. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Follow-ups from the Plugin SPI audit (all non-blocking): - L1/L4: replace the duplicated, index-based plugin logger closures in the composition root with a single explicit level-mapping helper, so adding a PluginLogLevel can never silently throw on a missing logger method. - L3: extract the createMind orchestration out of GenesisGate into a useCreateMind hook (matching the renderer hooks/ convention), restoring SRP in the gate component. - L5: add trailing newlines to packages/plugin-api/package.json and tsconfig.json. - Test hygiene: scrub enterprise-specific fixtures (EnterpriseOnboarding, pulse, genesis-minds-enterprise) from the public GenesisGate plugin test in favor of neutral example identifiers, and neutralize the example comment in the main-process plugin wiring. Skipped: L2 (GenesisFlow receives an unused createMind prop) is a benign, type-safe ISP wrinkle whose only fix adds JSX branching that hurts readability. L6 was already resolved by the seed-failure contract change. Validated: typecheck, eslint, deps:check, actionlint, markdownlint all clean; 68 SPI/GenesisGate tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.
Summary
Adds a small Service Provider Interface (SPI) that lets a separate, trusted package override Chamber's built-in Genesis onboarding without any plugin-specific code living in Chamber. Both seams default to a no-op, so the base Chamber build is unchanged unless a plugin is configured.
The contract lives in a new workspace-internal package,
@chamber/plugin-api.Why
Chamber needs an extension seam so downstream or branded distributions can replace the first-run experience without forking the whole app or leaking proprietary code into Chamber. This PR adds the generic seam only; it ships no consumer.
The two seams
CHAMBER_PLUGIN_RENDERER(resolved by thevirtual:chamber-pluginVite module)chamberPluginconfig field orCHAMBER_PLUGINenv varGenesisGaterendersplugin.onboarding ?? GenesisFlow.PluginHostdynamic-imports the configured plugin after services and IPC are wired, callsregisterMainonce, and logs-and-swallows any failure so a broken plugin never blocks boot.Trust model
This is a trusted-plugin model, not a sandbox. The controls are (1) opt-in loading and (2) a default-narrow capability handoff. A renderer plugin is bundled into the renderer and a main plugin runs with full main-process privileges, so only configure packages you trust. The genuinely enforced boundaries elsewhere (the chatroom ApprovalGate, the Electron sandbox flags, and seed-document path validation) are unchanged.
Capabilities
A renderer onboarding component receives
OnboardingProps:onComplete,createMind, and optionalserveOnboardingCanvas. Chamber owns all Electron access; the plugin only describes intent.createMindtreats mind creation as atomic and document seeding as best-effort: on seed failure it still selects the new mind and returns{ success: true, mindId, seedError };success: falsemeans no mind was created. The optional seed document is written to a fixed, Chamber-owned path (.chamber/onboarding.md) that Chamber writes but never reads back.Consumption
@chamber/plugin-apiis a workspace-internal, types-only contract (private, raw-source export, matching@chamber/sharedand@chamber/client). It is resolved in source by Chamber and by trusted plugin packages built within the workspace, and is not published to npm.Testing
expectTypeOf),PluginHostload / no-op / failure-isolation, the Vite virtual-module resolver,ConfigServicenormalization,seedOnboardingDocumentpath-safety, andGenesisGatefallback + plugin-override + createMind failure-path tests.tsc,eslint, dependency-cruiser, actionlint, and markdownlint all clean.Docs
See
ai-docs/plugin-spi.mdfor both seams, the env vars and config, the trust model, thecreateMindcontract, and minimal renderer and main authoring examples.