Agent Orchestrator · v0.4 plan · post plan-eng-review

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.

Reviewed 2026-05-10 7 forks resolved Maximalist scope Ships after v0.2 + v0.3 2.5–4 eng-weeks
Goal
Let community contributors ship new canvas types as @aoagents/ao-plugin-canvas-{name} npm packages without core PRs.
Scope
Maximalist: core machinery + example tiny plugin + real-npm-pack CI fixture + ao plugin install CLI auto-rebuild + install-warning UI.
Sequence
Ship v0.2 (CanvasProducer wiring) and v0.3 (mux push) first. v0.4 lands when those are stable.
Trust
Installing a renderer plugin grants full dashboard code execution. The install-time warning UI says so explicitly.
Tests
29 new tests across 6 files, all mandatory at land — no test deferral.

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."

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.

D1 → A

Strategic — defer v0.4 until v0.2 and v0.3 are live

LOCKED

v0.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.

D2 → A

Scope — Maximalist when v0.4 ships

LOCKED

Selective 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.

D3 → A

Discovery — single build-time walk, JSON + barrel are siblings

LOCKED

One 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.

D4 → A

Built-in canvasType reservation

LOCKED

Discovery 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.

D5 → A

Per-canvas error boundaries

LOCKED

Each 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).

D6 → A

Plugin package contract — strict, validated at discovery

LOCKED (re-framed)

Build-time validation enforces:

  1. package.json exports map has both "." (Node-safe schema) and "./renderer" (browser-safe component).
  2. peerDependencies.react is set; dependencies.react is not set (avoids two-React hooks errors).
  3. ./renderer entrypoint is compiled JS (not raw TSX).
  4. AST-level static check that renderer has no node:* imports and schema has no window/document references.

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.

D7 → A

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."

D8 → A

Plugin load failure policy — named-error-and-skip

LOCKED

Per-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.

D9 → A

Lazy renderer imports + per-canvas Suspense

LOCKED

Generated 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:

ConstraintWhat changes
Plugin install locationv0.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 timeoutPer-plugin import wrapped in a 5s timeout. Slow / hanging plugins are skipped with a named error rather than blocking AO startup.
Zod peer-dependencyPlugins 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 refinementsForbidden in plugin schemas at v0.4. Validated at discovery. Reduces validate-once semantics to "no transforms, no async."
Real npm-pack CI fixtureReplace 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 disambiguationThe "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 toolingAdd ao canvas prune --type X CLI to remove orphaned canvas JSON files in workspaces after plugin uninstall.
Reserve core-* canvasType prefixD4 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 flamegraphFlamegraphs 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 scopePlan + 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 modesThe 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 coverageThe 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:

Sequenced: any of CLI, AST lint, generator can run in parallel after the schema registry is in place.

NOT in scope (deferred)