Canvas-renderer plugin slot — v0.4 plan
The locked design for v0.4 after a full plan-eng-review (4 sections, 7 architecture forks, 1 codex outside-voice consult, 3 cross-model tensions resolved). v0.4 ships AFTER v0.2 and v0.3, with the full Maximalist scope.
@aoagents/ao-plugin-canvas-{name} npm packages without core PRs.ao plugin install CLI auto-rebuild + install-warning UI.Why v0.4 ships after v0.2 + v0.3
The plan-eng-review consulted codex (the outside-voice / second opinion step). Codex's verdict on v0.4-as-next-step was direct: "v0.4 should not be next. Ship v0.2 or v0.3 first. Renderer plugins are high-infrastructure, low-immediate-user-value."
- v0.2 (CanvasProducer wiring) ships PR-status, linked-issues, cost canvases the moment it lands — every existing AO user benefits with no install. Highest leverage per engineer-day.
- v0.3 (mux push) fixes the felt-slowness of 5s polling. Real UX gap that v0.4 would otherwise inherit.
- v0.4 (renderer plugins) is platform infrastructure that benefits zero existing users on day 1 and only kicks in when someone publishes a plugin. Ships best with a clear UX story (CLI auto-rebuild, install warning, real-npm-pack tested), which is what the Maximalist scope provides.
Locked decisions
Each row is the outcome of an interactive plan-eng-review fork. Defaults are not editable in the plan — to change one, re-open the review.
Strategic — defer v0.4 until v0.2 and v0.3 are live
LOCKEDv0.2 (CanvasProducer wiring) and v0.3 (mux push) ship first. v0.4 starts after v0.3 is stable. Codex outside-voice was decisive: the OSS-extensibility story needs the rebuild UX it can only justify shipping with the Maximalist scope, and meanwhile v0.2 + v0.3 deliver immediate user value.
Scope — Maximalist when v0.4 ships
LOCKEDSelective scope (defer CLI + warning UI to v0.4.1) was rejected after codex pointed out that build-time discovery makes "install + rebuild" the marquee user experience and deferring the CLI ships an intentionally confusing path. v0.4 ships with: core machinery + example plugin + CI fixture + ao plugin install CLI auto-rebuild + install-time trust warning UI. ~2.5–4 engineer-weeks of core work.
Discovery — single build-time walk, JSON + barrel are siblings
LOCKEDOne discovery moment (build-time) produces both packages/web/src/generated/canvas-plugins.json (manifest + plugin paths) and packages/web/src/generated/canvas-renderers.ts (renderer barrel). AO startup reads the JSON and dynamically imports each plugin to hydrate Zod schemas — no node_modules walk in the hot path. Eliminates the two-moment-drift class of bug where startup and build-time disagree on which plugins exist.
Built-in canvasType reservation
LOCKEDDiscovery refuses to load any plugin whose canvasType is in {markdown, diff, table, stats}. Plus reserve the core-* type prefix (per codex finding) for future built-in expansion. Refused with named error before AO starts. Closes the trojan-plugin-shadow attack surface.
Per-canvas error boundaries
LOCKEDEach rendered canvas wraps in its own React <ErrorBoundary>. A buggy plugin canvas blanks only that canvas with a stub showing the canvas type + plugin name + a link to the plugin's repo. Sibling canvases keep rendering. Per-rail boundary was rejected (one bad canvas would blank trusted core-git-diff + table results + PR status).
Plugin package contract — strict, validated at discovery
LOCKED (re-framed)Build-time validation enforces:
package.json exportsmap has both"."(Node-safe schema) and"./renderer"(browser-safe component).peerDependencies.reactis set;dependencies.reactis not set (avoids two-React hooks errors)../rendererentrypoint is compiled JS (not raw TSX).- AST-level static check that renderer has no
node:*imports and schema has nowindow/documentreferences.
Refuses non-conforming plugins with named errors. Codex re-frame applied: the AST scanner is a compatibility lint, not a security mechanism. It catches honest plugin-author footguns (Node API in browser bundle, DOM API in Node-side schema) but cannot reduce blast radius — transitive imports, dynamic import(), globalThis.window, malicious deps all bypass it. The trust statement does the security work; the AST scanner just keeps plugins shaped right.
Validate-once, narrow-in-dispatcher payload typing
LOCKED (scope clarified)Core validates plugin payload via the registered Zod schema during ingest. Dispatcher narrows the type via z.infer<schema> and passes typed payload to the plugin renderer. Plugin authors get payload.samples directly, not payload: unknown.
Codex scope clarification: end-to-end static typing across community plugins is not possible — CanvasArtifact at AO's type system level is BuiltIn | { payload: unknown }. Type narrowing works within a plugin's own component (we pass z.infer<theirSchema> locally via the generated barrel) but the dispatcher itself only knows unknown. Plan language updated to say "typed locally, not end-to-end."
Plugin load failure policy — named-error-and-skip
LOCKEDPer-plugin try/catch in discovery. Failures are logged with plugin name + reason, and the plugin is omitted from the registry. AO continues to start with reduced functionality. Type-id collisions remain fatal (refuse to start, names both plugins). Codex addition: schema imports are arbitrary startup code (plugins can do I/O, mutate globals, hang). Per-plugin try/catch + a per-plugin import timeout (5s) to handle abuse + latency.
Lazy renderer imports + per-canvas Suspense
LOCKEDGenerated barrel emits React.lazy(() => import("...")) per plugin renderer, with a per-canvas <Suspense> boundary in the rail. Each plugin loads as its own Next.js code-split chunk only when a canvas of that type appears. Bundle size scales with usage, not with installed plugin count. Eager imports were rejected — they would force a v0.5 generator-script rewrite the moment the ecosystem grew past ~5 plugins.
Design constraints from codex outside-voice (folded in)
Twelve additional codex findings were not big enough to be cross-model tensions but reshape the implementation. All accepted as default constraints:
| Constraint | What changes |
|---|---|
| Plugin install location | v0.4 must specify: plugins live in packages/web/package.json dependencies (the bundler's build context). The ao plugin install CLI adds them there, not at the monorepo root. |
| Schema import timeout | Per-plugin import wrapped in a 5s timeout. Slow / hanging plugins are skipped with a named error rather than blocking AO startup. |
| Zod peer-dependency | Plugins declare zod as peerDependency alongside React. Standardize on the Zod major core ships. Use safeParse only — no instanceof ZodError across version boundary. |
| ZodEffects / transforms / async refinements | Forbidden in plugin schemas at v0.4. Validated at discovery. Reduces validate-once semantics to "no transforms, no async." |
| Real npm-pack CI fixture | Replace the original "workspace fixture" plan with: npm pack the example plugin, install the tarball into a temp AO/web app, run discovery, build Next, start dashboard, render canvas. Proves the published-to-npm path, not just monorepo imports. |
| Unsupported-state disambiguation | The "Unsupported canvas type" placeholder must distinguish "plugin missing" / "stale build artifact" / "plugin failed discovery" / "AO version mismatch." Stub UI shows the state + remediation hint. |
| Stale-canvas cleanup tooling | Add ao canvas prune --type X CLI to remove orphaned canvas JSON files in workspaces after plugin uninstall. |
Reserve core-* canvasType prefix | D4 was for the four specific built-in names. Also reserve the core-* prefix at canvasType level (mirroring the existing id-level reservation) for future built-in expansion. |
| Fixture choice — not flamegraph | Flamegraphs have heavy payload + virtualization concerns. The first contract fixture should be a tiny renderer (e.g. canvas-counter: a single-metric tile with one prop). Keep flamegraph as a perf demo, not the slot-proves-it-works fixture. |
| Plan language — type narrowing scope | Plan + docs say "typed locally" instead of "typed end-to-end" so plugin authors don't expect AO core to know their payload shape. |
| Test count tied to failure modes | The 29-test count was process theater. Tests are organized by failure mode: stale artifact, missing rebuild, npm-packed plugin, Zod peer mismatch, renderer chunk failure, schema import throw, collision, uninstall, unsupported type, Next production build. |
| Next.js production-build coverage | The CI fixture must run next build (production) not just dev mode — file tracing in production output omits dynamic imports unless they were known at build time, which is the failure mode the architecture has to survive. |
Ship cost
2.5–4 engineer-weeks of core work for the full Maximalist scope, including:
- Discovery generator script (~3 days)
- Schema registry + canvas-log integration (~2 days)
- Web renderer dispatch + Suspense + error boundaries (~2 days)
- AST compatibility lint (~3 days, given dynamic import + transitive scan complexity)
ao plugin installCLI + auto-rebuild (~3 days)- Install-time trust warning UI (~2 days)
- Example tiny plugin + real-npm-pack CI fixture (~3 days)
ao canvas pruneCLI (~1 day)- 29 tests across 6 files (~3 days)
- Docs + migration guide for plugin authors (~2 days)
Sequenced: any of CLI, AST lint, generator can run in parallel after the schema registry is in place.
NOT in scope (deferred)
- Iframe sandboxing. Credible v0.5+ option for genuinely-untrusted renderers. Half-baked iframe is worse than no sandbox.
- Hot-reload of plugin renderers. Plugin authors restart AO; the dev loop is documented but not optimized for v0.4.
- Plugin marketplace UI. v0.4 plugins are discovered via npm — no AO-hosted catalog, no in-app browsing.
- Write-back from plugin canvases. Read-only at the canvas level. v0.5+ explores consent-gated action buttons.
- Image rendering in built-in markdown. Separate v0.5 question with its own allowlist policy.