Source spec: docs/plugins-spec.md (zh-CN: docs/plugins-spec.zh-CN.md).
Sibling docs: spec.md Β· skills-protocol.md Β· architecture.md.
Update protocol β read first
- This file is a living roadmap. Every PR that lands a chunk of the plugin system must flip the matching
[ ]to[x]in the same PR, and update Β§3 "Architecture state" if a new module / table / endpoint becomes real. - Do not edit
docs/plugins-spec.mdfrom this file's PRs except to fix factual drift; the spec is the contract, this file is the schedule. - The "Definition of done" gates in Β§8 are the only hard sign-off bar; an empty checkbox under a phase does not mean v1 is broken β only an empty checkbox under Β§8 does.
- When
docs/plugins-spec.mdpatches change phase numbering or atom names, mirror those changes here in the same PR (per Β§21.6 / Β§22.5 / Β§23.6 of the spec).
These are the five rules that decide every downstream design decision. They sit above phases and are checked by reviewers on every plugin-related PR.
- I1.
SKILL.mdis the floor;open-design.jsonis a sidecar; never bidirectionally couple.packages/plugin-runtime/adapters/agent-skill.tssynthesizes a schema-validPluginManifestfromSKILL.mdod:frontmatter (verified viapackages/plugin-runtime/tests/adapter-agent-skill.test.ts). The bundled e2e fixture underapps/daemon/tests/fixtures/plugin-fixtures/sample-plugin/ships both halves andapps/daemon/tests/plugins-e2e-fixture.test.tsexercises the merger. - I2. Apply is a pure function; side effects only after
POST /api/projects/POST /api/runs.apps/daemon/src/plugins/apply.tsis FS- and DB-free; the snapshot writer (snapshots.ts) and installer are the only modules that mutate persistent state.apps/daemon/tests/plugins-apply.test.tsasserts deterministic snapshots from the same inputs and refuses to touch the registry / FS. - I3.
AppliedPluginSnapshotis the only contract between "plugin" and "run".composeSystemPrompt()now accepts apluginBlockderived from the snapshot viapluginPromptBlock(snapshot)(apps/daemon/src/plugins/apply.ts); the run reads context through the snapshot. Plugin runs in web API-fallback mode are rejected at the HTTP layer (Phase 2A wires the 409); the snapshot table is the only writable surface for the contract. - I4. CLI is the canonical agent-facing API; UI mirrors CLI, not the other way round. Phase 1:
od plugin install/list/info/uninstall/apply/doctorand the matching/api/plugins/*HTTP routes ship in the same PR. Remainingod project/run/files/conversation/marketplacesubcommands roll in over Phase 1 / 2C / 3 PRs. - I5. Kernel/userspace boundary (spec Β§23) is drawn from day 1.
composeSystemPrompt()is structured as a pure assembler with a content table (DESIGN.md, craft, skill, plugin block, metadata); the newpluginBlockparameter slots in without restructuring. Phase 2A lifts the renderer intopackages/contracts/src/prompts/plugin-block.ts(PB1).
CI guard placement: each invariant must have at least one automated test that fails when the rule is violated. The test path is recorded next to the box when it lands.
packages/contracts/src/plugins/ β pure types + Zod schemas, no runtime deps
βββ manifest.ts β PluginManifest, GenUISurfaceSpec, PluginPipeline
βββ context.ts β ContextItem union (spec Β§5.2)
βββ apply.ts β ApplyResult, AppliedPluginSnapshot, InputFieldSpec
βββ marketplace.ts β MarketplaceManifest
βββ installed.ts β InstalledPluginRecord, TrustTier ('bundled' | 'trusted' | 'restricted')
βββ events.ts β GenUIEvent + pipeline_stage_* variants joined into PersistedAgentEvent
packages/plugin-runtime/ β pure TS; reusable in web / daemon / CI
βββ parsers/{manifest,marketplace,frontmatter}.ts
βββ adapters/{agent-skill,claude-plugin}.ts
βββ merge.ts β sidecar + adapter merge; open-design.json wins
βββ resolve.ts β ContextItem ref resolution (pure; no FS reads)
βββ validate.ts β JSON Schema validation
βββ digest.ts β manifestSourceDigest (frozen algorithm; CI fixtures)
apps/daemon/src/plugins/ β side-effect concentration zone
βββ registry.ts β three-tier scan + hot reload (existing skills.ts/design-systems.ts/craft.ts delegate here)
βββ installer.ts β github tarball / https / local / marketplace
βββ apply.ts β pure resolver; emits ApplyResult + draft snapshot
βββ snapshots.ts β Β§8.2.1 β the **only** writer to applied_plugin_snapshots
βββ pipeline.ts β Β§10.1 stage scheduler + Β§10.2 devloop + until evaluator
βββ connector-gate.ts β Β§9 capability gate, called by tool-tokens.ts and /api/tools/connectors/execute
βββ trust.ts β installed_plugins.capabilities_granted writer
βββ doctor.ts β schema + connector catalog + MCP dry-launch + atom refs
apps/daemon/src/genui/ β spec Β§10.3
βββ registry.ts
βββ events.ts
βββ store.ts β genui_surfaces table writer
Hard layering rules
packages/plugin-runtimedoes not importnode:fs. It receivesloader: (relpath) => Promise<string>. Daemon injects real FS, CI injects mocks, web preview sandbox injects fetch.apps/daemon/src/plugins/snapshots.tsis the only file that issuesINSERT/UPDATEagainstapplied_plugin_snapshots. CI guard:rg "applied_plugin_snapshots" --type ts -g '!**/*.test.ts'may matchINSERTonly insidesnapshots.ts.connector-gate.tsis a stateless validator ((snapshotId, connectorId) => allow | deny);tool-tokens.tscalls it before issuing a token, and/api/tools/connectors/executere-validates on every call to defeat token replacement.
This section tracks what exists in the repo today. Update in the same PR that lands the module; never let it lie about reality.
These notes capture the product/implementation answers that otherwise get lost between the spec and the code:
- No plugin selected does not mean a naked agent.
composeSystemPrompt()still always layers the Open Design base designer/discovery prompt, project metadata, active design system/craft, and daemon-owned safety/tooling guidance. Plugin context is additive: a selected plugin contributes snapshot-derived## Active plugin,## Plugin inputs, and active-stage atom blocks. Home free-form runs route through the bundled hiddenod-defaultscenario, which shapes task type and then returns to the normal design pipeline. - The pipeline is plugin-assembled, not a fixed wizard. The reference shorthand is
discovery -> plan -> generate -> critique, but the runnable shape comes fromod.pipeline.stages[].atoms[]on the applied plugin or bundled scenario fallback.apps/daemon/src/plugins/pipeline-runner.tsemits stage/GenUI events andpackages/contracts/src/prompts/atom-block.tsrenders the active stage body. Some atoms are still prompt fragments / permissive workers; observable atoms such asdiff-review,build-test, andhandoffnow emit durable files or signals. - GenUI is controlled rendering. Agents/plugins emit structured surface requests (
form,choice,confirmation,oauth-prompt) and OD renders them with product-owned React/CLI components. Inline<question-form>chat UI follows the same principle: parse structured data, render throughQuestionForm, and keep styling in OD. Plugin-bundled custom components are a separate sandboxed path behindgenui:custom-component. - AG-UI is interoperability, not the product UI runtime.
packages/agui-adapterandGET /api/runs/:runId/aguiare shipped so CopilotKit / AG-UI clients can consume an OD run. The internal web/desktop UI remains OD-native; adding CopilotKit itself is only justified for an explicit external embed/demo/client. - Scenario discovery still has one product gap.
apps/web/src/components/home-hero/chips.tsis a curated Home rail for high-frequency scenarios.apps/web/src/components/plugins-home/facets.tsis more data-driven and derives category/subcategory facets from plugin metadata. The desired next slice is a single scenario registry / manifest projection that feeds Home chips, plugin filters, composer tools, and@search.
| Path | Status | Notes |
|---|---|---|
packages/contracts/src/plugins/manifest.ts |
shipped | Phase 0 β Zod schema + PluginManifest type |
packages/contracts/src/plugins/context.ts |
shipped | Phase 0 β ContextItem, ResolvedContext |
packages/contracts/src/plugins/apply.ts |
shipped | Phase 0 β ApplyResult, AppliedPluginSnapshot, InputFieldSpec |
packages/contracts/src/plugins/marketplace.ts |
shipped | Phase 0 β MarketplaceManifest, TrustTier, MarketplaceTrust |
packages/contracts/src/plugins/installed.ts |
shipped | Phase 0 β InstalledPluginRecord, PluginSourceKind |
packages/contracts/src/plugins/events.ts |
shipped | Phase 0/2A β pipeline_stage_* and genui_* event variants used by daemon SSE / ND-JSON |
packages/contracts/src/prompts/plugin-block.ts |
shipped | Phase 2A (PB1); renderPluginBlock(snapshot) pure function shared by daemon + contracts composers |
packages/plugin-runtime/ |
shipped | Phase 1 β pure TS package: parsers, adapters, merge, resolve, validate, digest |
| Path | Status | Notes |
|---|---|---|
apps/daemon/src/skills.ts |
exists | Phase 1: independent loader; Phase 2A folds into plugins/registry.ts |
apps/daemon/src/design-systems.ts |
exists | same as above |
apps/daemon/src/craft.ts |
exists | same as above |
apps/daemon/src/connectors/ |
exists | reused as-is by connector-gate.ts |
apps/daemon/src/tool-tokens.ts |
exists | Phase 2A: wire to connector-gate.ts |
apps/daemon/src/prompts/system.ts |
shipped | Phase 1 β composeSystemPrompt() accepts pluginBlock derived from snapshot |
apps/daemon/src/server.ts |
shipped | Phase 1 β /api/plugins/*, /api/atoms, /api/applied-plugins/:snapshotId mounted |
apps/daemon/src/cli.ts |
shipped | Phase 1 β od plugin list/info/install/uninstall/apply/doctor |
apps/daemon/src/plugins/registry.ts |
shipped | Phase 1 β install root scan, manifest parse, SQLite reader/writer |
apps/daemon/src/plugins/installer.ts |
shipped | Phase 1 β local-folder install only; symlink + traversal + size guards |
apps/daemon/src/plugins/apply.ts |
shipped | Phase 1 β pure resolver; emits ApplyResult + draft snapshot |
apps/daemon/src/plugins/snapshots.ts |
shipped | Phase 1 β sole writer of applied_plugin_snapshots; PB2 expires_at stamping |
apps/daemon/src/plugins/atoms.ts |
shipped | Phase 1 β first-party atom catalog (spec Β§10) |
apps/daemon/src/plugins/connector-gate.ts |
shipped | Phase 2A β apply path connector resolution + token-issuance gate |
apps/daemon/src/plugins/pipeline.ts |
shipped | Phase 2A β devloop scheduler + until evaluator + OD_MAX_DEVLOOP_ITERATIONS |
apps/daemon/src/plugins/pipeline-runner.ts |
shipped | Phase 2A β runs pipeline against a live run, emits stage + GenUI events |
apps/daemon/src/plugins/resolve-snapshot.ts |
shipped | Phase 2A β snapshot resolver wired into POST /api/projects + /api/runs |
apps/daemon/src/plugins/marketplaces.ts |
shipped | Phase 3 β add / list / refresh / remove / trust + resolvePluginInMarketplaces |
apps/daemon/src/plugins/gc.ts |
shipped | Phase 5 (early) β snapshot GC worker + boot sweep |
apps/daemon/src/plugins/scaffold.ts |
shipped | Phase 4 β od plugin scaffold starter generator |
apps/daemon/src/plugins/export.ts |
shipped | Phase 4 β od plugin export <projectId> --as β¦ |
apps/daemon/src/plugins/publish.ts |
shipped | Phase 4 β od plugin publish --to <catalog> URL builder |
apps/daemon/src/plugins/bundled.ts |
shipped | Phase 4 (Β§23.3.5 entry slice) β boot walker for plugins/_official/** |
apps/daemon/src/plugins/atom-bodies.ts |
shipped | Phase 4 (Β§23.3.2 entry slice) β bundled-atom SKILL.md body loader |
apps/daemon/src/plugins/atoms/build-test.ts |
shipped | Phase 7 β typecheck + test shell-out runner; emits build.passing + tests.passing signals |
apps/daemon/src/plugins/atoms/code-import.ts |
shipped | Phase 7 β repo walker writing normalised <cwd>/code/index.json |
apps/daemon/src/plugins/atoms/design-extract.ts |
shipped | Phase 6/7 β token bag extractor reading code/index.json + writing code/tokens.json |
apps/daemon/src/plugins/atoms/figma-extract.ts |
shipped | Phase 6 β Figma REST shell-out β figma/{tree,tokens,meta}.json |
apps/daemon/src/plugins/atoms/token-map.ts |
shipped | Phase 6/7 β exact + normalised-hex + fuzzy-name crosswalk against the active design system |
apps/daemon/src/plugins/atoms/rewrite-plan.ts |
shipped | Phase 7 β heuristic ownership classifier + per-leaf step generator |
apps/daemon/src/plugins/atoms/patch-edit.ts |
shipped | Phase 7 β unified-diff applier with shell-tier safety gate + per-step receipts + atomic file writes |
apps/daemon/src/plugins/atoms/diff-review.ts |
shipped | Phase 7-8 β review/{diff.patch,summary.md,decision.json,meta.json} from receipts |
apps/daemon/src/plugins/atoms/auto-surfaces.ts |
shipped | Phase 8 β auto-derives __auto_diff_review_<stageId> choice surface for each stage that lists diff-review |
apps/daemon/src/plugins/atoms/diff-review-genui-bridge.ts |
shipped | Phase 8 β POST /api/runs/:id/genui/:surfaceId/respond \u2192 runDiffReview() decision update |
apps/daemon/src/plugins/atoms/handoff.ts |
shipped | Phase 8 β recordHandoff + isDeployableAppEligible + runHandoffAtom (pipeline-driven promotion ladder) + runAndPersistHandoff (<cwd>/handoff/manifest.json round-trip) |
apps/daemon/src/plugins/validate.ts |
shipped | Phase 4 β od plugin validate <folder> author-side lint helper |
apps/daemon/src/plugins/pack.ts |
shipped | Phase 4 β od plugin pack <folder> distribution archive helper |
apps/daemon/src/plugins/search.ts |
shipped | Phase 4 β searchInstalledPlugins helper backing od plugin list/search filters |
apps/daemon/src/plugins/diff.ts |
shipped | Phase 4 β diffPlugins helper backing od plugin diff <a> <b> |
apps/daemon/src/plugins/snapshot-diff.ts |
shipped | Phase 4 β diffSnapshots helper backing od plugin snapshots diff <a> <b> |
apps/daemon/src/plugins/stats.ts |
shipped | Phase 4 β pluginInventoryStats / snapshotInventoryStats helpers backing od plugin stats |
apps/daemon/src/plugins/simulate.ts |
shipped | Phase 4 β simulatePipeline / parseSignalKv helpers backing od plugin simulate |
apps/daemon/src/plugins/verify.ts |
shipped | Phase 4 β verifyPlugin orchestrator backing od plugin verify (CI meta-command) |
apps/daemon/src/storage/db-inspect.ts |
shipped | Phase 5 β inspectSqliteDatabase helper backing od daemon db status |
apps/daemon/src/plugins/events.ts |
shipped | Phase 4 β in-memory plugin event ring buffer + SSE feed backing od plugin events tail |
packages/plugin-runtime/src/pipeline-fallback.ts |
shipped | spec Β§23.3.3 β resolveAppliedPipeline falls back to a bundled scenario when od.pipeline is absent |
plugins/_official/atoms/<atom>/{SKILL.md,open-design.json} |
shipped | Phase 4 / 6 / 7 / 8 β 13 first-party atom plugins (4 implemented + 9 reserved fragments) |
plugins/_official/scenarios/<id>/{SKILL.md,open-design.json} |
shipped | Phase 4 (Β§23.3.3) β bundled scenario/router/export plugins, including the four taskKind defaults plus od-default Home free-form routing |
packages/agui-adapter/ |
shipped | Phase 4 β pure-TS AG-UI canonical event encoder |
packages/contracts/src/prompts/atom-block.ts |
shipped | Phase 4 β renderActiveStageBlock(stageId, bodies) pure renderer |
tools/pack/docker-compose.yml |
shipped | Phase 5 β hosted-mode reference manifest |
tools/pack/helm/open-design/templates/** |
shipped | Phase 5 β Deployment / Service / Secret / ConfigMap / PVCs / Ingress / NOTES |
tools/pack/helm/open-design/values-{aws,gcp,azure,aliyun,tencent,huawei,self}.yaml |
shipped | Phase 5 β per-cloud overrides (volume + ingress diffs) |
deploy/Dockerfile plugins/_official COPY |
shipped | Phase 5 β bundled atoms travel with the image |
.github/workflows/docker-image.yml |
shipped | Phase 5 β multi-arch ghcr.io push (:edge / :version) |
apps/daemon/src/storage/project-storage.ts |
shipped | Phase 5 β ProjectStorage interface + Local impl + S3 stub |
apps/daemon/src/storage/daemon-db.ts |
shipped | Phase 5 β DaemonDb config resolver (sqlite default, postgres stub) |
GET /api/plugins/:id/asset/* |
shipped | Phase 4 β sandboxed plugin asset endpoint (Β§9.2 CSP) |
apps/daemon/src/plugins/trust.ts |
shipped | Phase 1 + Phase 2A β validateCapabilityList, grantCapabilities, revokeCapabilities |
apps/daemon/src/plugins/doctor.ts |
shipped | Phase 1 (manifest + atom + ref checks) β expanded Phase 3 |
apps/daemon/src/genui/registry.ts |
shipped | Phase 2A β F8 cross-conversation cache + lifecycle |
apps/daemon/src/genui/events.ts |
shipped | Phase 2A β genui_* + pipeline_stage_* event payload helpers |
apps/daemon/src/genui/store.ts |
shipped | Phase 2A β sole writer of genui_surfaces, prefill / lookup / revoke |
| Table | Status | Phase |
|---|---|---|
installed_plugins |
shipped | Phase 1 β source_kind enum permissive (bundled allowed) per F3 |
plugin_marketplaces |
shipped | Phase 1 β schema only; populated in Phase 3 |
applied_plugin_snapshots |
shipped | Phase 1 β full Β§11.4 shape with expires_at; GC worker lands Phase 5 |
runs.applied_plugin_snapshot_id ALTER |
n/a | runs are in-memory in apps/daemon/src/runs.ts; the in-memory run carries the snapshot id until runs become a SQL table |
conversations.applied_plugin_snapshot_id ALTER |
shipped | Phase 1 β column added by migratePlugins() |
projects.applied_plugin_snapshot_id ALTER |
shipped | Phase 1 β column added by migratePlugins() |
run_devloop_iterations |
shipped | Phase 2A |
genui_surfaces |
shipped | Phase 2A β three indexes per Β§11.4 |
| Endpoint | Status | Phase |
|---|---|---|
GET /api/plugins |
shipped | Phase 1 |
GET /api/plugins/:id |
shipped | Phase 1 |
POST /api/plugins/install (SSE) |
shipped | Phase 1 β local-folder source only; tarball lands Phase 2A |
POST /api/plugins/:id/uninstall |
shipped | Phase 1 |
POST /api/plugins/:id/apply |
shipped | Phase 1 β emits ApplyResult + manifest digest (no run side-effects) |
POST /api/plugins/:id/doctor |
shipped | Phase 1 β manifest lint + atom + ref check |
GET /api/atoms |
shipped | Phase 1 β first-party atom catalog |
GET /api/applied-plugins/:snapshotId |
shipped | Phase 1 β used by run replay tooling |
POST /api/runs/:runId/replay |
shipped | Phase 2A |
GET /api/plugins/:id/preview |
shipped | Phase 2B β sandboxed iframe entry; resolves od.preview.entry with sensible fallbacks |
GET /api/plugins/:id/example/:name |
shipped | Phase 2B β matches against folder name / basename / declared title in od.useCase.exampleOutputs[] |
POST /api/plugins/:id/trust |
shipped | Phase 2A β capability grant / revoke against Β§5.3 vocabulary |
GET / POST /api/marketplaces |
shipped | Phase 3 entry slice |
POST /api/marketplaces/:id/trust |
shipped | Phase 3 entry slice |
GET /api/marketplaces/:id/plugins |
shipped | Phase 3 entry slice |
GET /api/runs/:runId/devloop-iterations |
shipped | Phase 2A |
GET /api/runs/:runId/genui |
shipped | Phase 2A |
GET /api/projects/:projectId/genui |
shipped | Phase 2A |
POST /api/runs/:runId/genui/:surfaceId/respond |
shipped | Phase 2A |
POST /api/projects/:projectId/genui/:surfaceId/revoke |
shipped | Phase 2A |
POST /api/projects/:projectId/genui/prefill |
shipped | Phase 2A |
GET /api/applied-plugins |
shipped | Phase 5 (early) β audit list |
GET /api/projects/:projectId/applied-plugins |
shipped | Phase 5 (early) |
POST /api/applied-plugins/prune |
shipped | Phase 5 (early) β operator escape hatch |
GET /api/daemon/status |
shipped | Phase 1.5 |
POST /api/daemon/shutdown |
shipped | Phase 1.5 β loopback-only |
GET /api/runs/:runId/agui |
shipped | Phase 4 β pipes events through @open-design/agui-adapter |
| Command | Status | Phase |
|---|---|---|
od plugin install/list/info/uninstall/apply/doctor |
shipped | Phase 1 + Phase 2A β install accepts local / github: / https://*.tar.gz / bare plugin name (Phase 3 resolution) |
od plugin run applyβstart shorthand |
shipped | Phase 2A β --inputs, --input k=v, --grant-caps, --follow |
od plugin trust (with connector:<id> form) + --revoke |
shipped | Phase 2A β backed by POST /api/plugins/:id/trust |
od plugin snapshots list / prune |
shipped | Phase 5 (early) β operator escape hatch |
od plugin replay |
shipped | Phase 2A |
od ui list/show/respond/revoke/prefill |
shipped | Phase 2A |
od marketplace add/list/info/refresh/remove/trust |
shipped | Phase 3 entry slice |
od project create/list/info/delete |
shipped | Phase 1 follow-up β accepts --plugin/--inputs/--grant-caps |
od run start/watch/cancel/list/info (with --follow, ND-JSON) |
shipped | Phase 1 follow-up |
od files list/read/write/upload/delete |
shipped | Phase 1 follow-up + Phase 2C |
od daemon start --headless / --serve-web / status / stop |
shipped | Phase 1.5 |
od conversation list/info |
shipped | Phase 2C entry slice |
od files diff |
absent | Phase 2C |
od project import (CLI wrapper of /api/import/folder) |
absent | Phase 2C |
od conversation new |
absent | Phase 2C |
od plugin scaffold (interactive starter) |
shipped | Phase 4 β apps/daemon/src/plugins/scaffold.ts + od plugin scaffold --id <id> |
od plugin export <projectId> --as od|claude-plugin|agent-skill |
shipped | Phase 4 β apps/daemon/src/plugins/export.ts + POST /api/applied-plugins/export |
od plugin publish --to anthropics-skills|awesome-agent-skills|clawhub|skills-sh |
shipped | Phase 4 β apps/daemon/src/plugins/publish.ts + --open browser launch |
od atoms list / show |
shipped | Phase 4 β wraps GET /api/atoms |
od skills list / show |
shipped | Phase 4 β wraps GET /api/skills{,/:id} |
od design-systems list / show |
shipped | Phase 4 β wraps GET /api/design-systems{,/:id} |
od craft list / show |
shipped | Phase 4 β new GET /api/craft{,/:id} |
od status / od version |
shipped | Phase 4 |
od marketplace search "<query>" [--tag <t>] |
shipped | Phase 3 β substring search over every configured catalog |
od skills/design-systems/craft/atoms list/show |
absent | Phase 4 |
od status/doctor/version/config |
partial | Phase 4 (some pieces exist; audit) |
| Component | Status | Phase |
|---|---|---|
apps/web/src/components/InlinePluginsRail.tsx |
shipped | Phase 2A |
apps/web/src/components/ContextChipStrip.tsx |
shipped | Phase 2A |
apps/web/src/components/PluginInputsForm.tsx |
shipped | Phase 2A |
apps/web/src/components/PluginsSection.tsx |
shipped | Phase 2B β composable host-agnostic widget |
applyPlugin() helper in apps/web/src/state/projects.ts |
shipped | Phase 2A β also exports renderPluginBriefTemplate |
apps/web/src/components/GenUISurfaceRenderer.tsx |
shipped | Phase 2A (confirmation/oauth-prompt first-class; form/choice fall back to JSON Schema preview until Phase 2A.5) |
apps/web/src/components/GenUIInbox.tsx |
shipped | Phase 2A |
NewProjectPanel plugin rail mount |
shipped | Phase 2B (entry slice) β PluginsSection mounted under the project-name input |
ChatComposer plugin rail mount |
shipped | Phase 2B β PluginsSection variant='strip' rendered above the composer input when a projectId is bound |
apps/web/src/components/MarketplaceView.tsx |
shipped | Phase 2B β catalog grid + trust filters + configured-catalogs panel; routes /marketplace. |
apps/web/src/components/PluginDetailView.tsx |
shipped | Phase 2B β /marketplace/:id (alias /plugins/:id); 'Use this plugin' calls applyPlugin β Home. |
apps/web/src/components/HomeHero.tsx + home-hero/chips.ts |
shipped | Current product entrypoint β curated scenario chip rail; transitional until a unified scenario registry drives Home + filters + composer tools |
apps/web/src/components/PluginsHomeSection.tsx + plugins-home/facets.ts |
shipped | Data-derived community filters from manifest/taskKind/scenario/tags/pipeline metadata plus curated category taxonomy |
ββ contracts/plugins/* ββ
β β
plugin-runtime (parsers + merge + resolve + validate + digest)
β
ββββββββββββΌββββββββββββββββββββββββββ
β β β
registry installer apply (pure)
β β β
ββββββ¬ββββββ β
β snapshots βββββ connector-gate
β β β
composeSystemPrompt(snapshotId) β tool-tokens
β β β
ββββββββββββ runs βββββββββββββββ β
β β
pipeline + devloop + genui βββββββββββββββ
β
SSE/ND-JSON events
β
βββββββββββββββ΄ββββββββββββββ
CLI (plugin/run/files/ui) Web (rail/strip/inputs/genui)
Three reads from the graph (drove the Β§6 phase reorder)
snapshots.tsis the keystone. It must land in Phase 1 week 1, before pipeline / genui / connector-gate.pipeline.tsandgenui/*are co-required for the first marketable plugin (make-a-deckneedsdirection-picker+oauth-prompt); they must land in the same phase.- CLI and Web parallelize cleanly once
ApplyResultJSON is stable; the only sync point is the ND-JSON event schema inpackages/contracts/src/plugins/events.ts.
- F1. Freeze
manifestSourceDigestalgorithm in Phase 0. Implementation inpackages/plugin-runtime/src/digest.ts; input{manifest, inputs, resolvedContextRefs}β sha256 hex.packages/plugin-runtime/tests/digest.test.tspins 2 known-good digests + canonical-key-order invariant; daemon upgrades cannot change them. - F2. Define
PersistedAgentEventplugin variants in Phase 1, even if they fire later. Variants live inpackages/contracts/src/plugins/events.ts(pipeline_stage_*,genui_surface_*); pipeline / genui emitters land Phase 2A. - F3.
installed_plugins.source_kindaccepts'bundled'from Phase 1.PluginSourceKindSchemapermissive:bundled / user / project / marketplace / github / url / local. - F4.
PluginAssetRef.stageAtdefaults to'run-start', never'project-create'. Default baked intopackages/contracts/src/plugins/apply.ts. - F5.
--jsonoutput uses contracts types; no inline reshape incli.ts. Phase 1 CLI ships--jsonforlist/info/apply/doctorreturning the daemon JSON verbatim; the next CLI rev importsApplyResultetc. from contracts to satisfy the compile-time guarantee. - F6.
OD_MAX_DEVLOOP_ITERATIONSlives inapps/daemon/src/app-config.ts, default 10, override via env. Read viareadPluginEnvKnobs(); consumed by Phase 2Apipeline.ts. - F7.
od plugin doctorvalidatesod.connectors.required[]againstconnectorService.listAll()from Phase 1. Phase 1 doctor validates manifest schema, atoms, and resolved skill / DS / craft refs; the connector lookup wires in onceconnectorServiceis exposed to the doctor module (Phase 1 cleanup PR). - F8. Cross-conversation cache (
genui_surfaceslookup) goes live with the table β i.e. Phase 2A β and a daemon test asserts the secondoauth-promptdoes not broadcast. Covered byapps/daemon/tests/plugins-pipeline-runner.test.ts(reuses a project-tier surface answer across conversations). - F9. Snapshot lifecycle env vars (PB2) live in
apps/daemon/src/app-config.tsfrom Phase 1:OD_SNAPSHOT_UNREFERENCED_TTL_DAYS(default30, set to0to disable),OD_SNAPSHOT_RETENTION_DAYS(default unset, opt-in),OD_SNAPSHOT_GC_INTERVAL_MS(default6 * 60 * 60 * 1000). All three live inreadPluginEnvKnobs();applied_plugin_snapshots.expires_atis stamped on insert; the GC worker lands Phase 5.
The spec Β§16 ordering is reader-facing; this is the build order. Each phase has explicit deliverables, validation steps, and an exit criterion. Flip checkboxes in PRs that land each item.
Deliverables
-
docs/schemas/open-design.plugin.v1.jsonβ JSON Schema v1. -
docs/schemas/open-design.marketplace.v1.jsonβ JSON Schema v1. -
packages/contracts/src/plugins/{manifest,context,apply,marketplace,installed,events}.ts(types + Zod schemas; no logic). - Re-export from
packages/contracts/src/index.ts. -
packages/plugin-runtime/src/digest.tswith frozen sha256 algorithm + fixture cases (packages/plugin-runtime/tests/digest.test.ts).
Validation
-
pnpm --filter @open-design/plugin-runtime test -
pnpm guard && pnpm typecheck - CI digest stability: re-running
digest()on the fixtures matches the pinned hex.
Exit criterion
- Importing
import type { ApplyResult, AppliedPluginSnapshot } from '@open-design/contracts'works from daemon and web. β verified.
Why merged with the spec's "headless MVP CLI loop" β see I4. The spec's Phase 1 explicitly pulls this forward; this plan keeps that.
Deliverables (week 1: data layer)
- SQLite migration for
installed_plugins,plugin_marketplaces,applied_plugin_snapshots(includingexpires_at INTEGERper PB2). Therunstable is in-memory inapps/daemon/src/runs.ts; the in-memory run carries the snapshot id today.projectsandconversationsgetapplied_plugin_snapshot_idALTERs inmigratePlugins(). -
apps/daemon/src/app-config.tsdefinesOD_SNAPSHOT_UNREFERENCED_TTL_DAYS(default30),OD_SNAPSHOT_RETENTION_DAYS(default unset),OD_SNAPSHOT_GC_INTERVAL_MS, andOD_MAX_DEVLOOP_ITERATIONS(F6) underreadPluginEnvKnobs(). Apply path stampsexpires_aton insert; GC worker lands Phase 5. -
packages/plugin-runtimeparsers / adapters / merger / resolver / validator + digest. -
apps/daemon/src/plugins/registry.tsβ install-root scan, sidecar + adapter merge, SQLite reader/writer. (Hot reload + project tier scan land Phase 2A.) -
apps/daemon/src/plugins/installer.tsβ local folder install with path-traversal guard, 50 MiB size cap, symlink rejection. GitHub tarball / HTTPS sources land Phase 2A. -
apps/daemon/src/plugins/apply.tsβ pure; emitsApplyResultwith draft snapshot. -
apps/daemon/src/plugins/snapshots.tsβ sole writer ofapplied_plugin_snapshots. (Repo-levelrgguard wiring inscripts/guard.tslands in the Phase 2A polish PR.) - Refactor
apps/daemon/src/{skills,design-systems,craft}.tsto delegate toregistry.ts. Phase 1 keeps the existing loaders independent so/api/skills,/api/design-systems,/api/craftendpoints remain byte-for-byte stable; Phase 2A folds them into the plugin registry.
Deliverables (week 2: surface layer)
- HTTP:
GET /api/plugins,GET /api/plugins/:id,POST /api/plugins/install(SSE),POST /api/plugins/:id/uninstall,POST /api/plugins/:id/apply,POST /api/plugins/:id/doctor,GET /api/atoms,GET /api/applied-plugins/:snapshotId.POST /api/projects/POST /api/runscontinue to accept their existing payloads; the explicitpluginId/appliedPluginSnapshotIdplumbing lands as a follow-up Phase 1 PR once therunsSQL migration is in place. -
composeSystemPrompt()inapps/daemon/src/prompts/system.tsaccepts apluginBlockrendered from the snapshot viapluginPromptBlock(snapshot)and emits## Active plugin+## Plugin inputssections. Shape: pure assembler + content table (per I5). - CLI:
od plugin install/list/info/uninstall/apply/doctor.od project / run / filessubcommands stay scheduled for the Phase 1 follow-up PR. - Phase 1
od plugin doctorcovers: schema validation, SKILL.md parse, atom id existence check, resolved-context ref check, digest drift detection. MCP dry-launch and connector existence (F7) land in the Phase 1 cleanup PR.
Validation
-
pnpm --filter @open-design/plugin-runtime testcovers: digest stability,parseManifest+parseMarketplace, SKILL frontmatter adapter, sidecar+adapter merge precedence,validateSafecross-field rules. -
apps/daemon/tests/plugins-{apply,snapshots,installer,e2e-fixture}.test.tscover apply purity, snapshot writer, installer guards, and the closed-loop installβapplyβsnapshotβdoctor walk. - e2e-1 closed loop β
apps/daemon/tests/plugins-e2e-fixture.test.tsruns the Β§12.5 walk against the bundledapps/daemon/tests/fixtures/plugin-fixtures/sample-plugin/fixture without spinning the HTTP server. - e2e-2 pure apply across runs β Phase 1 follow-up: drive
applyPluginthroughPOST /api/plugins/:id/applyagainst a running daemon and assert two consecutive applies share the samemanifestSourceDigest. - e2e-3 headless run β needs
od daemon start --headless(Phase 1.5) and theod run start --plugin <id>plumbing (Phase 1 follow-up).
Exit criterion
- Phase 1 daemon-only walkthrough is green:
od plugin install --source <fixture>βod plugin listβod plugin apply <id>produces a stableAppliedPluginSnapshot. The Β§12.5 web-driven walkthrough requires the Phase 1 follow-up PR + Phase 1.5 headless flag.
Pulled out of spec Β§16 Phase 5 because Phase 1 e2e needs it. Avoids "Phase 1 looks green on macOS desktop, breaks on Linux CI" false positives.
Deliverables
-
od daemon start --headlessflag (no electron, no web bundle). -
od daemon start --serve-webflag (web UI without electron). Today this is an alias of--headlessbecause the v1 daemon serves both API and web UI from the same Express app; the flag is reserved so packaged callers can branch on it. - Honor
OD_BIND_HOSTandOD_PORTin headless mode (the flags forward into the env so the existing daemon code path picks them up unchanged). -
od daemon stop,od daemon status --json.
Validation
-
apps/daemon/tests/daemon-lifecycle.test.tscovers the/api/daemon/statusshape and the loopback-only enforcement on/api/daemon/shutdown. -
apps/daemon/tests/plugins-headless-run.test.tscovers e2e-3's HTTP-level walkthrough; the full Docker re-run is deferred to the Phase 5 cloud-deployment PR.
Phase 2A β Pipeline + devloop + GenUI(confirmation/oauth-prompt) + connector-gate + Web inline rail (4β6 d)
Deliverables (daemon)
-
apps/daemon/src/plugins/pipeline.tsβ stage scheduler;untilevaluator; devloop withOD_MAX_DEVLOOP_ITERATIONSceiling. -
apps/daemon/src/plugins/pipeline-runner.tsβ bridges the scheduler onto a live run's SSE stream + GenUI cache. - SQLite migration:
run_devloop_iterations,genui_surfaces(3 indexes),connectors_required_json/connectors_resolved_json/mcp_servers_jsoncolumns onapplied_plugin_snapshots. -
apps/daemon/src/genui/{registry,events,store}.tsβ confirmation, oauth-prompt, form, choice surfaces; reuse the existingapps/daemon/src/connectors/flow foroauth.route='connector'. - Cross-conversation cache (F8) β
lookupResolved+ emitgenui_surface_response { respondedBy: 'cache' }. -
apps/daemon/src/plugins/connector-gate.tsβ apply path connector resolution + token-issuance gate./api/tools/connectors/executere-validates per call (CONNECTOR_NOT_GRANTED). - HTTP:
GET /api/runs/:runId/genui,GET /api/projects/:projectId/genui,POST /api/runs/:runId/genui/:surfaceId/respond,POST /api/projects/:projectId/genui/:surfaceId/revoke,POST /api/projects/:projectId/genui/prefill,POST /api/runs/:runId/replay,GET /api/runs/:runId/devloop-iterations. - SSE / ND-JSON streams emit
pipeline_stage_started/completed,genui_surface_request/response/timeout,genui_state_syncedper F2. - API-fallback rejection:
/api/proxy/*returns409 PLUGIN_REQUIRES_DAEMON(e2e-7). - PB1 β
renderPluginBlock(snapshot)lives inpackages/contracts/src/prompts/plugin-block.ts. Both composers import it; v1 fallback still 409s.
Deliverables (CLI)
-
od plugin trust <id> --capabilities β¦(withconnector:<id>form) +--revoke. -
od plugin apply --grant-caps a,b+--input k=v(repeated). -
od plugin replay <runId>. -
od ui list/show/respond/revoke/prefill. - CLI structured error envelope for Β§12.4 exit codes (64β73).
-
od plugin run <id>applyβstart shorthand (full ND-JSON streaming viaod run watchlands as part of the Phase 1 follow-up). -
od plugin snapshots list / prune(operator escape hatch).
Deliverables (web)
-
applyPlugin(pluginId, projectId?)helper inapps/web/src/state/projects.ts. -
InlinePluginsRail,ContextChipStrip,PluginInputsForm. -
GenUISurfaceRendererforconfirmation+oauth-prompt(cards / modal);form/choiceship a fallback JSON-Schema preview + textarea until Phase 2A.5. -
GenUIInboxdrawer. - Mount the trio in
NewProjectPanelandChatComposerβPluginsSectionwrapsInlinePluginsRail/ContextChipStrip/PluginInputsFormand is mounted under the project-name input inNewProjectPanel.tsx:467and above the composer input inChatComposer.tsx:701(variant='strip').
Validation
- e2e-4 replay invariance β
apps/daemon/tests/plugins-dod-e2e.test.ts. - e2e-5 GenUI cross-conversation β
apps/daemon/tests/plugins-pipeline-runner.test.ts. - e2e-6 connector gate β
apps/daemon/tests/plugins-dod-e2e.test.ts+plugins-tool-token-gate.test.ts. - e2e-7 api-fallback rejection β
apps/daemon/tests/proxy-routes.test.ts. - Daemon unit test: pipeline stage scheduler converges on a critique signal in β€3 iterations β
apps/daemon/tests/plugins-pipeline-runner.test.ts. - Daemon unit test: F8 cache hit does not broadcast β
apps/daemon/tests/plugins-pipeline-runner.test.ts.
Deliverables
-
GenUISurfaceRendererextended forformandchoice; JSON Schema β React form bridge (small, in-tree; no external dep added). Strict subset:type:objectproperties whose leaves are scalars (string/number/integer/boolean) or single-level enums; nested objects/arrays fall back to the JSON textarea.defaultValueis honoured so cross-conversation re-asks pre-fill. (apps/web/src/components/GenUISurfaceRenderer.tsxJsonSchemaFormSurface+readObjectSchemaFields). - CLI parity:
GET /api/runs/:runId/genui/:surfaceIdenriches the response with the snapshot's surface spec (incl. JSON Schema).od ui show --schemaprints just the schema for headless agents (apps/daemon/src/cli.ts:UI_BOOLEAN_FLAGS+ the--schemashortcut inrunUiShow).
Validation
- Web test:
apps/web/tests/components/GenUISurfaceRenderer.schema-form.test.tsxcovers the structured form path (string + select + integer + boolean), default-value seeding, single-enum choice routing through the existing button-group renderer, and the JSON-textarea fallback for unsupported leaves. - Daemon test:
apps/daemon/tests/plugins-genui-spec-enrichment.test.tsboots the daemon, installs a fixture plugin with a form surface, creates a project + snapshot, drops agenui_surfacesrow, and asserts theGET /api/runs/:runId/genui/:surfaceIdresponse carriesspec.schemaexactly as declared in the manifest. - Daemon test (deferred): a
formsurface answered viaod ui respond --value-json '...'and a UI answer both emitgenui_surface_responsewithrespondedBy: 'user'β kept open for the dedicated CLI β UI parity sweep in Phase 4 e2e-9.
Deliverables
- Routes
/marketplace,/marketplace/:id(alias/plugins/:id) inapps/web/src/router.ts:33. -
MarketplaceView,PluginDetailView(apps/web/src/components/MarketplaceView.tsx,apps/web/src/components/PluginDetailView.tsx). -
ChatComposerintegratesPluginsSection(which composesInlinePluginsRail+ContextChipStrip+PluginInputsForm) atChatComposer.tsx:701;applyPlugin()accepts the boundprojectId(apps/web/src/state/projects.ts). -
GET /api/plugins/:id/previewand/api/plugins/:id/example/:namewith the Β§9.2 sandbox CSP (default-src 'none'; connect-src 'none'; ...),X-Content-Type-Options: nosniff, and the same envelope as/asset/*. The preview entry followsod.preview.entrywithpreview/index.html+index.htmlfallbacks; the example endpoint matches against folder name / basename / declared title inod.useCase.exampleOutputs[]. (apps/daemon/src/server.ts:servePluginSandboxedHtml.) - Preview path traversal / symlink / size guards β the helper rejects
..segments, refuses to follow symlinks vialstat, and caps payloads at 5 MiB.
Validation
- Browser test: a malicious-fixture preview cannot fetch
/api/*(CSPconnect-src 'none'). - e2e: install local plugin β marketplace β detail preview β "Use" β Home or ChatComposer prefilled β run produces design.
Deliverables
-
od files write/upload/delete/diff. -
od project delete/import,od run list/logs --since. -
od conversation list/new/info(basic).
Validation
- Extend the Β§12.5 walk-through:
od project importan external folder βod plugin applyβod plugin replay <runId>reruns on top.
Deliverables
-
od marketplace add/list/info/refresh/remove/trustβ Phase 3 entry slice. -
GET / POST /api/marketplaces,POST /api/marketplaces/:id/trust,GET /api/marketplaces/:id/plugins. -
od plugin install <name>resolves through configured marketplaces (resolvePluginInMarketplaces+POST /api/plugins/installbare-name detection). Marketplace trust does NOT auto-propagate β see spec Β§9. - Trust UI on
PluginDetailView(capability checklist + Grant action). - Apply pipeline gates by
trust+capabilities_granted(already partly in Phase 2A; this phase wires UI + marketplace). - Bundle plugin installer (multiple skills + DS + craft β registry under namespaced ids).
-
od plugin doctor <id>runs full validation including bundle expansion.
Validation
- e2e: install plugin from a local mock
marketplace.json, rotate ref, uninstall. - e2e: restricted plugin cannot start MCP server until Grant clicked; check
applied_plugin_snapshots.capabilities_grantedupdates.
Deliverables
-
docs/atoms.md;GET /api/atomsreturns implemented + reserved (with(planned)marker). Source of truth:apps/daemon/src/plugins/atoms.ts. -
od plugin export <projectId> --as od|claude-plugin|agent-skillβapps/daemon/src/plugins/export.ts+POST /api/applied-plugins/export. -
od plugin run <id> --input k=v --follow(apply + run start wrapper) β landed in Β§3.B3 (Phase 2A). Full ND-JSON streaming viaod run watchis also shipped (Phase 1 follow-up Β§3.F1). -
od plugin scaffoldinteractive starter βapps/daemon/src/plugins/scaffold.ts. -
od plugin publish --to anthropics-skills|awesome-agent-skills|clawhub|skills-sh(PR template launcher) βapps/daemon/src/plugins/publish.ts. - CLI parity remainder:
od skills/design-systems/craft/atoms list/show,od status,od version,od marketplace search,od doctor,od config get/set/list/unset. - Optional
plugins/_official/atoms/<atom>/SKILL.mdextraction (spec Β§23.3.2 patch 2) β entry slice ships four atom SKILL.md fragments + the bundled boot walker; the system.ts β SKILL.md prompt-composer rewiring stays open. -
@open-design/agui-adapterpackage;GET /api/runs/:runId/aguiSSE endpoint emits AG-UI canonical events. - Plugin manifest upgrade:
od.genui.surfaces[].component(capability gategenui:custom-component) β schema accepts the field; doctor flags missing-capability + path-traversal; web sandbox loader stays scheduled.
Validation
- e2e-9 UI β CLI parity: pick 5 desktop UI workflows; replay each through
od β¦only; produced artifacts byte-for-byte equal. - AG-UI smoke: a CopilotKit React client subscribes to
/api/runs/:runId/aguiand renders surfaces unmodified. This is an external-interop smoke, not a blocker for OD-native web/desktop rendering.
Deliverables
-
linux/amd64+linux/arm64Dockerfile per spec Β§15.1 (deploy/Dockerfile; entry-slice base isnode:24-alpinewithNODE_IMAGEbuild-arg override βnode:24-bookworm-slim; bundled atom plugins ship inside the image). - CI pushes
:edgeon main,:<version>on tag β.github/workflows/docker-image.yml. -
tools/pack/docker-compose.yml,tools/pack/helm/β chart templates (Deployment / Service / Secret / ConfigMap / PVCs / Ingress / NOTES) shipped, per-cloudvalues-<cloud>.yamloverrides shipped (AWS / GCP / Azure / Aliyun / Tencent / Huawei / self-hosted). - Bound-API-token guard: daemon refuses to bind
OD_BIND_HOST=<non-loopback>withoutOD_API_TOKEN; bearer middleware on/api/*skipped only on loopback peers and on the open probes (/api/health,/api/version,/api/daemon/status). -
ProjectStorageadapter substrate βLocalProjectStorage(v1 default) wired + tested;S3ProjectStorageinterface-locked stub;resolveProjectStoragereadsOD_PROJECT_STORAGE. AWS SDK wiring stays as the next Phase 5 PR. -
DaemonDbadapter substrate βresolveDaemonDbConfigreadsOD_DAEMON_DB+OD_PG_*; the SQLite path is the only reachable backend until the postgres adapter lands. - Snapshot retention enforcement job (PB2). Landed early (Β§3.A5): periodic worker (
OD_SNAPSHOT_GC_INTERVAL_MS, default 6 h) deletes expired rows. Referenced-row TTL viaOD_SNAPSHOT_RETENTION_DAYSstays opt-in. CLI escape hatch:od plugin snapshots prune --before <ts>.
Validation
-
docker runsmoke: image starts, web UI renders,od plugin installworks inside container. - Multi-cloud smoke: deploy compose to AWS Fargate, GCP Cloud Run, Azure Container Apps; produce a fixed plugin's artifact byte-for-byte equal across clouds.
- Pluggable storage smoke: same plugin alternated between local-disk + SQLite and S3 + Postgres; artifacts identical.
These are tracked but not part of v1 sign-off. Listed here so spec patches that promote (planned) atoms have a place to update.
- Phase 6 β figma-migration native: implement
figma-extract+token-map; ship officialfigma-migrationplugin. All atom impls + asset pass landed (plan Β§3.M3 / Β§3.N4 / Β§3.O2 / Β§3.P1 / Β§3.P2 / Β§3.Q2):figma-extractwalks the Figma REST API into<cwd>/figma/{tree,tokens,meta}.jsonAND honoursofflineAssets:falseto download per-leaf-node assets viaGET /v1/images(50-id chunks, per-id failure isolation, configurable size cap,assets/<id>.<ext>layout).token-mapcrosswalks any source bag against the active design system. Bundled scenario pluginod-figma-migrationships the canonical pipeline. - Phase 7 β code-migration native (Β§20.3 Β§21.3.2):
code-import,design-extract,rewrite-plan,patch-edit,diff-review,build-test. All six atom impls landed (plan Β§3.N1 / Β§3.N2 / Β§3.O2 / Β§3.O3 / Β§3.O4 / Β§3.O5). Bundled scenario pluginod-code-migrationships the canonical pipeline (code-importβdesign-extract+token-mapβrewrite-planβpatch-edit β build-testdevloop βdiff-reviewβhandoff). Live HTTP wiring for the per-stage runner keeps to scheduled. - Phase 8 β production code delivery native: repo-aware multi-file patch orchestration; native review-and-apply surface; promote
handoffKind: 'deployable-app'from reservation to implementation. End-to-end wiring landed (plan §3.N3 / §3.O4 / §3.O5 / §3.P3 / §3.Q1 / §3.R1):patch-editenforces shell-tier safety + writes per-step receipts;diff-reviewemits review/{diff.patch,summary.md,decision.json,meta.json}; the daemon auto-derives a__auto_diff_review_<stageId>choice surface for every diff-review stage; the web composer'sGenUISurfaceRendererrenders the diff-review surface natively (Accept all / Reject all / Partial⦠per-file checklist) plus a generic single-enum-property choice fallback;POST /api/runs/:runId/genui/:surfaceId/respondnow bridges the diff-review choice surface response intorunDiffReview()so the user's decision lands onreview/decision.jsonimmediately.recordHandoff()enforces append-only export/deploy targets;isDeployableAppEligible()centralises the §11.5.1 promotion rule.ArtifactManifestcarries the full reserved provenance surface.
These were originally spec Β§18 open questions; they are now resolved and propagated into both this plan and docs/plugins-spec.md proper. Future spec patches that revisit them must update both files in the same PR.
- PB1. Lift
## Active pluginblock intopackages/contracts/src/prompts/plugin-block.tsin Phase 2A (was Phase 4). Decision: accepted as proposed. BothcomposeSystemPrompt()implementations (daemon + contracts) import the same renderer. Spec Β§11.8 patched to drop the "Phase 4 lifts the block" bullet and the CI byte-equality cross-check fixture; spec Β§18 patched to mark the open question resolved. Plan Β§6 Phase 2A gains the deliverable; Phase 4 loses it. - PB2.
AppliedPluginSnapshotunreferenced-row TTL. Decision: accepted with one modification to preserve spec Β§8.2.1's reproducibility-first stance. Final shape:applied_plugin_snapshots.expires_at INTEGERcolumn lands in Phase 1 (NULL allowed).- Snapshots referenced by any
runs.applied_plugin_snapshot_id/conversations.applied_plugin_snapshot_id/projects.applied_plugin_snapshot_idkeepexpires_at = NULL(pinned forever; reproducibility unchanged). - Unreferenced snapshots receive
expires_at = applied_at + OD_SNAPSHOT_UNREFERENCED_TTL_DAYS(default 30 d, set to0to disable). This is the apply-then-cancel garbage-growth defense. - The "expire even referenced" knob
OD_SNAPSHOT_RETENTION_DAYSis operator-opt-in only, default unset; when set, a referenced row may expire ifapplied_atis older than the window AND the referencing row is itself terminal (run finished, conversation archived, project deleted). - Both env vars live in
apps/daemon/src/app-config.ts(per F6 pattern). Phase 1 ships the column + config wiring; Phase 5 ships the periodic enforcement job. - Spec Β§11.4 patched to add the
expires_atcolumn; spec Β§18 patched to mark the open question resolved.
v1 ships when all of the following pass on a clean Linux CI container without electron. Each row links to the daemon / e2e test path that asserts it.
- e2e-1 cold install β
od plugin install ./fixtures/sample-pluginβ<OD_DATA_DIR>/plugins/sample-plugin/exists.installed_pluginshas one row withtrust='restricted',source_kind='local'.- Test path:
apps/daemon/tests/plugins-e2e-fixture.test.ts
- e2e-2 pure apply β two consecutive applies share
manifestSourceDigest; the project cwd byte size is unchanged;applied_plugin_snapshotsis not written byapplyPlugin()itself (the resolver is the writer).- Test path:
apps/daemon/tests/plugins-dod-e2e.test.ts(e2e-2 pure apply across runs).
- Test path:
- e2e-3 headless run (full Β§8 contract). Install β project create β run start β status β snapshot fetch all walked at the HTTP layer; the run's first SSE event is
pipeline_stage_started(asserted via the live SSE stream) and the snapshot id is pinned through every step.firePipelineForRun()runs synchronously insidePOST /api/runsbeforedesign.runs.start()schedules the agent.- Test path:
apps/daemon/tests/plugins-headless-run.test.ts(bothwalks install β project create β run start β status with snapshot pinnedandfirst SSE event on a plugin run with od.pipeline is pipeline_stage_started).
- Test path:
- e2e-4 replay invariance β after a same-id plugin upgrade,
renderPluginBlock(snapshot)returns the byte-equal prompt block; the live applyPlugin against the upgraded plugin produces a differentmanifestSourceDigest.- Test path:
apps/daemon/tests/plugins-dod-e2e.test.ts(e2e-4 replay invariance).
- Test path:
- e2e-5 GenUI cross-conversation β second conversation in the same project does not broadcast a fresh
genui_surface_request; it emitsgenui_surface_response { respondedBy: 'cache' }instead.- Test path:
apps/daemon/tests/plugins-pipeline-runner.test.ts(reuses a project-tier surface answer across conversations).
- Test path:
- e2e-6 connector trust gate β
resolvePluginSnapshotrejects with HTTP 409 / exit 66 /capabilities-requiredwhen the snapshot is restricted andconnector:<id>is missing. Independently,checkConnectorAccessrejects the same call so a leaked tool token cannot bypass Β§5.3.- Test path:
apps/daemon/tests/plugins-dod-e2e.test.ts(e2e-6 connector trust gate) +apps/daemon/tests/plugins-tool-token-gate.test.ts.
- Test path:
- e2e-7 api-fallback rejection β every
/api/proxy/*entry returns409 PLUGIN_REQUIRES_DAEMONwhen a body smugglespluginIdorappliedPluginSnapshotId.- Test path:
apps/daemon/tests/proxy-routes.test.ts(API fallback rejects plugin runs).
- Test path:
- e2e-8 apply purity regression β 100 applies grow the snapshot count by 100, leave the project cwd byte size unchanged, and emit no
.mcp.json.- Test path:
apps/daemon/tests/plugins-dod-e2e.test.ts(e2e-8 apply purity regression).
- Test path:
Plus repo-wide gates
-
pnpm guardclean. -
pnpm typecheckclean. -
pnpm --filter @open-design/contracts testclean. -
pnpm --filter @open-design/plugin-runtime testclean. -
pnpm --filter @open-design/daemon testβ all 56plugins-*.test.ts(391 tests) green. Three unrelated pre-existing failures remain (finalize-design.test.tsresolveCurrentArtifact path normalization,chat-route.test.tsstalled-json-stream timeout,connection-test.test.tshard-cancel timeout). They were inherited from PR #832 and the chat/connection timeout test refactors and do not block the plugin loop; tracked separately. -
pnpm --filter @open-design/web testclean.
| Field | Value |
|---|---|
| Current phase | Phase 2A + 1 + 1.5 + 2B + 2C entry slice + 3 (full) + 4 (full incl. OD_BUNDLED_ATOM_PROMPTS default ON) + 5 (full incl. live S3 impl; postgres adapter still stubbed) + 6 (full incl. asset rasterisation) + 7 (all six atom impls) + 8 (full incl. GenUI \u2192 decision bridge) + scenarios bundle + bundled-scenario fallback resolver |
| Next planned PR | (a) Phase 2C β od files write/upload/delete/diff + od project import + od conversation new. (b) Phase 3 β Trust UI on PluginDetailView + bundle plugin installer. (c) Phase 4 e2e-9 β UI β CLI parity walkthrough (5 workflows). (d) postgres adapter wiring inside the DaemonDb resolver. (e) Scenario registry convergence so Home chips, plugin filters, composer tools, and @search project from the same manifest/scenario taxonomy. |
| Open spec push-backs | none β PB1 / PB2 resolved (see Β§7) |
Last sync against docs/plugins-spec.md |
2026-05-13 (clarified default/no-plugin behavior, od-default routing, daemon system-prompt layering, plugin-assembled pipeline stages, OD-native controlled GenUI rendering, AG-UI adapter as interoperability only, and the current Home rail vs PluginsHome facet convergence gap) |
Update this table on every plugin-system PR merge. When the value of "Current phase" advances, also flip the matching deliverables in Β§6 and the modules in Β§3.
- Spec:
docs/plugins-spec.mdΒ·docs/plugins-spec.zh-CN.md - Skills protocol:
docs/skills-protocol.md - Architecture overview:
docs/architecture.md - Repository conventions:
AGENTS.md,apps/AGENTS.md,packages/AGENTS.md - Adjacent active plan:
docs/plans/manual-edit-mode-implementation.md