| name | change-plugin-runtime |
| description | Add, modify, or review Synergy Plugin API 4 definitions, generated manifests, plugin-kit builds, installation transactions, server-authoritative approval reviews, process runtimes, operations/events/hooks, marketplace behavior, or trusted UI contribution lifecycle. |
Change the Plugin Runtime
Trace the Single Contract
- Read Plugin documentation and the focused contract for the affected area.
- Start with source types in
packages/plugin, then plugin-kit build output, host discovery/install under packages/synergy/src/plugin, runtime generation/dispatch under plugin-runtime, server routes, and the Web host under packages/app/src/plugin.
- Trace
definePlugin() → generated manifest/artifacts → metadata-only validation → approval review → installation transaction → contribution adapter → lazy runtime generation → invocation context/Host Service → disposer or lifecycle cleanup.
- Load
change-execution-boundaries for host capability enforcement, change-server-api for routes/SDK, change-persistence for lock/approval/config migration, and develop-frontend for the Web host.
Preserve the Architecture
definePlugin() is the only source of identity, compatibility, capabilities, contributions, and handlers. Do not add a source manifest, handler map, or nested permission tree. Preserve the frozen API-family decoder and its single boundary adapter rather than adding runtime compatibility branches.
- Keep plugin ID identical across package name, artifact basename, manifest, registry, lockfile, approval, runtime generation, asset URL, and UI surface namespace.
manifest.name is display text and must not participate in identity checks or artifact naming.
- Validate generated metadata, paths, hashes, and approval before importing executable code. Derive manifest and permissions hashes only through
@ericsanchezok/synergy-plugin/integrity; plugin-kit, marketplace publication, installation, and approval must not maintain private payload or canonical JSON implementations. The manifest hash protects identity/integrity and review freshness; user authorization is the structured publisher/access grant. Approval reviews are server-authoritative: clients fetch the current review, submit only target plus opaque reviewToken, and rely on the server to bind the canonical target to the current manifest hash, permissions hash, source, and signer. Stale reviews return a refreshed review without writes.
- Treat API4 as the stable family: do not delete, rename, narrow, or change the meaning of stable public fields, contribution kinds, Host Services, hooks, or types. Plugin and plugin-kit npm package versions follow the Synergy product release version, while additive releases keep
apiVersion: "4.0"; plugins raise compatibility.synergy only when using a newer host addition. Keep deprecated stable APIs implemented and typed. experimental.* is excluded from this promise.
Adding a Host Service method, hook point, or contribution kind is not by itself a transport change. Keep the runtime protocol version unchanged unless the IPC envelope or handshake semantics actually change. Any real protocol revision requires a frozen executable artifact from the previous release, a host compatibility test, and an explicit negotiation or transition strategy before implementation. The running host owns the selected protocol version. A process Runner echoes the activation value in ready rather than re-reading a mutable source-tree constant, so plugin-only development reloads cannot mix a stale host selection with newer Runner files.
- Reconfirm only a first non-policy install, added/broadened access, unknown constraint change, or publisher/source change. Equal/narrower same-publisher updates refresh the grant silently. Do not compute or present an overall plugin risk rating. Keep runtime capability/permission/sandbox enforcement intact.
- Keep contribution kinds flat and adapter-owned. Adding a kind means adding its public type/schema, adapter, validation, lifecycle disposal, and tests—not a branch in a central registration loop.
- Treat generated operation, event, and tool JSON Schema as canonical metadata. Plugin-kit must convert live Zod schemas inside the definition-loader child before the descriptor crosses its JSON process boundary; never serialize Zod internals or attempt to reconstruct them in the parent. Reject schemas such as transforms that cannot be represented as JSON Schema during build. Tool inputs must be top-level objects, and AJV-backed execution validation must not round-trip schemas through Zod. Settings-gated tools are filtered for the current Scope and checked again at dispatch. Settings-gated MCP contributions use the same schema-default-aware condition contract and atomically refresh the plugin's complete MCP server set when settings change.
- External plugins use
process; only trusted built-ins may use inProcess. Do not restore worker mode or describe the process boundary as an OS sandbox.
- Classify invocation termination by the owner that triggered it, not by the shape of
AbortSignal.reason. Only the runtime manager's own deadline is TIMEOUT and may contain a hung external process; an upstream abort is CANCELLED even when its reason is a TimeoutError, and must reach the active handler before ordinary generation cleanup.
- One active generation is shared across enabled Scopes. Inject Scope/Session per invocation and reject stale-generation responses.
- Expose Synergy internals only through capability-gated Host Services. Do not pass a raw SDK client, server URL, token, or mutable current Scope into plugin code.
- Extend the existing host subsystem for every contribution. Agent contributions enter the native Agent registry; delegated work enters native Cortex and child Sessions; tools, settings, and UI enter their host registries. Never add a plugin-local Agent registry, scheduler, task lifecycle, transcript store, permission model, or renderer beside the host implementation.
- Keep an Agent contribution's manifest
id separate from its public agent.name. Registry lookup, delegation, and Agent Host Service calls use agent.name. Name collisions fail closed: built-in and configured Agents take priority over plugin contributions, plugin contributions are first-wins in stable discovery order, and later duplicates never replace the registered Agent.
- Keep prompt/native-task exposure separate from host-owned invocation. A private plugin Agent uses
hidden: true; the owner plugin may launch it only after plugin ID, generation, declared contribution, and task.delegate allowlist checks. Non-owned targets retain ordinary Agent visibility.
- Keep Host Service capability approval separate from runtime permission evaluation. Plugin installation and contribution authorization always use the original manifest capability ID. Tool admission may map a Host capability to a control-profile capability only through an explicit owner-reviewed mapping; unknown mappings remain conservative. For delegated work, validate manifest capability
task.delegate, map Tool admission to task, then evaluate the concrete Host call through control-profile permission task. Preserve structured Host Service error codes across runtime IPC.
- Preserve optional structured log
details end to end for both process and trusted inProcess runtimes, including error code and reason. Count the complete entry, including details, against the log rate limit's byte budget.
- Treat
asset.write, shell.execute, and settings.write as narrow Host Service contracts. Assets become host-owned attachments whose asset:// URL and localPath identify the durable Asset copy, shell execution accepts argv rather than shell source and still crosses ordinary permission/sandbox enforcement, and settings writes remain plugin- and Scope-bound.
- Keep plugin MCP contributions declarative and validate them with the shared strict local/remote schemas. Resolve them under stable
${pluginId}::${contributionId} identities, serialize OAuth mutation and cleanup by that identity, and replace a plugin's complete MCP server set atomically only after every candidate validates.
- Keep plugin CLI commands flat under
synergy <pluginId> <command>. Keep command discovery and help free of Scope-registration side effects, resolve the persistent Scope only when a declared command executes, parse declared options in the host, invoke the contribution through the normal runtime context, and use Host Services rather than direct host internals.
- Keep operations finite and schema-validated. Use declared events for invalidation; do not add a generic plugin Job or business-data store.
- Use host-declared observer/transform/guard hook points with deterministic ordering and contribution-level degradation.
- For trusted UI, enforce approval, UI API major, plugin-kit Solid compilation, host runtime linking, named exports, artifact hash, Scope/Session context, and one disposer per registration. Resolve every plugin asset route against the active normalized server URL so remote origins and deployment path prefixes such as
/proxy/<port> survive for trusted UI imports, themes, and icons; never load a root-relative plugin asset URL directly from the browser. Resource identity includes opaque id/title/state; reuse the same panel/resource tab and keep distinct resources separate. Renderer registry installation, replacement, lazy completion, and disposal must all invalidate mounted resolvers; keep stale-loader identity guards so a draining generation cannot replace the current renderer. Keep themes and icons as validated, namespaced data contributions; themes use the shared structured JSON schema, never arbitrary CSS (trusted component CSS is the one exception: plugin-kit extracts imported stylesheets to a sibling ui/index.css of the UI bundle and the Web host injects that stylesheet as a <link> during surface registration, removing it on reload, disable, or uninstall; probe the sibling with a 404-tolerant fetch from uiArtifact.entry and keep failure per-plugin so one bad stylesheet does not drop other assets). Theme build, validate, and dev paths share @ericsanchezok/synergy-plugin/theme, validate source and packaged JSON, include declarative asset content in generation identity, and publish a complete theme registry generation atomically.
- Preserve transactional install/update/remove rollback and explicit lifecycle failure semantics. Configured approval uses the existing transaction, rollback, and reload path; registry approval completes install/update through the existing upsert transaction. Synergy must not guess how to migrate or delete plugin-owned business data.
23a. Keep
lifecycle.install exactly-once per installation. Persist delivery state on the lockfile entry (lifecycleInstall: pending / completed / failed, absent for plugins without the contribution). Inside a host process deliver immediately after the transaction commits; outside a host process (standalone CLI) leave pending and deliver at next boot before the runtime.started broadcast or at the next plugin runtime reload (with catch-up, since reload has no broadcast). Never re-run a completed install: freshInstall requires no loaded plugin AND no lockfile entry (legacy entries installed before tracking ran the hook synchronously), and updates preserve the recorded state. Failed installs are never retried automatically; synergy plugin retry-install <id> re-queues only failed/pending entries and rejects generation mismatches with PluginInstallLifecycleGenerationMismatchError. Skip entries whose delivery is in flight so a config-watcher reload cannot interrupt a running hook.
- Resolve a targeted configured update from the canonical lockfile identity before loading candidate manifests. A broken unrelated configured spec must not prevent updating the requested plugin; the all-plugin update path remains responsible for the complete configured set.
- Composer, selection, text-action, and message-slot contributions adapt the App's native controllers and registries. Capability-gate each specialized surface context, remount it by Scope/Session/generation ownership, and keep plugin business panels and result protocols outside Core.
- Sessionless plugin inference uses the existing Host Service RPC and Core
AgentCall; require owned hidden Agents or an explicit allowlist and enforce hard runtime/input/output ceilings. Directed terminal delivery is exactly-once best effort: log only stable redacted delivery status and handler counts, release capacity regardless, and leave durable recovery and explicit retry to the plugin's business state. Never expose provider/model selection, tools, raw LLM streams, or a private plugin inference runtime.
- Keep compiler dependencies reachable from the packaged Synergy CLI statically analyzable so Bun includes them in standalone executables. A package dependency in
node_modules is not sufficient for runtime require() from /$bunfs.
- Give every external generation one owned memory monitor and stop it on every lifecycle exit. A memory-limit callback may restart only its exact active registry generation with the same manifest and limits; never let a stale callback stop or replace a newer generation. Attribute trusted
inProcess allocations to the Control Plane rather than double-counting plugin RSS.
Verify
- Add or update behavior tests at the owning boundary: descriptor/schema, plugin-kit build/validate/pack/sign, metadata-only discovery, approval, transaction rollback, runtime generation, operation/event/hook contract, server route, or Web registration lifecycle. For install-lifecycle changes cover fresh install, legacy lockfile entry (no
lifecycleInstall), update preservation, offline CLI pending, in-host delivery, boot/reload catch-up, in-flight skip, retry completed guard, and generation mismatch. Hash contract changes require a fixed public hash vector plus a real plugin-kit sign/registry-entry to host-verification test.
- Cover duplicate IDs, undeclared capabilities, handler mismatch, invalid schemas/hashes, disabled Scope, timeout/cancel/crash, stale generation, trusted UI export/runtime mismatch, upgrade failure, and force uninstall when relevant.
- For runtime memory changes, cover startup/stop ownership, the measured recycle effect, and a stale generation firing after an atomic replacement.
- Run public package typecheck/build, inspect a packed artifact, and verify a compiled standalone executable can invoke compiler-backed commands. Run focused host/Web tests, regenerate OpenAPI/SDK or config schema when their sources change, and finish with
bun run quality:quick.
- Update the canonical plugin docs and this Skill in the same change. Delete obsolete guidance instead of appending migration caveats to current-state docs.
Handoff
Report public contract changes, generated artifacts, capability/approval effects, runtime/generation behavior, Host Services, operation/event/hook behavior, UI lifecycle, transaction/migration effects, tests, and docs.