diff --git a/docs/ExtensionsSystemAgentExecutionPlan.md b/docs/ExtensionsSystemAgentExecutionPlan.md new file mode 100644 index 000000000..87394f878 --- /dev/null +++ b/docs/ExtensionsSystemAgentExecutionPlan.md @@ -0,0 +1,813 @@ +# Canvas Extensions PoC Agent Execution Plan + +Status: executable handoff plan +Source RFC: [ExtensionsSystemRFC.md](./ExtensionsSystemRFC.md) +Source PoC plan: [ExtensionsSystemPoCPlan.md](./ExtensionsSystemPoCPlan.md) +Target: a working proof of concept that can be built by multiple agents without one agent holding all context +Current repo baseline: merged through `upstream/main` at `40844533`. This checklist assumes `agentServer` `1.27.0`, the current local/public auth modes, frontend-only/backend-only partial stacks, MCP catalog data from `@openhands/extensions/integrations`, the merged PR #1246 tool visualizer registry, draft PR #1277 as the current lead for extension-facing visualizer registration, and public skills defaulting on unless `VITE_LOAD_PUBLIC_SKILLS=false`. + +## 1. How To Use This Plan + +This is the checklist an implementation agent should follow. The RFC is the product and architecture contract; the PoC plan explains the shape of the proof. This file turns both into task-sized work. + +Planning branch and implementation branch policy: + +- Treat `dv/extensions-poc-v1` as the planning branch. It contains the RFC, PoC plan, execution checklist, and example-extension planning artifacts. Do not add implementation commits to that branch unless the task is explicitly another planning/doc update. +- Start implementation from a fresh pull of current `upstream/main`, not from the planning branch. Use one branch per PR slice, for example `dv/extensions-g0-contracts`, `dv/extensions-g1-cli-host`, and so on. +- If the planning branch has not landed on `main` yet, read these planning artifacts from `dv/extensions-poc-v1` (for example with `git show dv/extensions-poc-v1:docs/ExtensionsSystemAgentExecutionPlan.md`) or from a separate worktree. Do not merge the planning branch into an implementation branch just to access the docs. +- Keep commit history clean by landing one gate or narrow task group per PR. If a later gate needs a type or helper from an earlier gate, branch from the merged earlier PR or rebase onto current `upstream/main` after it lands. +- Reference this plan and `dv/extensions-poc-v1` in each implementation PR so reviewers can connect code slices back to the source architecture without mixing planning commits into implementation history. +- Before opening an implementation PR, verify the branch diff contains code/tests for that slice only, not unrelated planning-doc churn from `dv/extensions-poc-v1`. + +Rules for every agent: + +- Read this file, the RFC, the PoC plan, and `AGENTS.md` before editing. +- Run `git status --short` before starting and before handing off. +- Claim a task ID in the handoff notes or PR description before editing overlapping files. +- Keep each task scoped to its listed files unless implementation discovers a real dependency. +- End every task with: files changed, tests run, remaining risks, and the next task ID that is unblocked. +- Do not weaken repo rules: frontend Agent Server calls use typed clients; Extension Host routes use the dedicated wrapper exception; UI strings go through i18n; package dependencies stay exact-pinned. +- Install, update, and remove are CLI-only in every mode; never add browser controls for them. Enable, disable, and new-extension discovery are CLI + restart by default, but become live browser actions on the development source stack when the launcher reports `liveExtensionManagement` (the Vite-served `npm run dev` / `dev:minimal` stack). Gate the host's `enable`/`disable`/`rescan` routes and the Extensions page toggles/rescan behind that capability; the packaged/Docker/static page stays read-only. See RFC §15.2 and §27 Decision 16. + +Suggested verification tiers: + +```sh +npm run typecheck +npm test +npm run build +npm run build:lib +npm pack --dry-run +``` + +Run the narrowest useful command while iterating. Before declaring a gate complete, run the gate's listed verification. + +## 2. Multi-Agent Work Model + +Use gates to coordinate merge order. Tasks inside a gate can be split, but the gate should not be considered complete until all acceptance checks pass. + +| Gate | Purpose | Parallelism | Blocks | +|---|---|---|---| +| G0 | Risk burners and contracts | Mostly serial | Everything else | +| G1 | Manager, CLI, install store, host routes | Manager/CLI and launcher work can split after shared contracts land | UI, view host, conversation merge | +| G2 | Example extension and package release path | Can run alongside G1 after contracts land | View host, acceptance demo | +| G3 | Frontend Extensions inventory page and CanvasExtensionService | Can run once Extension Host registry shape is stable | View host UX, dev mode UX | +| G4 | Browser-module view host, left navigation, settings panels, right panels, and visualizers | Can run after registry/assets exist; visualizers can split after shared types land | Acceptance demo | +| G5 | Conversation contribution merge | Can run after launch-contributions endpoint exists | Final proof | +| G6 | Dev mode watch/remount | Can run after manager, host, and view host exist | Final proof | +| G7 | Acceptance, release smoke, polish | Serial final pass | PoC complete | + +Avoid these overlapping edits unless one agent owns the integration: + +- `bin/agent-canvas.mjs`, `scripts/dev-safe.mjs`, `scripts/dev-with-automation.mjs`, `scripts/static-server.mjs`, and `vite.config.ts` should have one launcher/routing owner at a time. +- `src/api/agent-server-adapter.ts` and `src/api/conversation-service/agent-server-conversation-service.api.ts` should have one conversation-merge owner at a time. +- Sidebar route work should coordinate with Extensions page route work before editing `src/routes.ts`. +- `package.json` exports/files changes should coordinate with release-smoke work. + +## 3. Gate G0: Risk Burners And Contract + +### G0.1 Canvas Extension Package Smoke + +- [ ] Owner: +- [ ] Depends on: none +- [ ] Files likely touched: + - `examples/extensions/sunset-theme/**` + - `examples/extensions/hello-page/**` + - `examples/extensions/app-panel/**` + - `examples/extensions/shell-toolkit/**` + - optional smoke helper under `scripts/` or `__tests__/canvas-extensions/` +- [ ] Implement: + - Use the example Canvas Extensions as package/manifest fixtures, not as finalized runtime contracts. + - Confirm a directory with `agent-canvas.extension.json` or `package.json#agentCanvas.manifest` is detected as a Canvas Extension. + - Confirm theme-only, `mount()`-only, `activate()`-only, and combined `activate()` + `mount()` examples map cleanly to the planned manifest types. + - Confirm portable agent plugin, portable skill, and MCP-template markers are detected without being classified as Canvas Extensions. + - Do not exercise `.agent` writes in G0; plugins and skills are separate reusable agent artifacts, not Canvas Extensions, and their installer behavior is tested in G1. +- [ ] Tests: + - Add repeatable manifest/artifact-detection fixtures where possible. + - Document any example files that are intentionally provisional human examples rather than executable contracts. +- [ ] Done when: + - Canvas Extension package detection is proven without relying on portable plugin/skill behavior. + - Portable plugin/skill/MCP markers are clearly separate from Canvas Extensions in diagnostics. +- [ ] Handoff notes: + +### G0.2 Static Dynamic Import Smoke + +- [ ] Owner: +- [ ] Depends on: none +- [ ] Files likely touched: + - `examples/extensions/hello-page/dist/index.js` + - `__tests__/canvas-extensions/` or a small script fixture +- [ ] Implement: + - Create a browser-ready ESM fixture exporting `mount({ root, context })`. + - Verify static production build can dynamically import it from `/canvas-extension-assets/...`. + - Add a negative fixture that uses a bare runtime import such as `react`. +- [ ] Tests: + - Validation rejects bare runtime imports with an actionable diagnostic. + - Valid module imports with only bundled/relative imports. +- [ ] Done when: + - DOM-island runtime works without Vite resolving shared dependencies. +- [ ] Handoff notes: + +### G0.3 Shared Types And Manifest Validation + +- [ ] Owner: +- [ ] Depends on: none +- [ ] Files likely touched: + - `src/canvas-extensions/types.ts` + - `src/canvas-extensions/manifest-schema.ts` + - `src/canvas-extensions/manifest-validation.ts` + - `src/canvas-extensions/artifact-detection.ts` + - `src/canvas-extensions/storage-paths.ts` + - `src/themes/color-themes.ts` + - `__tests__/canvas-extensions/manifest-validation.test.ts` + - `package.json` + - `tsconfig.lib.json` if needed for emitted type paths + - keep new public types under `src/canvas-extensions/`; do not recreate the old `src/addons/` namespace +- [ ] Implement: + - Manifest v1 types, contribution types, registry entry types, diagnostics, installable artifact kinds. + - Contribution types for left navigation, `colorThemes`, `settingsPanels`, `conversationRightPanels`, and `toolVisualizers`. + - `AgentCanvasColorThemeDefinition` aligned with the current app theme model: `label`, `scale`, `heroui`, and optional semantic `tokens`. + - Validation for required fields, ID regex, duplicate/mutually exclusive `browser.module` and `browser.entry`, path containment, reserved `browser.entry`. + - Validation for color theme IDs and allowed theme token keys. + - Artifact detection order: Canvas Extension, portable agent plugin, portable skill, MCP placeholder. + - Storage helpers for `~/.openhands/agent-canvas/installations`. + - Public package exports `@openhands/agent-canvas/canvas-extensions` and the OpenHands agent-team-approved visualizer export, with emitted `dist/extensions/*` and matching visualizer output. +- [ ] Tests: + - Valid extension manifest. + - Missing required fields. + - Bad ID. + - Traversal in manifest/browser/icon/agent-plugin-reference paths. + - Both `browser.module` and `browser.entry`. + - Reserved `browser.entry` diagnostic. + - Theme-only extension manifest is valid. + - Invalid color theme token key is rejected. + - Portable plugin / `SKILL.md` detection returns `agent-plugin` / `skill` kinds without entering the Canvas Extension manifest validator. +- [ ] Verification: + +```sh +npm run typecheck +npm test -- __tests__/canvas-extensions/manifest-validation.test.ts +npm run build:lib +``` + +- [ ] Done when: + - Type consumers can import from `@openhands/agent-canvas/canvas-extensions` and the final visualizer authoring export. + - No runtime host, CLI store, or UI behavior is introduced in this gate. +- [ ] Handoff notes: + +## 4. Gate G1: Manager, CLI, Host, And Routing + +### G1.1 Extension Manager Library + +- [ ] Owner: +- [ ] Depends on: G0.3 +- [ ] Files likely touched: + - `scripts/extension-manager.mjs` + - `__tests__/canvas-extensions/extension-manager.test.ts` + - `__tests__/canvas-extensions/fixtures/**` +- [ ] Implement: + - Store bootstrap: private `package.json`, `package-lock.json`, `artifacts.json`, `config.json`, `logs/`, `dev/`, `node_modules/`. + - Read/write registry state with atomic writes (temp-file + rename) and single-read loads. Guard multi-step install/update/remove with an exclusive `installations/.lock`; `rescan` reads a consistent snapshot and backs off (waits/retries or reports `install in progress`) if the lock is held. + - Install Canvas Extension manifests from local paths first; tarball/npm spec can follow in the same gate if scoped. + - Run npm installs in the private store with `--ignore-scripts` by default. + - Enable/disable/remove/update state transitions exposed as manager functions; the CLI calls them directly and the host calls enable/disable/rescan only when live management is enabled. The manager itself is mode-agnostic. + - Disabled wins over enabled if config is manually inconsistent. + - Dispatch portable skills/plugins to the agent-artifact installer path; keep them out of the Canvas Extension registry. + - Typed unsupported diagnostics for MCP-template artifacts. + - Registry projection with `assetBaseUrl`, diagnostics, state, version, package name. + - Launch contribution projection from extensions enabled at process startup. +- [ ] Tests: + - Store bootstrap. + - Local extension install and enable. + - `--no-enable` leaves disabled/installed. + - Portable skill/plugin artifacts route to the `.agent` installer and do not appear in the Canvas Extension registry. + - Unsupported MCP-template artifact. + - Duplicate ID collision. + - Invalid manifest state. + - Remove preserves settings by default. + - Path traversal rejection. + - Concurrent CLI write + Host read never yields a torn/partial `config.json`/`artifacts.json`; rescan during a held lock waits or reports `install in progress`. +- [ ] Verification: + +```sh +npm test -- __tests__/canvas-extensions/extension-manager.test.ts +``` + +- [ ] Done when: + - CLI and host can share the manager without duplicating store logic. +- [ ] Handoff notes: + +### G1.2 CLI Dispatch And Commands + +- [ ] Owner: +- [ ] Depends on: G1.1 +- [ ] Files likely touched: + - `bin/agent-canvas.mjs` + - optional `scripts/extension-cli.mjs` + - optional `scripts/agent-artifact-installer.mjs` + - `__tests__/canvas-extensions/extension-cli.test.ts` +- [ ] Implement: + - Dispatch `install`, `list`, `enable`, `disable`, `remove`, `update`, `doctor`, and `dev-extension` before stack startup. + - Dispatch before `--public`, `--frontend-only`, and `--backend-only` stack validation. + - Dispatch before checking for `build/`. + - Dispatch before importing `scripts/dev-with-automation.mjs`. + - `--disable-extensions` remains a process-local stack-start flag. + - Help text includes management commands and global install happy path. + - `agent-canvas install ` writes the selected `.agent` target through the agent-artifact installer after explicit interactive confirmation or a non-interactive approval flag. + - `agent-canvas list plugins` and `agent-canvas list skills` report portable agent artifacts separately from `list canvas-extensions`. + - `doctor` reports invalid manifests, portable skill/plugin `.agent` install diagnostics, unsupported MCP-template placeholders, missing assets, package/export issues where detectable. +- [ ] Tests: + - `agent-canvas list canvas-extensions` works with no `build/`. + - `agent-canvas list plugins` and `agent-canvas list skills` work with no `build/`. + - Installing a portable skill/plugin updates a temp `.agent` target and does not create a Canvas Extension registry entry. + - `agent-canvas doctor` works with no `build/`. + - Unknown command produces useful help. + - `install --install-scripts=deny` is default. +- [ ] Verification: + +```sh +node bin/agent-canvas.mjs list canvas-extensions +node bin/agent-canvas.mjs doctor +npm test -- __tests__/canvas-extensions/extension-cli.test.ts +``` + +- [ ] Done when: + - Management commands are usable from source checkout and packed global install without launching services. +- [ ] Handoff notes: + +### G1.3 Extension Host HTTP Service + +- [ ] Owner: +- [ ] Depends on: G1.1 +- [ ] Files likely touched: + - `scripts/extension-host.mjs` + - `__tests__/canvas-extensions/extension-host.test.ts` +- [ ] Implement: + - `GET /api/canvas/canvas-extensions/registry` + - `GET /api/canvas/canvas-extensions/diagnostics` + - `GET /api/canvas/canvas-extensions/:id/settings` + - `PATCH /api/canvas/canvas-extensions/:id/settings` + - `GET /api/canvas/canvas-extensions/launch-contributions` + - `GET /canvas-extension-assets/:id/*assetPath` + - `POST /api/canvas/canvas-extensions/:id/enable`, `POST /api/canvas/canvas-extensions/:id/disable`, and `POST /api/canvas/canvas-extensions/rescan` — registered only when the launcher reports `liveExtensionManagement`. + - Session API key guard for settings mutation and for the gated enable/disable/rescan routes. + - Static asset path containment and content types. +- [ ] Tests: + - Registry and diagnostics read. + - Settings mutation rejects missing/bad API key. + - Install/update/remove browser routes are absent in every mode. + - `enable`/`disable`/`rescan` routes are absent when `liveExtensionManagement` is false and present + API-key-guarded when true. + - Live enable/disable mutates `config.json`; rescan surfaces a newly installed extension. + - Asset serving works and traversal fails. + - Launch-contributions filters disabled/invalid extensions. +- [ ] Verification: + +```sh +npm test -- __tests__/canvas-extensions/extension-host.test.ts +``` + +- [ ] Done when: + - A toy registry and browser module can be fetched through the host directly. +- [ ] Handoff notes: + +### G1.4 Launcher And Proxy Integration + +- [ ] Owner: +- [ ] Depends on: G1.3 +- [ ] Files likely touched: + - `scripts/dev-safe.mjs` + - `scripts/dev-with-automation.mjs` + - `scripts/static-server.mjs` + - `vite.config.ts` + - `config/defaults.json` if adding default extension host port + - launcher/proxy tests under `__tests__/` +- [ ] Implement: + - Allocate/start Extension Host in `npm run dev`, `dev:minimal`, `dev:static`, and packaged `agent-canvas`. + - Route `/api/canvas/canvas-extensions/*` and `/canvas-extension-assets/*` before `/api/*` and before SPA fallback. + - Add Vite proxy support for direct Vite access. + - Add static server route support for direct static-port access. + - Cover `--frontend-only` and `--backend-only`: frontend-only may expose local Extensions/browser assets with agent-runtime diagnostics; backend-only should skip browser asset hosting unless a future CLI-only host mode explicitly needs it. + - Emit frontend env/runtime metadata: extension enabled flag, `localInstallStoreReadable`, `liveExtensionManagement`, route/base if needed. + - Derive `liveExtensionManagement` from the existing `services.frontend.kind` in `scripts/runtime-services-info.mjs`: true for the Vite dev server (`npm run dev` / `dev:minimal`), false for the static server (packaged CLI, Docker, `dev:static`). Use it to register/skip the host enable/disable/rescan routes. + - Do not expose a write-capable Extension Host key in ``. +- [ ] Tests: + - Route precedence unit tests for `scripts/static-server.mjs` and ingress route ordering. + - Launcher config test for `localInstallStoreReadable=true` only when this launcher starts the local Agent Server. + - Launcher config test for `liveExtensionManagement=true` only on the Vite dev stack and false for static/packaged/Docker. + - Partial-stack route tests for frontend-only/backend-only behavior. + - Vite config contains extension routes before generic `/api` where applicable. +- [ ] Verification: + +```sh +npm test -- __tests__/vite-config.test.ts +npm test -- __tests__/canvas-extensions +``` + +- [ ] Done when: + - Registry JSON and browser module assets can be fetched through ingress and through the direct frontend/static port. +- [ ] Handoff notes: + +## 5. Gate G2: Example Extension And Release Path + +### G2.1 Hello Canvas Extension Fixture + +- [ ] Owner: +- [ ] Depends on: G0.3 +- [ ] Files likely touched: + - `examples/extensions/hello-canvas/package.json` + - `examples/extensions/hello-canvas/agent-canvas.extension.json` + - `examples/extensions/hello-canvas/dist/index.js` + - `examples/extensions/hello-canvas/dist/*.svg` + - optional README under the fixture +- [ ] Implement: + - Manifest with `id: "hello.canvas"`. + - One `browser.module` exporting both `activate(context)` (registers the visualizer, settings panel, and right panel — see RFC §13.1) and `mount(params)` (renders the view). + - One primary sidebar contribution after Automations. + - One launch template. + - One context block with a recognizable marker. + - No portable agent plugin fixture in this task; plugins are separate reusable agent artifacts and are tested only as optional agent-runtime references in G5. + - Minimal settings schema. + - No bare runtime imports in the shipped module. +- [ ] Tests: + - Manifest validation fixture. + - Asset route fixture. + - Launch contribution fixture. +- [ ] Done when: + - This one fixture can drive the full acceptance demo. +- [ ] Handoff notes: + +### G2.2 Release Packaging Smoke + +- [ ] Owner: +- [ ] Depends on: G0.3, G1.2 +- [ ] Files likely touched: + - `package.json` + - optional `scripts/package-smoke.mjs` + - `__tests__/canvas-extensions/package-smoke.test.ts` if automated +- [ ] Implement: + - Ensure `files` includes every runtime script/build artifact needed by the global CLI and Extension Host. + - Ensure `./canvas-extensions` and the final visualizer export resolve from the packed package. + - Add a repeatable smoke for packed tarball install into a temp npm prefix if practical. +- [ ] Verification: + +```sh +npm run build +npm run build:lib +npm pack --dry-run +``` + +- [ ] Done when: + - Packed package contains `bin`, `scripts`, `build`, `dist/extensions`, `dist/visualizers`, and any advertised schema files. + - `agent-canvas list canvas-extensions` and `agent-canvas doctor` work from packed/global install. +- [ ] Handoff notes: + +## 6. Gate G3: Frontend Extensions Inventory + +### G3.1 CanvasExtensionService API Wrapper + +- [ ] Owner: +- [ ] Depends on: G1.3 +- [ ] Files likely touched: + - `src/api/canvas-extensions-service.ts` + - `src/api/no-direct-agent-server-calls.test.ts` + - `src/hooks/query/use-extensions.ts` +- [ ] Implement: + - Dedicated wrapper for every `/api/canvas/canvas-extensions/*` call. + - Narrow test exception for this wrapper, including the current `fetch('/api/...')` guard. + - React Query hooks for registry, diagnostics, settings read/patch, and launch contributions. + - Capability-gated enable/disable/rescan mutation methods that call the host routes only when `liveExtensionManagement` is true and invalidate the registry query on success; never expose install/update/remove mutations. +- [ ] Tests: + - No-direct-Agent-Server guard still fails arbitrary `/api/*` fetches outside the wrapper. + - Wrapper builds the expected routes and auth headers. + - Enable/disable/rescan mutations invalidate the registry query and are not exposed (or no-op) when `liveExtensionManagement` is false. +- [ ] Verification: + +```sh +npm test -- src/api/no-direct-agent-server-calls.test.ts +npm run typecheck +``` + +- [ ] Done when: + - Frontend consumers never hand-roll Extension Host fetches. +- [ ] Handoff notes: + +### G3.2 Extensions Page + +- [ ] Owner: +- [ ] Depends on: G3.1 +- [ ] Files likely touched: + - `src/routes.ts` + - `src/routes/canvas-extensions-page.tsx` + - `src/components/features/skills/extensions-navigation.tsx` + - `src/routes/extensions-hub.tsx` if the existing Customize hub needs route/link updates + - `src/i18n/translation.json` + - generated i18n files via `npm run make-i18n` + - `tests/e2e/snapshots/**` +- [ ] Implement: + - Replace or redirect `/plugins` to Extensions while preserving bookmarks. + - Extensions nav item in the existing Customize area (`/customize` primary entry, desktop redirect to `/skills`, mobile Customize hub). + - Treat legacy "extensions" file/component names as existing Customize implementation details; do not use them as new product vocabulary. + - Simple stacked view of installed Canvas Extensions and their status. + - Status grouping/filtering only as needed for scanability: Enabled, Disabled, Invalid, Dev. + - Rows/cards show display name, package, version, state, contribution badges, required secrets, diagnostics, source path for dev entries. + - Read `liveExtensionManagement` from launcher metadata. When false: read-only with CLI guidance for enable/disable plus a restart note. When true: per-row enable/disable toggles and a rescan action wired to the gated service methods, with registry-query invalidation on success. + - CLI guidance for install, update, and remove in every mode; never render browser install/update/remove actions. + - User-facing copy goes through i18n keys. +- [ ] Tests: + - Component tests for empty/enabled/invalid/dev/needs-review states. + - Read-only CLI guidance when `liveExtensionManagement` is false; no toggles rendered. + - Enable/disable toggles and rescan rendered and functional when `liveExtensionManagement` is true, including registry invalidation on a successful toggle. + - Snapshot tests for key states if scope allows in the PR. +- [ ] Verification: + +```sh +npm run make-i18n +npm run typecheck +npm test +``` + +- [ ] Done when: + - A user can inspect `hello.canvas` status and diagnostics from the UI, with management handled by CLI and restart. +- [ ] Handoff notes: + +## 7. Gate G4: Browser Module View Host And UI Surfaces + +### G4.1 Extension Runtime Host + +- [ ] Owner: +- [ ] Depends on: G1.3, G2.1, G3.1 +- [ ] Files likely touched: + - `src/routes.ts` + - `src/routes/extension-view-host.tsx` + - `src/canvas-extensions/runtime/*` + - `src/i18n/translation.json` +- [ ] Implement: + - Route `/canvas-extensions/:extensionId/:viewId/*`. + - Fetch registry, resolve enabled view, import `assetBaseUrl + browser.module`. + - Use `import(/* @vite-ignore */ url)` or equivalent for served extension modules. + - Add cache-busting version token. + - Startup activation pass: for each enabled extension with a `browser.module`, import it once and call `activate(context)` so route-less contributions register independent of any open view (RFC §13.1). Call the `activate()` disposable on disable/remove. + - Call the module's `mount({ root, context })` for the view; call `dispose()` on unmount/remount. + - Provide minimal async context: metadata/settings, `navigation.navigate`, `navigation.openExternal`, `ui.toast`, settings read/patch. + - Local error boundary and diagnostics link. +- [ ] Tests: + - `activate()` runs once at startup for an enabled extension and registers route-less contributions with no view open; a throwing `activate()` is isolated to that extension. + - Mount success. + - Mount failure shows local error and does not crash Canvas. + - Dispose called on unmount/remount. + - Settings read/patch calls wrapper. +- [ ] Verification: + +```sh +npm run typecheck +npm test +``` + +- [ ] Done when: + - `hello.canvas` view renders from served browser module. +- [ ] Handoff notes: + +### G4.2 Left Navigation Entries + +- [ ] Owner: +- [ ] Depends on: G4.1 +- [ ] Files likely touched: + - `src/components/features/sidebar/sidebar.tsx` + - `src/components/features/sidebar/sidebar-rail-body.tsx` + - related mobile/sidebar components + - `src/i18n/translation.json` + - snapshot tests +- [ ] Implement: + - Render enabled `primarySidebar` / `afterAutomations` view contributions after Automations and before conversation list. + - Deterministic sort: `order`, extension display name, view ID. + - Expanded sidebar shows icon + title. + - Collapsed sidebar shows icon with tooltip. + - Mobile drawer shows same relative position. + - Disabled/invalid extensions do not render entries. +- [ ] Tests: + - Expanded/collapsed/mobile render positions. + - Disabled/invalid entries absent. + - Sorting deterministic. +- [ ] Verification: + +```sh +npm run typecheck +npm run test:e2e:snapshots -- --grep "sidebar" +``` + +- [ ] Done when: + - `hello.canvas` sidebar item appears immediately after Automations. +- [ ] Handoff notes: + +### G4.3 Extension Tool Visualizers + +- [ ] Owner: +- [ ] Depends on: G0.3, G1.3, G3.1 +- [ ] Coordination required: OpenHands agent team decision on PR #1277 or its successor +- [ ] Files likely touched: + - `src/components/features/chat/tool-visualizers/*` + - `src/canvas-extensions/visualizers/*` + - `src/api/canvas-extensions-service.ts` + - `package.json` + - `tsconfig.lib.json` + - visualizer tests under `__tests__/components/features/chat/tool-visualizers/` + - extension tests under `__tests__/canvas-extensions/` +- [ ] Implement: + - Public visualizer authoring export matched to the OpenHands agent team's approved API; use PR #1277 as the current starting point. + - Extension visualizer registry layer that composes before built-in visualizers and markdown/default fallback. Extensions register visualizers during the module's `activate(context)` pass (RFC §13.1), not a standalone default-export function. + - `matches()` support for narrowing one event variant. + - No Canvas-only `priority` field unless the agent team approves it. + - Manifest projection for `contributes.toolVisualizers`. + - Error boundary/fallback behavior: thrown extension renderer records a diagnostic and falls through to the next renderer/default. + - Fixture extension visualizer for one MCP/tool variant. +- [ ] Tests: + - Extension visualizer wins over built-in when matching. + - Multiple matching extension visualizers follow the approved registry order. For PR #1277 this means latest registration first. + - `matches()` narrows one action/observation variant without replacing the whole kind. + - Built-in visualizer still handles unmatched events. + - Markdown fallback still handles unregistered events. + - Throwing extension renderer falls through. +- [ ] Verification: + +```sh +npm test -- __tests__/components/features/chat/tool-visualizers +npm run build:lib +npm run typecheck +``` + +- [ ] Done when: + - A local extension can change how one event/tool renders without forking Agent Canvas. +- [ ] Handoff notes: + +### G4.4 Color Themes, Settings Panels, And Conversation Right Panels + +- [ ] Owner: +- [ ] Depends on: G4.1 +- [ ] Files likely touched: + - `src/themes/color-themes.ts` + - `src/components/features/settings/app-settings/theme-input.tsx` + - settings route/components + - conversation right-panel/tab components + - `src/canvas-extensions/panels/*` + - `src/i18n/translation.json` + - tests under `__tests__/canvas-extensions/` + - focused component tests near settings and conversation panel hosts +- [ ] Implement: + - Color theme registry for `contributes.colorThemes`. + - Project enabled extension color themes into Settings > Application > Color Theme. + - Support theme-only extensions with no view/panel/visualizer contribution. + - Fall back to the default built-in theme if the selected extension theme is disabled, removed, or invalid. + - Settings panel registry for `contributes.settingsPanels`. + - Settings panel manifests declare metadata only; browser modules register implementations by contribution ID during `activate()` (RFC §13.1). + - Render settings panels only under a visible Extensions header after built-in settings sections. + - Conversation right-panel registry for `contributes.conversationRightPanels`. + - Conversation right-panel manifests declare metadata only; browser modules register implementations by contribution ID during `activate()` (RFC §13.1). + - Render conversation right panels alongside Files, Browser, and Terminal. + - Stable panel props: conversation ID/status where relevant, active backend summary, workspace summary, extension settings helpers, navigation/toast helpers. + - Deterministic ordering by `order`, extension display name, and panel contribution ID. + - Per-panel error boundaries. + - No raw store, React Query client, or Canvas-internal component API in the public contract. +- [ ] Tests: + - Extension color theme appears in the Color Theme dropdown. + - Theme-only extension contributes no other UI and remains valid. + - Selected extension theme falls back when disabled/invalid. + - Settings panels render only under the Extensions header after built-in settings. + - Missing settings panel implementations produce diagnostics instead of breaking Settings. + - Conversation right panels render beside Files, Browser, and Terminal. + - Missing right-panel implementations produce diagnostics instead of breaking the conversation UI. + - Ordering is deterministic. + - Disabled/invalid extensions do not render panel content. + - Panel errors are localized and do not break settings or conversation pages. +- [ ] Done when: + - A local Canvas Extension can add a color theme, a settings panel, and a conversation right panel without a full app route. +- [ ] Handoff notes: + +## 8. Gate G5: Conversation Contributions + +### G5.0 Optional Agent Plugin Reference Smoke + +- [ ] Owner: +- [ ] Depends on: G1.3, G3.1 +- [ ] Files likely touched: + - optional standalone fixture outside the core Canvas Extension package fixture + - optional smoke helper under `scripts/` or `__tests__/canvas-extensions/` +- [ ] Implement: + - Treat portable agent plugins as separate reusable agent artifacts that a Canvas Extension may reference, never as Canvas Extensions themselves. + - If agent plugin references remain in MVP scope, create the smallest standalone portable plugin fixture with a visible skill/context marker. + - Start a local conversation with top-level `plugins: [{ source: absolutePluginPath }]`. + - Confirm pinned Agent Server `1.27.0` accepts `PluginSource` and loads from a local path. + - Run the same preflight against an ACP configuration or document that ACP remains context-only. +- [ ] Tests: + - Add a repeatable smoke where possible. + - If fully automated live smoke is too expensive, document exact manual command and observed payload/server response. +- [ ] Done when: + - Local-path agent plugin reference support is proven, or agent plugin reference merge is explicitly feature-flagged/deferred. + - ACP behavior is decided for optional agent plugin references. +- [ ] Handoff notes: + +### G5.1 Launch Contributions And Runtime Compatibility + +- [ ] Owner: +- [ ] Depends on: G1.3, G3.1 +- [ ] Files likely touched: + - `src/api/canvas-extensions-service.ts` + - `src/canvas-extensions/runtime-compatibility.ts` + - `__tests__/canvas-extensions/runtime-compatibility.test.ts` +- [ ] Implement: + - `CanvasExtensionsService.getLaunchContributions()`. + - Runtime classes: `canvas-local`, `agent-server-local`, `agent-server-remote`, `cloud-runtime`, `acp-runtime`. + - Use launcher-issued `localInstallStoreReadable`. + - Disable package-relative/local agent plugin paths unless filesystem-local. + - Disable agent plugin references contributed by Canvas Extensions for ACP unless the G5.0 smoke proves allowed. + - Do not assume public marketplace skills are loaded; they now default on but remain opt-out via `VITE_LOAD_PUBLIC_SKILLS=false`. +- [ ] Tests: + - Compatibility matrix across all runtime classes. + - Disabled reasons are stable and UI-ready. + - Remote/cloud never receive local absolute paths. +- [ ] Done when: + - Launch templates can show why each contribution will or will not be included. +- [ ] Handoff notes: + +### G5.2 Create Conversation Merge + +- [ ] Owner: +- [ ] Depends on: G5.1 +- [ ] Files likely touched: + - `src/api/conversation-service/agent-server-conversation-service.api.ts` + - `src/api/agent-server-adapter.ts` + - `src/hooks/mutation/use-create-conversation.ts` + - launch/home components + - `__tests__/api/agent-server-adapter.test.ts` + - `__tests__/canvas-extensions/conversation-contributions.test.ts` +- [ ] Implement: + - `extensionSystemSuffix` adapter option. + - Append `` and `` after ``. + - Merge Canvas Extension-contributed agent plugin references with existing `/launch` plugin selections as `PluginSource` entries. + - Dedupe by `source/ref/repo_path`. + - Preserve current top-level start payload shape: `agent_settings`, `workspace`, `confirmation_policy`, `tool_module_qualnames`, optional `initial_message`, optional `plugins`, and runtime fields such as `hook_config`, `agent_definitions`, `security_analyzer`, `tags`, and `secrets`. + - Keep `conversationInstructions` as user-message content only. +- [ ] Tests: + - Payload includes extension suffix when selected/enabled. + - Payload omits extension context when disabled. + - Existing runtime services suffix still appears first. + - `PluginSource` merge order and dedupe. + - Local-path agent plugin reference skipped on remote/cloud. +- [ ] Verification: + +```sh +npm test -- __tests__/api/agent-server-adapter.test.ts +npm run typecheck +``` + +- [ ] Done when: + - Starting `hello.canvas` launch template produces the expected payload without depending on LLM behavior. +- [ ] Handoff notes: + +## 9. Gate G6: Dev Mode + +### G6.1 Dev Registration And Watch + +- [ ] Owner: +- [ ] Depends on: G1.1, G1.3, G4.1 +- [ ] Files likely touched: + - `scripts/extension-manager.mjs` + - `scripts/extension-host.mjs` + - `bin/agent-canvas.mjs` + - `src/api/canvas-extensions-service.ts` + - `src/routes/canvas-extensions-page.tsx` + - tests under `__tests__/canvas-extensions/` +- [ ] Implement: + - `agent-canvas install --dev`. + - `agent-canvas dev-extension register/list/unregister`. + - Store registrations in `~/.openhands/agent-canvas/installations/dev/dev-extensions.json`. + - No repo auto-discovery. + - Serve assets directly from registered source folder. + - Watch manifest and declared output files. + - Manifest changes revalidate and update diagnostics. + - Permission, network, secret, or agent-affecting contribution expansion moves the dev extension to needs-review/disabled-for-next-run until CLI re-approval and restart. + - Browser module changes bump version token and remount affected views. + - Agent-side changes require new conversation. +- [ ] Tests: + - Registration/unregistration. + - Absolute path storage. + - Traversal rejection. + - Invalid manifest update transitions to invalid diagnostics. + - Permission-expanding manifest update transitions to needs-review/disabled-for-next-run. + - Asset change emits remount/cache-bust signal. +- [ ] Done when: + - A developer can rebuild `hello.canvas` and see the view remount without rebuilding Canvas. +- [ ] Handoff notes: + +## 10. Gate G7: Acceptance And Polish + +### G7.1 End-To-End Acceptance Demo + +- [ ] Owner: +- [ ] Depends on: G1 through G6 +- [ ] Run: + +```sh +npm run build +npm run build:lib +npm pack --dry-run +node bin/agent-canvas.mjs install ./examples/extensions/hello-canvas --yes +node bin/agent-canvas.mjs list canvas-extensions +node bin/agent-canvas.mjs doctor +node bin/agent-canvas.mjs +``` + +- [ ] Browser checks: + - Extensions shows `hello.canvas` enabled with no diagnostics in the read-only inventory. + - Extensions shows CLI/restart guidance rather than browser management actions. + - Left navigation entry appears after Automations and before conversations. + - Extension view renders. + - Extension color theme appears in Settings > Application > Color Theme and can be selected. + - Settings panel appears under a visible Extensions header after built-in settings. + - Conversation right panel appears beside Files, Browser, and Terminal. + - Extension settings patch persists. + - Launch template shows context and optional agent-plugin-reference preflight. + - Conversation payload includes extension suffix. + - Remote/cloud compatibility path shows disabled reason for local agent plugin reference path. + - Dev registration and remount path works in `npm run dev`. + - In `npm run dev`, the Extensions page enable/disable toggle reconciles UI contributions without a restart, and rescan surfaces a newly installed extension; the same build served via the static/packaged path shows the page read-only with no toggles. +- [ ] Done when: + - The PoC works from a clean checkout. +- [ ] Handoff notes: + +### G7.2 Packed Global Install Acceptance + +- [ ] Owner: +- [ ] Depends on: G7.1 +- [ ] Run from a temp directory/prefix: + - Install the packed `@openhands/agent-canvas` tarball. + - Verify `agent-canvas list canvas-extensions`. + - Verify `agent-canvas doctor`. + - Install a packed or local `hello.canvas` extension. + - Launch `agent-canvas` and repeat the browser acceptance checks. +- [ ] Done when: + - The happy path matches the upcoming npm release story: global install, then `agent-canvas`. +- [ ] Handoff notes: + +### G7.3 Final Verification + +- [ ] Owner: +- [ ] Depends on: G7.1, G7.2 +- [ ] Run: + +```sh +npm run typecheck && npm test && npm run build +``` + +- [ ] Also run targeted snapshot/live checks if the PR touched those surfaces: + +```sh +npm run test:e2e:snapshots +npm run test:e2e:live -- --check +``` + +- [ ] Done when: + - Full repo verification passes or failures are documented as unrelated/pre-existing. + - The final handoff lists exact commands, output summary, known risks, and follow-up tasks. +- [ ] Handoff notes: + +## 11. Cross-Cutting Acceptance Criteria + +The PoC is not complete unless all of these are true: + +- [ ] Management CLI commands work without starting services. +- [ ] Management CLI commands work when no `build/` directory exists. +- [ ] Global npm package contains every runtime artifact it needs. +- [ ] Extension Host routes work through Vite, ingress, static server, and packaged CLI. +- [ ] `/api/canvas/canvas-extensions/*` traffic is isolated behind `src/api/canvas-extensions-service.ts`. +- [ ] Ordinary Agent Server API calls still use `@openhands/typescript-client`. +- [ ] Browser modules are trusted same-origin DOM islands and documented/consented as such. +- [ ] Bare runtime imports in extension browser modules are rejected for MVP. +- [ ] Portable skills/plugins install into `.agent` files through the CLI agent-artifact path and never appear in the Canvas Extension registry. +- [ ] Local agent plugin paths are sent only when `localInstallStoreReadable=true`. +- [ ] Install, update, and remove are CLI-only with no browser routes or controls in any mode. +- [ ] Enable/disable/rescan host routes and Extensions page controls exist only when `liveExtensionManagement=true` (the Vite dev stack); the packaged/Docker/static page is read-only and restart-bounded. +- [ ] Route gating is enforced server-side (the Host registers live routes from its own launch-mode computation); the Host validates the session API key on settings and the live routes; a spoofed client flag yields 404s, not privilege. +- [ ] Live enable/disable reconciles UI contributions without a restart; agent plugin and context changes still apply only to conversations created after the change. +- [ ] Route-less contributions (visualizers, themes, settings/right panels) register during the module's `activate()` pass and work with no view open; views render via `mount()`. +- [ ] The install store survives concurrent CLI writes and Host reads without torn state (atomic write+rename, `installations/.lock`). +- [ ] ACP runtimes do not receive incompatible agent plugin/MCP references contributed by Canvas Extensions. +- [ ] `conversationInstructions` never receives extension system context. +- [ ] Extension context composes with existing `` suffix. +- [ ] `hello.canvas` proves install, registry, UI, left navigation, view, color theme, settings panel, conversation right panel, tool visualizer, launch context, and dev remount. Agent plugin reference preflight is a separate optional G5 concern because plugins are not Canvas Extensions. +- [ ] Tests cover the risk surfaces listed in the PoC plan. + +## 12. Handoff Template + +Use this at the end of every agent task: + +```md +Task ID: +Status: complete | blocked | partial +Files changed: +Tests run: +Result: +Known risks: +Next unblocked tasks: +Notes for next agent: +``` + +Blocked tasks should include the exact command, error, and the smallest proposed next step. Avoid broad "needs investigation" handoffs; name the file, route, command, or test that failed. diff --git a/docs/ExtensionsSystemPoCPlan.md b/docs/ExtensionsSystemPoCPlan.md new file mode 100644 index 000000000..e2b6ed0eb --- /dev/null +++ b/docs/ExtensionsSystemPoCPlan.md @@ -0,0 +1,460 @@ +# Canvas Extensions MVP / PoC Build Plan + +Status: working build plan +Source RFC: [ExtensionsSystemRFC.md](./ExtensionsSystemRFC.md) +Agent execution checklist: [ExtensionsSystemAgentExecutionPlan.md](./ExtensionsSystemAgentExecutionPlan.md) +Target branch: `dv/extensions-poc-v1` +Current repo baseline: merged through `upstream/main` at `40844533`. The plan below has been refreshed for the current launcher/auth split, partial-stack modes, MCP integrations catalog, `agentServer` pin `1.27.0`, the merged PR #1246 tool visualizer registry, draft PR #1277 as the lead extension-facing visualizer proposal, and public skills defaulting on. + +## 1. Purpose + +This document turns the Canvas Extensions RFC into a practical MVP / PoC build path. The RFC should stay clean and reviewable as the product/architecture proposal. This plan is the working artifact for agents and developers implementing the proof, testing risky assumptions, and tightening the design based on real code. + +For multi-agent implementation, use [ExtensionsSystemAgentExecutionPlan.md](./ExtensionsSystemAgentExecutionPlan.md) as the task checklist. It breaks this plan into gates, parallel workstreams, task IDs, file ownership, verification commands, and handoff notes. + +The MVP should be built as a vertical proof, not as isolated architecture layers. The first complete proof should demonstrate: + +1. A local or npm-packed Canvas Extension installs into `~/.openhands/agent-canvas/installations`. +2. The Extensions page under Customize shows the installed Canvas Extension and its diagnostics (read-only in packaged/static mode; with live enable/disable on the `npm run dev` stack). +3. A trusted `browser.module` view renders at `/canvas-extensions/:extensionId/:viewId/*`. +4. A view can contribute a left navigation entry after Automations and before the conversation list, matching the current `SidebarRailBody` order (`New Chat`, `Customize`, `Automations`, then conversations). +5. A Canvas Extension can register a color theme that appears in Settings > Application > Color Theme; a theme-only extension is valid. +6. A Canvas Extension can add a settings panel only under a visible Extensions header after built-in settings. +7. A Canvas Extension can add a conversation right panel alongside Files, Browser, and Terminal. +8. A local Canvas Extension can register a custom tool visualizer for one MCP/action variant and safely fall back to built-in rendering when it does not match or throws. +9. A launch template can append extension context; optional agent plugin references are handled as separate agent-runtime contributions, not as Canvas Extensions. +10. Dev mode can register a local source folder, detect output changes, and remount the view without rebuilding Canvas. +11. In a `npm run dev` stack, the Extensions page can enable/disable an extension and pick up a newly installed extension without restarting Canvas; the same stack run from a packaged/static build stays read-only and restart-bounded. + +## 1.1 Scope Locks From The RFC + +These decisions should keep the build from spreading sideways: + +- The CLI installer handles three artifact classes in MVP: Canvas Extensions, portable agent plugins, and portable skills. Canvas Extensions install into the Agent Canvas extension store and have enable/disable lifecycle. Portable skills/plugins install into `.agent` files through the agent-artifact path and never appear as Canvas Extensions. MCP-template artifacts can be detected but remain unsupported placeholders until their install UX is designed. +- CLI management commands must dispatch before stack startup, before the static `build/` existence check, and before importing `scripts/dev-with-automation.mjs`. +- CLI management commands must also dispatch before `--public`, `--frontend-only`, and `--backend-only` stack validation, because `install/list/doctor` should work even when no frontend/backend is being launched. +- Extension install, update, and remove are CLI-only in every mode. Enable/disable and new-extension discovery are CLI + restart by default; users shut down Canvas, run the CLI command, then restart. The one exception is the development source stack (`npm run dev` / `dev:minimal`): when the launcher reports `liveExtensionManagement`, the Customize > Extensions page gains enable/disable toggles and a rescan action that take effect without a restart, and the host registers gated `enable`/`disable`/`rescan` routes. In packaged, Docker, and static modes the page stays read-only inventory and diagnostics. This `vite`-vs-`static` distinction is what lets an in-Canvas agent author an extension and have the user enable it in the same dev session. +- Extension author types need real package subpaths. Keep Canvas Extension management types under `@openhands/agent-canvas/canvas-extensions`; for visualizer authoring, follow the OpenHands agent team's approved export from PR #1277 or its successor. The current placeholder is `@openhands/agent-canvas/canvas-visualizers`, emitted under `dist/visualizers/*`, but do not freeze that name before the agent-team decision. +- Conversation contribution work targets the current start payload: top-level `agent_settings` plus optional top-level `plugins`. Do not reintroduce the legacy `agent` payload shape. +- Package-relative/local agent plugin references are optional agent-runtime contributions only when the launcher says `localInstallStoreReadable=true`. ACP runtimes skip agent plugin references contributed by Canvas Extensions unless a pinned-version smoke proves the exact plugin shape is compatible. +- The first route-less/registry-backed workstreams are color themes, custom tool visualizers, settings panels, and conversation right panels, not arbitrary root-mounted components. Generic conversation slots remain deferred until a concrete workflow cannot be handled by a right panel or visualizer. + +## 1.2 Focused Workstream From Issue #481 + +The issue feedback and product feedback point to five concrete places where users should be able to add content without forking Agent Canvas: + +1. **Custom tool/event visualizers.** PR #1246 added an internal visualizer dispatcher that already asks a registry for a React body and falls back to markdown. Extensions should expose that work through the OpenHands agent-team-approved API. Draft PR #1277 is the current lead: custom/addon visualizers register through a narrow API, run before built-ins, fall through to built-ins/default markdown, support `matches()`, and intentionally avoid `priority`. +2. **Left navigation entries.** A Canvas Extension can add a top-level navigation item after Automations and before the conversation list. +3. **Color themes.** A Canvas Extension can add one or more color themes to Settings > Application > Color Theme, including a theme-only package with no view or panel. +4. **Settings panels.** A Canvas Extension can add configuration UI only under a visible Extensions header after built-in settings. +5. **Conversation right panels.** A Canvas Extension can add a panel beside Files, Browser, and Terminal for conversation-adjacent tools and context. + +The implementation order should be visualizers first where possible because they are narrow, already have a merged internal renderer path, and have a clear failure mode: try extension renderer, then built-in renderer, then markdown/default fallback. The visualizer implementation should not fork from the OpenHands agent team's design; use #1277 or its successor as the source of truth. Color themes are also a narrow early surface because the app already centralizes built-in themes in `src/themes/color-themes.ts` and exposes them through Settings > Application > Color Theme. Settings panels and conversation right panels should follow as explicit product surfaces. Generic "mount anything at app root" slots remain deferred. + +## 2. Risk-Burner Spikes + +Do these first, before investing in polished UI. + +### 2.1 Canvas Extension Package Smoke + +Use the existing example Canvas Extensions as package and manifest fixtures: + +```text +examples/extensions/sunset-theme +examples/extensions/hello-page +examples/extensions/app-panel +examples/extensions/shell-toolkit +``` + +Confirm the detector treats directories with `agent-canvas.extension.json` or `package.json#agentCanvas.manifest` as Canvas Extensions, validates the extension manifest, and keeps the examples mapped to the planned contribution shapes: theme-only, `mount()`-only, `activate()`-only, and combined `activate()` + `mount()`. These examples are human-readable fixtures and may contain provisional runtime assumptions; the manifest shape is what this smoke should validate. + +Also confirm portable agent plugin, portable skill, and MCP-template markers are detected without being classified as Canvas Extensions. Do not exercise `.agent` writes in this risk burner: plugins and skills are reusable agent artifacts and the actual install behavior belongs in the CLI/agent-artifact installer smoke. MCP-template artifacts should still return a clear unsupported placeholder diagnostic. + +### 2.2 Static Dynamic-Import Smoke + +Serve a tiny browser-ready ESM file from a local test host and dynamically import it from the static production build. + +The extension module must use only bundled or relative imports. This proves the DOM-island `mount()` contract works without Vite, shared React, import maps, or module federation. + +Add a negative fixture with a bare runtime import such as `react` and verify validation rejects it with an actionable diagnostic. Type-only imports from `@openhands/agent-canvas/canvas-extensions` are fine because the extension build erases them. Author source may import from the final visualizer authoring subpath, but emitted browser modules must bundle those helpers; raw runtime imports from Agent Canvas package subpaths are not MVP-supported. + +### 2.3 Launcher Route Smoke + +Start a toy Extension Host and route `/api/canvas/canvas-extensions/*` plus `/canvas-extension-assets/*` through the existing ingress/static-server/Vite proxy paths. + +Cover: + +- `scripts/dev-with-automation.mjs` ingress routes. +- `scripts/dev-safe.mjs` / `dev:minimal`, where direct Vite proxying must handle Extension Host routes without the automation ingress. +- `scripts/static-server.mjs` routes used by the packaged CLI/static mode. +- `vite.config.ts` proxy support for direct Vite access, especially `/canvas-extension-assets/*`. +- `--frontend-only` and `--backend-only` partial-stack behavior: frontend-only can expose Extensions/extension views but should show backend-dependent diagnostics, while backend-only should not start the Extension Host view/assets path unless a future CLI-only host mode explicitly needs it. + +Exit criteria: a browser opened through either the ingress port or direct Vite/static port can fetch registry JSON and import a browser module from `/canvas-extension-assets/...`. + +### 2.4 Npm Package / CLI Smoke + +Before any UI work, prove the upcoming release path: + +```sh +npm run build +npm run build:lib +npm pack --dry-run +node bin/agent-canvas.mjs list canvas-extensions +node bin/agent-canvas.mjs doctor +``` + +Then install from a packed tarball into a temp npm prefix and verify `agent-canvas list` and `agent-canvas doctor` work without a source checkout. This catches missing `scripts`, `build`, `dist/extensions`, `dist/visualizers`, schema files, and command-dispatch regressions before the feature depends on them. + +## 3. Walking Skeleton + +Build the thinnest end-to-end slice next. + +### 3.1 Shared Contract + +Add: + +- `src/canvas-extensions/types.ts` +- manifest constants +- artifact detection +- storage-path helpers +- path normalization +- manifest validation +- public package export wiring for `@openhands/agent-canvas/canvas-extensions` and the agent-team-approved visualizer export + +Keep validation deliberately boring: + +- manifest v1 required fields +- ID rules +- relative-path containment +- mutually exclusive `browser.module` / `browser.entry` +- `reserved-not-yet-supported` diagnostic for `browser.entry` + +Avoid a large JSON Schema dependency unless configuration forms force it. + +PR 0 should also update `package.json#exports` for `./canvas-extensions` and, once decided, the visualizer authoring export. Make sure the library build emits `dist/extensions/*` and the matching visualizer output. Keep the actual Extension Host and CLI behavior out of PR 0. + +### 3.2 Manager Library + +Add `scripts/extension-manager.mjs` as the single source of truth for Canvas Extension install-store reads/writes. Both CLI and HTTP host call this module. Add a separate agent-artifact installer module, or an explicitly separated CLI path, for portable skill/plugin `.agent` file writes; the Extension Host should not own those files. + +It owns: + +- Store bootstrap: `package.json`, `package-lock.json`, `config.json`, `artifacts.json`, `logs/`. +- Artifact detection. +- Install Canvas Extension manifests from local path, tarball, npm spec. +- Dispatch portable skills/plugins to the agent-artifact installer, which writes the selected `.agent` file and keeps them out of the Canvas Extension registry. +- Typed unsupported diagnostics for placeholder MCP templates. +- Enable/disable/remove/update through CLI state transitions only. +- Registry and launch-contribution projection. +- Dev registration reads/writes. + +### 3.3 CLI Dispatch + +Update `bin/agent-canvas.mjs` so install/manage commands dispatch before stack startup: + +```sh +agent-canvas install ./examples/extensions/hello-canvas --yes +agent-canvas install ./examples/agent-artifacts/example-plugin --yes +agent-canvas install ./examples/agent-artifacts/example-skill --yes +agent-canvas list canvas-extensions +agent-canvas list plugins +agent-canvas list skills +agent-canvas enable hello.canvas +agent-canvas disable hello.canvas +agent-canvas doctor +agent-canvas --disable-extensions +``` + +This dispatch must happen before public/partial-stack validation, before checking for `build/`, and before importing launcher scripts. MVP Canvas Extension install can support local extension paths first, then npm specs. Npm installs for Canvas Extension packages must run in the private install store with install scripts denied by default. Portable plugin/skill inputs should require an explicit `.agent` target or clear interactive confirmation before writing; MCP-template inputs should return clear unsupported diagnostics rather than mutating user runtime state. + +### 3.4 Extension Host + +Add `scripts/extension-host.mjs`, a local Node HTTP service backed by `extension-manager`. + +It should expose: + +- registry +- diagnostics +- launch contributions +- settings +- static asset routes + +The host never exposes HTTP routes for install, update, or remove; those are CLI-only in every mode. It registers `POST /:id/enable`, `POST /:id/disable`, and `POST /rescan` **only when the launcher reports `liveExtensionManagement`** (the Vite dev stack); in packaged/Docker/static modes those routes are absent and lifecycle stays CLI-only and restart-bounded. The gated routes mutate `config.json` / re-read the store through `extension-manager` and require the same session API key model as settings mutations. The launcher passes the session key to the Host at startup and the Host validates it on settings and the gated routes; route gating is enforced server-side (the Host registers the live routes from its own launch-mode computation), so the frontend `liveExtensionManagement` flag is only a UX mirror. + +### 3.5 Launcher Integration + +Add an `extensionHostPort` to launcher config, start the host before the frontend, and route it before `/api/*`: + +- `/api/canvas/canvas-extensions/*` -> Extension Host. +- `/canvas-extension-assets/*` -> Extension Host. +- `/api/automation/*` -> automation. +- `/api/*`, `/sockets`, server metadata -> Agent Server. + +The launcher also emits frontend env/runtime metadata for: + +- `VITE_EXTENSIONS_ENABLED` or equivalent kill-switch state. +- `VITE_LOCAL_INSTALL_STORE_READABLE=true` only when this `agent-canvas` process started the local Agent Server. +- `VITE_LIVE_EXTENSION_MANAGEMENT=true` only when the frontend is served by the Vite dev server (`npm run dev` / `dev:minimal`), false for the static server used by the packaged CLI, Docker, and `dev:static`. Derive it from the existing `services.frontend.kind` value in `scripts/runtime-services-info.mjs` rather than introducing a separate detector. This flag gates both the host's enable/disable/rescan routes and the Extensions page management controls. +- Extension Host route/base metadata if direct Vite/static access needs it. +- Extension Host URL for dev tooling if needed. + +Do not expose a write-capable Extension Host key in agent prompts. + +Route precedence is the failure-prone part: `/api/canvas/canvas-extensions/*` and `/canvas-extension-assets/*` must match before `/api/*` and static SPA fallback in every launcher path. + +Partial-stack mode rule: frontend-only may start enough Extension Host surface to render local Extensions and extension browser assets, but must mark agent-runtime contributions unavailable because no Agent Server is running. Backend-only should skip frontend asset hosting and extension browser routes by default; CLI management still works because it dispatches before stack startup. + +## 4. Frontend Proof + +### 4.1 API Wrapper + +Add `src/api/canvas-extensions-service.ts` for every `/api/canvas/canvas-extensions/*` call. + +Update `src/api/no-direct-agent-server-calls.test.ts` with one narrow allowlist entry for this wrapper, mirroring automation but covering the current blanket `fetch('/api/...')` rule as well as axios. The exception should allow only `/api/canvas/canvas-extensions/*` from the wrapper; it must not weaken the Agent Server rule for arbitrary `/api/*` calls. + +### 4.2 Extensions Page + +Replace `/plugins` with an Extensions view, while preserving redirects/bookmarks. Use the existing Customize layout and navigation: `/customize` is the primary-sidebar hub entry, desktop redirects to `/skills`, mobile renders the Customize hub, and the current Customize navigation contains Skills, MCP Servers, and a coming-soon Plugins item. + +The page should: + +- Add an Extensions nav item in the Customize navigation. +- Treat legacy file names such as `extensions-hub.tsx` and `extensions-navigation.tsx` as existing Customize implementation details, not product vocabulary for the new Canvas Extensions system. +- Show a simple stacked view of installed Canvas Extensions and their status. +- Use status grouping/filtering only as needed for scanability: Enabled, Disabled, Invalid, Needs review, and Dev. +- Show install source, version, contribution badges, required secrets, diagnostics, dev source paths, and restart-required / needs-review messaging. +- Read `liveExtensionManagement` from the launcher metadata. When false (packaged/Docker/static), the page is read-only and shows CLI guidance for enable/disable plus a restart note. When true (`npm run dev`), render per-row enable/disable toggles and a rescan action wired to the host routes via `canvas-extensions-service`, and invalidate the registry query on success so changes reflect immediately. +- Always show CLI guidance for install, update, and remove; never render browser install/update/remove actions in any mode. +- Stay dense and operational; no marketplace browsing in MVP. +- Route all visible strings, tooltips, and action labels through i18n keys. + +### 4.3 View Host + +Add `/canvas-extensions/:extensionId/:viewId/*` and a route component that: + +- Fetches registry data. +- Resolves `assetBaseUrl + browser.module`. +- Dynamically imports the module with a cache-busting version. +- Calls the module's `mount({ root, context })` for the view. Route-less contributions (visualizers, themes, settings/right panels) are registered separately during the startup `activate(context)` pass, not here (see RFC §13.1). +- Calls `dispose()` on unmount/remount. +- Provides minimal context: extension metadata/settings, `navigation.navigate`, `navigation.openExternal`, `ui.toast`, `settings.readExtensionSettings`, and `settings.patchExtensionSettings`. + +The extension view should be visibly extension-owned but render in a Canvas-owned route container. Errors stay local to the extension view. + +### 4.4 Left Navigation Entries + +Support view navigation contributions that render in the main Canvas Sidebar: + +```json +{ + "navigation": { + "location": "primarySidebar", + "slot": "afterAutomations", + "order": 300, + "icon": "./dist/cost-dashboard.svg" + } +} +``` + +MVP scope: + +- Only `primarySidebar` + `afterAutomations` is required. +- Entries render after Automations and before `SidebarConversationList`. +- Entries navigate to `/canvas-extensions/:extensionId/:viewId/*`. +- Disabled/invalid extensions do not render entries. +- Sorting is deterministic: `order`, extension display name, view ID. +- Expanded sidebar shows icon + title. +- Collapsed sidebar shows icon with a tooltip. +- Mobile drawer shows the same entry in the same relative position. + +This is the path that proves the original addons-style PoC works. Example: `hello.canvas` can include a Cost Dashboard view that appears immediately after Automations. + +### 4.5 Tool Visualizer Extension Surface + +Use the merged built-in visualizer implementation as the first route-less JS proof. + +MVP scope: + +- Align public authoring types and export naming with the OpenHands agent team's approved visualizer API; use PR #1277 as the current starting point. +- Project enabled extension visualizers into a registry that is consulted before built-in visualizers. +- Support `matches()` so an extension can override one specific MCP tool or event variant without replacing an entire action/observation kind. +- Do not add a Canvas-only `priority` field unless the agent team approves it. +- Preserve the existing fallback chain: extension visualizer, built-in visualizer, markdown fallback. +- Wrap extension visualizer bodies in error boundaries; a thrown renderer should record a diagnostic and fall through to the next renderer/default. +- Keep visualizer modules in the trusted local browser-code model. No marketplace or sandbox claim yet. + +Example fixture: `hello.canvas` registers a visualizer for one synthetic or MCP-style tool call and renders a compact card. Tests should prove extension-first order, `matches()` narrowing, built-in fallback, markdown fallback, and throw-to-next-renderer behavior. + +### 4.6 Color Theme Extension Surface + +Add Canvas Extension color themes to the existing application theme selector. + +MVP scope: + +- Project enabled `contributes.colorThemes` entries into the same data source used by Settings > Application > Color Theme. +- Support theme-only extensions with no view, panel, visualizer, or agent contribution. +- Validate theme IDs and allowed token keys before exposing them to the dropdown. +- Disabled/invalid extensions do not expose theme options. +- If a selected extension theme becomes unavailable, fall back to the default built-in theme and show a diagnostic. +- Keep this as theme definition data, not arbitrary CSS or a custom extension-owned settings control. + +Example fixture: `hello.canvas` contributes one recognizable color theme so Settings > Application > Color Theme can select it without opening any extension-owned UI. + +### 4.7 Settings Panels + +Add Canvas Extension settings panels under one visible Extensions header after all built-in settings sections. + +MVP scope: + +- Panels render only in the settings experience, not in the old top-level Extensions section. +- Built-in settings keep their current order; Canvas Extension panels are grouped after them. +- Disabled/invalid extensions do not render settings panels. +- Canvas owns the panel chrome, save/cancel affordances, loading states, and error boundaries. +- Panel renderers receive extension settings helpers only; no raw settings store or React Query client access. +- Manifest entries declare metadata only. The browser module registers implementations by panel contribution ID during `activate()` (see RFC §13.1); missing registrations show diagnostics. + +### 4.8 Conversation Right Panels + +Add Canvas Extension panels to the existing conversation right-panel/tab system beside Files, Browser, and Terminal. + +MVP scope: + +- Panels declare `conversationRightPanels` in the manifest. +- Tabs sort deterministically by `order`, extension display name, and panel ID. +- Disabled/invalid extensions do not render panels. +- Panels receive active conversation ID/status, workspace summary, backend summary, and host APIs for navigation, toasts, and extension settings. +- Canvas owns panel placement, collapsed/expanded behavior, loading state, and error boundaries. +- Manifest entries declare metadata only. The browser module registers implementations by panel contribution ID during `activate()` (see RFC §13.1); missing registrations show diagnostics. + +## 5. Conversation Contributions + +Add contribution merge late enough that the install/store/view path already works. + +### 5.0 Optional Agent Plugin Reference Smoke + +Portable agent plugins are separate reusable agent artifacts, not Canvas Extensions. A Canvas Extension may reference an agent plugin as an agent-runtime contribution, but that compatibility check belongs with conversation launch work, not the core extension package smoke. + +If agent plugin references remain in MVP scope, add a tiny standalone portable plugin fixture and start a local conversation with its resolved absolute path in top-level `plugins`. Confirm the current pinned Agent Server (`config/defaults.json` pins `agentServer` to `1.27.0`) accepts the `PluginSource` shape (`source`, optional `ref`, optional `repo_path`) and can load a package-relative path when `localInstallStoreReadable` is true. The plugin root should contain `.plugin/plugin.json` or another supported portable plugin manifest; if a fixture uses a local path, resolve `source` directly to the plugin root and omit `repo_path`. + +Also run the same contribution preflight with an ACP agent configuration. If ACP fails or the plugin injects MCP/hooks/tool/agent-definition state, keep ACP on context-only extension support and show disabled reasons for agent plugin references. If the local-path smoke fails for the pinned server, keep context contributions in MVP and move agent plugin reference merge behind a feature flag until the Agent Server team confirms the loading contract. + +### 5.1 Launch Contributions Endpoint + +`CanvasExtensionsService.getLaunchContributions()` fetches enabled extension contributions and returns a frontend-ready, compatibility-filtered shape. + +### 5.2 Runtime Compatibility + +Compute disabled reasons before launch: + +- Context blocks: allowed anywhere `system_message_suffix` is accepted. +- Remote agent plugin sources: allowed where `plugins` are accepted. +- Package-relative/local agent plugin paths: allowed only when `localInstallStoreReadable` is true and the optional agent plugin reference smoke passes. +- ACP runtimes: context-only unless the optional agent plugin reference smoke proves the exact plugin shape is ACP-compatible. +- MCP templates: visible as setup requirements, never silently installed. +- Public skills: do not assume public marketplace skills are loaded; current frontend defaults to loading them unless `VITE_LOAD_PUBLIC_SKILLS=false`, but users and deployments can opt out. + +### 5.3 Create-Conversation Merge + +Extend the create-conversation path with an `extensionSystemSuffix` option: + +- `useCreateConversation()` can receive extension launch selections. +- `AgentServerConversationService.createConversation()` asks the extensions service for enabled/selected contributions. +- Canvas Extension-contributed agent plugin references merge with existing `/launch` plugin selections as `PluginSource` entries and dedupe by `source/ref/repo_path`. +- `agent-server-adapter.ts` appends `` and `` after ``. +- The adapter continues to preserve the current top-level payload shape: `agent_settings`, `workspace`, `confirmation_policy`, `tool_module_qualnames`, optional `initial_message`, optional `plugins`, and runtime fields such as `hook_config`, `agent_definitions`, `security_analyzer`, `tags`, and `secrets`. No `agent` payload field is added. +- `conversationInstructions` remains user-message content and never receives extension context. + +For the first proof, a `hello.canvas` launch template should append a recognizable context block so the payload/unit test can prove the suffix path works without depending on LLM behavior. + +## 6. Dev Mode + +Dev mode should prove contributor ergonomics without turning Canvas into a build runner: + +```sh +agent-canvas install ../my-extension --dev +agent-canvas dev-extension register ../my-extension +agent-canvas dev-extension list +agent-canvas dev-extension unregister hello.canvas +``` + +Rules: + +- Dev registration stores absolute paths in `~/.openhands/agent-canvas/installations/dev/dev-extensions.json`. +- Canvas never auto-discovers repo folders. +- The extension project owns its build/watch command. +- Canvas watches the manifest and declared output files. +- Manifest changes revalidate and update diagnostics. +- Permission, network, secret, or agent-affecting contribution expansion moves the dev extension to needs-review/disabled-for-next-run until CLI re-approval and restart. +- Browser module changes bump a version token and remount only affected views. +- Agent-side contribution changes require a new conversation. +- The install store is shared between the CLI and the running Host. Writes use temp-file-then-rename and multi-step transactions take `installations/.lock`, so a CLI install followed by a Host `rescan` (the agent-authoring loop) never reads torn state (see RFC §9). + +The example extension should include a minimal `npm run build -- --watch` authoring flow in docs, but Canvas should not run that command automatically. + +Dev-folder watch (above) and live management (§1.1, §3.4, §4.2) share the same `npm run dev` tier but solve different problems: watch reloads an extension's *code*, while live management enables/disables and discovers *installed* extensions without a restart. Both are gated by the launcher and absent in packaged/Docker/static runs. Live management reuses the dev-watch remount path to mount/unmount reconcilable UI contributions when enablement or the registry changes; agent plugin and context changes still require a new conversation. + +## 7. Acceptance Demo + +The MVP is proven when this scriptable path works from a clean checkout: + +```sh +npm run build +npm run build:lib +npm pack --dry-run +node bin/agent-canvas.mjs install ./examples/extensions/hello-canvas --yes +node bin/agent-canvas.mjs list canvas-extensions +node bin/agent-canvas.mjs doctor +node bin/agent-canvas.mjs +``` + +Then in the browser: + +1. Open Extensions and see `hello.canvas` enabled with no diagnostics in the read-only inventory. +2. See its left navigation entry after Automations and before the conversation list. +3. Open its extension view from the left navigation and see browser-module UI render. +4. Open Settings > Application > Color Theme and select the extension-provided color theme. +5. Open settings and confirm its panel appears under the visible Extensions header after built-in settings. +6. Change a setting in the extension panel and see it persist. +7. Open a conversation and confirm its right panel appears beside Files, Browser, and Terminal. +8. Trigger its fixture tool/event and verify the Canvas Extension visualizer renders with fallback still intact. +9. Start its launch template and verify the conversation payload includes the extension context suffix. +10. In `npm run dev`, register the same extension with `--dev`, rebuild the extension output, and see the view remount with the new code. +11. In `npm run dev`, toggle `hello.canvas` off and back on from the Extensions page and see its UI contributions unmount/remount without restarting Canvas; install a second extension from another terminal, trigger rescan, and see it appear. Then confirm the same build served by the packaged/static path shows the page read-only with no toggles. + +Release-path acceptance should also install the packed `@openhands/agent-canvas` tarball into a temp npm prefix and repeat `agent-canvas list`, `agent-canvas doctor`, and one packed/local extension install without relying on repo-local files outside the package. + +## 8. Testing Strategy + +**Unit:** manifest validation; reserved `browser.entry` diagnostics; path traversal rejection; artifact detection; portable skill/plugin dispatch to the `.agent` installer; unsupported MCP-template diagnostics; Canvas Extension install-store bootstrap; enable/disable/remove/update state transitions through the shared manager (driven by CLI, and by the host enable/disable/rescan routes when live management is enabled); capability gating of those routes by `liveExtensionManagement`; duplicate ID handling; color theme projection/validation/fallback; asset route path validation; CLI arg parsing before build checks; no-install-scripts default; visualizer contribution projection; visualizer ordering/fallback/error-boundary behavior aligned with #1277 or its successor; settings panel projection/registration/ordering; conversation right-panel projection/registration/ordering; launch contribution projection; context suffix rendering; optional `PluginSource` merge/dedupe; runtime compatibility classification; dev registration, manifest revalidation, and permission-drift re-approval. + +**Node integration:** run the Extension Host against a temp install store; install the `hello.canvas` fixture from local path and npm-packed tarball through the CLI; install one portable skill and one portable plugin into a temp `.agent` target and verify they do not appear in the Canvas Extension registry; fetch registry and asset URLs; verify route precedence before `/api/*` in Vite, ingress, static server, and packaged CLI paths; verify settings mutation routes require the session API key; verify no install/update/remove browser routes exist in any mode; verify `enable`/`disable`/`rescan` routes are absent when `liveExtensionManagement` is false and present (and API-key-guarded) when it is true; verify rescan picks up a newly installed extension and live enable/disable mutates `config.json`; verify a concurrent CLI install while the Host is live (then rescan) never yields a torn/partial `config.json`/`artifacts.json` and that rescan waits or reports `install in progress` while the lock is held; verify `doctor` reports invalid manifests, portable skill/plugin `.agent` install diagnostics, unsupported MCP-template placeholders, and missing assets. + +**Release packaging:** `npm pack --dry-run`; packed-tarball install into a temp npm prefix; verify `agent-canvas list`, `agent-canvas doctor`, and an extension install work without a source checkout; verify `@openhands/agent-canvas/canvas-extensions` and the final visualizer export resolve for type consumers. + +**Component:** Extensions page empty/installed/enabled/invalid/dev/needs-review states under Customize in both presentations — read-only with CLI/restart guidance when `liveExtensionManagement` is false, and with enable/disable toggles plus rescan when it is true (including registry-query invalidation on a successful toggle); CLI/restart guidance; left navigation entries in expanded/collapsed/mobile states; extension view loading/error/remount; extension color theme appears in Settings > Application > Color Theme and applies through the existing theme input; settings panel placement under the visible Extensions header; extension settings read/patch; extension visualizer renders/falls back inside the existing event wrapper; conversation right panels render beside Files/Browser/Terminal; launch template preflight for incompatible agent plugin paths. + +**E2E snapshots:** Extensions page with enabled extension under Customize; invalid extension diagnostics; left navigation entry after Automations; extension color theme visible in Settings > Application; settings panel under Extensions header; conversation right panel beside Files/Browser/Terminal; extension view rendered from browser module; launch template showing extension context and any optional agent plugin reference; remote-backend disabled reason for local agent plugin paths; dev extension view remount after output change. + +**Live E2E:** optional after the walking skeleton. If added, keep one cheap local live test that starts a conversation from `hello.canvas` and verifies the created payload/events show the extension context suffix. Follow existing live E2E rules under `tests/e2e/live/`. + +**Verification:** + +```sh +npm run typecheck && npm test && npm run build +``` + +## 9. Defer Until After Proof + +- Marketplace/catalog discovery. +- Sandboxed iframe runtime. +- Rich marketplace/catalog management for portable skills/plugins. +- Standalone MCP-template management. +- Package signing/provenance UI. +- Rich command palette integration. +- Agent-mediated installation. +- Automatic MCP installation. +- Per-run exact extension enablement. +- Shared React/design-system runtime imports for extensions. +- Remote/cloud delivery of local extension package contents. +- Arbitrary root-mounted route-less components. diff --git a/docs/ExtensionsSystemRFC.md b/docs/ExtensionsSystemRFC.md new file mode 100644 index 000000000..210859a57 --- /dev/null +++ b/docs/ExtensionsSystemRFC.md @@ -0,0 +1,1552 @@ +# RFC: Canvas Extensions + +Status: RFC, ready for review +Target: `OpenHands/agent-canvas` + +Current repo baseline: merged through `upstream/main` at `40844533`. The current package version is `1.0.0-rc.6`, `config/defaults.json` pins `agentServer` to `1.27.0`, `@openhands/extensions` is `0.4.0`, and `@openhands/typescript-client` is `1.24.3`. + + +## 1. Summary + +Agent Canvas should ship a first-class **Canvas Extensions** system: user-installable npm packages that extend the local Canvas experience and optionally contribute components to the agent runtime through already-supported Agent Server surfaces. + +A Canvas Extension can contribute UI views, left navigation entries, color themes, settings panels, conversation right panels, custom tool/event visualizers, launch templates, optional references to reusable agent plugins, MCP server templates, and system-prompt context blocks. Canvas Extensions are installed and managed by a small local Node service started by the `agent-canvas` launcher. Agent-side behavior is forwarded only through already-supported Agent Server surfaces such as `plugins`, MCP settings, and `AgentContext`; Canvas does not patch or load code inside the Agent Server. + +**MVP delivers:** + +- CLI install/enable/disable/remove for Canvas Extension packages, CLI install support for portable agent skills/plugins into `.agent` files, plus an Extensions page under Customize for Canvas Extension inventory and diagnostics. +- Trusted same-origin extension views and dev-mode authoring with live reload. +- Focused UI contribution surfaces: left navigation entries; color themes in Settings > Application > Color Theme; settings panels under an Extensions header; conversation right panels beside Files/Browser/Terminal; and tool visualizers that compose with the built-in registry from PR #1246. + +The tool visualizer contract follows the OpenHands agent team's direction (draft PR #1277 is the current lead) rather than a parallel Canvas-only renderer; see §12.9. The `agent-canvas` CLI becomes a small artifact installer for three distinct classes: Canvas Extensions, portable agent plugins, and portable skills. Canvas Extensions install into the Agent Canvas extension store; skills and plugins install into `.agent` files and remain portable agent artifacts, not Canvas Extensions. + +**Deferred:** marketplace, signing, sandboxed iframe views, broad root-mounted/shared-dependency runtimes, MCP-template installation/management, richer portable-skill/plugin marketplace UX, and agent-mediated installation. The manifest reserves a future iframe entry point (`browser.entry`) so stronger isolation can be added later without changing package shape; see §11.1. + +**Lifecycle tiers (capability-gated by launch mode).** Extension lifecycle has two tiers, selected by how Canvas was launched, not by a per-extension setting: + +- **Restart-bounded (default).** Packaged global installs, Docker images, and any static-served build are CLI-only: install, enable, disable, update, and remove run from the CLI, and changes apply on the next process start. The Extensions page is read-only inventory and diagnostics. This is the baseline assumed throughout the rest of this RFC. +- **Live management (development source stack only).** A stack started from a source checkout with `npm run dev` or `npm run dev:minimal` additionally gets live management. The launcher issues a `liveExtensionManagement` capability flag, the Customize > Extensions page exposes enable/disable controls, and newly installed extensions are picked up by re-reading the install store and refetching the registry — without a full Canvas restart. This is the mode in which an in-Canvas agent can build an extension and the user can enable it in the same session, mirroring how Canvas already lets users add an LLM profile and refresh what is available. + +Live management removes only the restart step for enable/disable and new-extension discovery on a trusted local dev machine. It does not relax the consent model, the permission re-approval rules, or the agent-runtime boundary: newly discovered extensions still arrive disabled, permission-expanding states still require review, and agent-side contributions (agent plugin references, context suffix) still apply only at conversation creation. See §15.2, §17, §20, §24, and §27 Decision 1. + +## 2. Motivation + +Today the Canvas surface area is fixed at build time. There is no supported path for users, partners, or our own product teams to ship a new launch template, a curated MCP setup, a UI view, or a packaged set of skills without modifying and rebuilding the application. This blocks three things we want: + +1. **Ecosystem.** Partners and the community cannot extend Canvas without forking. +2. **Vertical solutions.** We cannot ship product-specific launch flows (GitHub review, infra ops, data analysis) as composable add-ons that share a single distribution mechanism. +3. **Agent-built tools.** A future where an agent builds a Canvas extension during a conversation and proposes installing it is a meaningful product differentiator. Extensions are the substrate that makes that possible. + +The system must preserve the existing security and operational boundaries between Canvas (a local client + launcher) and the Agent Server (which may be local, remote, cloud, or ACP-backed). + +## 3. Product Vocabulary + +- **Canvas Extension** — an Agent Canvas-specific npm package with an `agent-canvas.extension.json` manifest. It extends the Agent Canvas UI, UX, and local product behavior. +- **Canvas Extension Host** — local Node HTTP service launched by `agent-canvas`; validates, serves, and projects installed Canvas Extension packages. +- **OpenHands Extensions Catalog** — the existing `@openhands/extensions` package/repository that provides shared OpenHands assets such as integrations/catalog data. It is not the Canvas Extension format and should not be treated as an installable Canvas Extension package by default. +- **Portable Agent Plugin** — a reusable agent artifact, usually rooted at `.plugin/plugin.json` or a compatible plugin manifest, that can contain skills, scripts/hooks, MCP configuration, agents, slash commands, or related agent runtime assets. Plugins are intended to work across agent tools such as Codex, Claude, Gemini, and OpenHands. The `agent-canvas` CLI may install them into `.agent` files, but they are not Canvas Extensions. +- **MCP Server** — external tool server config installed into agent settings. +- **Skill** — a portable agent skill, usually a `SKILL.md` artifact or a skill directory. Skills install into `.agent` files through the agent-artifact installer and are not Canvas Extensions. +- **Tool Visualizer** — a React renderer for one action/observation kind or one more specific tool variant, selected before the built-in markdown fallback. +- **Color Theme** — a Canvas Extension-provided theme definition that appears as an option in Settings > Application > Color Theme. Theme-only Canvas Extensions are valid and should not need to ship a view or settings panel. +- **Settings Panel** — a Canvas Extension-owned settings surface shown only under a visible Extensions header after the built-in settings sections. +- **Conversation Right Panel** — a Canvas Extension-owned panel in the conversation work area alongside built-in Files, Browser, and Terminal panels. + +User-facing UI should use "Canvas Extension" when the distinction matters, and may shorten to "Extension" only inside an already-labeled Canvas Extensions area. Avoid "plugin" in the Canvas product surface because portable agent artifacts already own that term. Avoid naming top-level app routes or hubs "Extensions" because the product already renamed that old section to Customize. + +## 4. Goals + +- Install from npm package names, version ranges, tarballs, or local paths. +- Works when the user globally installs Agent Canvas and runs `agent-canvas`. +- Supports CLI install/manage commands, persisted enablement, restart-bounded lifecycle changes, and a process-level diagnostic kill switch. +- In a development source stack (`npm run dev`), additionally supports live enable/disable from the Extensions page and live discovery of newly installed extensions without a full restart, so an in-Canvas agent can author an extension and the user can enable it in the same session. +- Management commands work from a globally installed npm package without starting the full stack and without requiring the static `build/` directory. +- Keeps the Agent Server unmodified for MVP. +- Installs portable skills/plugins without reclassifying them as Canvas Extensions or putting them in the Canvas Extension registry. +- Lets one Canvas Extension package ship UI plus agent-runtime descriptors. +- Third-party executable code is opt-in and auditable. +- Local development works without rebuilding Agent Canvas. +- Preserves current Skills, MCP, Launch, Automations, and Settings flows. + +## 5. Non-Goals + +- No extension may patch or monkeypatch the Agent Server. +- No extension may register server-side tools except through portable agent plugin and MCP mechanisms that the Agent Server already supports. +- No stable extension API may expose Canvas internals, parent React components, or DOM mutation hooks. MVP browser modules are trusted same-origin code, so DOM access is a trust concern rather than an enforceable sandbox boundary. +- No broad root-mounted route-less JavaScript surface lands before narrower slot APIs prove the need. Tool visualizers are the first route-less JS surface because the registry seam already exists and the fallback behavior is well defined. +- No repository-local files may modify the running Canvas browser experience simply because a conversation is attached to that repository. +- No npm install scripts run by default during extension installation. +- No portable skill/plugin installed through `agent-canvas` becomes a Canvas Extension, appears in the Extensions inventory, or gains Canvas UI contribution powers. They remain agent artifacts installed into `.agent` files. +- No standalone MCP template artifact becomes independently active in the first MVP slice; MCP-template install UX remains deferred. +- No public marketplace, ratings, payments, reviews, or remote trust service in the first PRs. + +## 6. Prior Art + +Two existing systems informed this design. + +**Hermes (Nous Research).** We adopt: explicit opt-in enablement, three lifecycle states (enabled/disabled/installed), schema-validated manifests with capability declarations, multiple narrow contribution surfaces instead of one all-powerful hook API, good diagnostics (`list`, `doctor`, debug logs, skipped reasons), and explicit secret/env declarations. We do **not** adopt Hermes' direct `ctx.register_tool()` model or its automatic loading of user/project code — Canvas should not be an Agent Server plugin loader, and tool override belongs in agent runtime governance. + +**Pi (pi.dev).** We adopt: a single generic `install` verb that detects artifact type, multiple authoring shapes (single directory, packaged module, package wrapping skills/agent plugins/MCP templates), a developer reload story for explicitly registered local extensions, and clear language that local extensions are code requiring trust. We do **not** adopt Pi's repository-auto-discovery of extensions, its in-process tool registration, or its built-in tool overriding — those collapse the local/remote runtime boundary that Canvas must preserve. + +A prior internal prototype (`dv/addons-clean-v1`) contributed the typed host API shape, manifest validation, stable extension IDs, route host with error boundary, sidebar ordering, and path-normalization patterns. It is not merge-ready: it assumes a Vite build-time registry, which a globally-installed CLI cannot regenerate per install. The current tree no longer has a `src/addons/` implementation to remove; PR 0 should use the `src/canvas-extensions/` namespace so neither the old `addons` vocabulary nor the old app-level "Extensions" section leaks back in. + +## 7. Architecture + +Three layers, each with a clear boundary: + +```mermaid +flowchart LR + User["User runs agent-canvas"] --> CLI["bin/agent-canvas.mjs"] + CLI --> Host["Canvas Extension Host (Node)"] + CLI --> AgentFiles["Portable agent config (.agent files)"] + CLI --> Ingress["Ingress proxy"] + CLI --> AS["Agent Server"] + CLI --> UI["Static or Vite Canvas frontend"] + + Ingress -->|"/api/canvas/canvas-extensions/*"| Host + Ingress -->|"/canvas-extension-assets/*"| Host + Ingress -->|"/api/automation/*"| Automation["Automation backend"] + Ingress -->|"/api/* and /sockets"| AS + Ingress -->|"/*"| UI + + UI -->|"registry, settings, live enable/rescan"| Host + UI -->|"create conversation with PluginSource[]"| AS + Host -->|"reads npm packages"| Store["~/.openhands/agent-canvas/installations"] +``` + +### 7.1 Extension Host (local Node service) + +The Extension Host runs in every launcher mode that supports extensions: the packaged `agent-canvas` CLI, `npm run dev`, `npm run dev:minimal`, and `npm run dev:static`. It owns Canvas Extension state only: + +- Canvas Extension package install/update/remove (always with `--ignore-scripts` by default). +- Manifest discovery and schema validation. +- Local package cache and `package-lock.json` for reproducibility. +- Enabled/disabled state. +- Extension settings and non-secret config. +- Serving extension browser assets. +- Serving a registry JSON document to the frontend. +- Producing launch contributions for the frontend. +- Diagnostics and logs. + +Portable skills and plugins use the CLI's agent-artifact installer instead. That installer writes/updates the appropriate `.agent` files and must not project those artifacts into the Canvas Extension registry or serve them as Canvas browser assets. + +The Extension Host **must not** proxy arbitrary requests to the Agent Server. When it exposes Agent Server data through the supported extension API, it does so through narrow, typed, permission-declared capabilities. + +### 7.2 Frontend Runtime + +The Canvas frontend owns: + +- Read-only extension inventory/diagnostics UI (the Extensions page). +- Navigation for extension views. +- Activation: importing each enabled extension's `browser.module` once at startup and calling `activate(context)` so route-less contributions register independent of view mounting (see §13.1). +- The extension view host that mounts trusted extension browser modules into Canvas-owned route containers (with a reserved future iframe runtime; see §12.1). +- Permission display, CLI management guidance, and secret setup prompts. +- Settings forms generated from extension JSON Schemas. +- Merging enabled extension launch contributions into conversation create payloads. +- Toasts, settings query invalidation, and error boundaries for extension surfaces. + +### 7.3 Agent Runtime (Agent Server) + +The Agent Server is unchanged for MVP. Canvas treats it as a black box and only uses documented request fields: + +- `Conversation(..., plugins=[PluginSource(...)])` via the create-conversation payload. +- `AgentContext.system_message_suffix`, already used by Canvas for the `` block ([src/api/agent-server-adapter.ts:91-225](src/api/agent-server-adapter.ts:91)). +- Existing MCP settings APIs. +- Existing skills APIs where supported. + +## 8. CLI UX + +Command dispatch is part of the release contract. `agent-canvas install`, `list`, `enable`, `disable`, `remove`, `update`, and `doctor` must parse and run before the launcher checks for a static `build/` directory or imports `scripts/dev-with-automation.mjs`. The full stack still requires packaged frontend assets, but management commands must work from a global npm install and from source checkouts that have not run `npm run build`. + +Default Canvas Extension lifecycle is CLI-only and restart-bounded. Users enable, disable, update, or remove Canvas Extensions from the CLI, then restart `agent-canvas`. In packaged, Docker, and static-served modes the Customize > Extensions page is read-only inventory and diagnostics; it does not install, enable, disable, update, or uninstall packages in the browser, and a running Canvas process does not promise live revocation after an out-of-band CLI state change. + +The same CLI install surface also handles portable agent skills and plugins. Those artifacts are installed into `.agent` files through an agent-artifact installer and stay portable across agent tools; they are not enabled through the Extensions page, not served by the Extension Host, and not granted Canvas UI contribution APIs. If an install target is ambiguous, the CLI should require an explicit target/scope before mutating `.agent` files. + +**Development source stack exception.** When Canvas is launched with `npm run dev` or `npm run dev:minimal`, the launcher issues the `liveExtensionManagement` capability and the Extensions page additionally exposes enable/disable controls plus discovery of newly installed extensions without a restart (see §15.2, §17, §24). Install, update, and remove remain CLI verbs even in this mode; live management covers enable/disable and registry discovery, which are the steps an in-Canvas agent authoring loop needs. The CLI verbs below behave identically in both tiers. + +### Global install and launch + +```sh +npm install -g @openhands/agent-canvas +agent-canvas +``` + +### Install + +`agent-canvas install` is a single generic verb that detects what is being installed and routes through the matching installer. + +```sh +agent-canvas install @acme/agent-canvas-github +agent-canvas install @acme/agent-canvas-github@1.2.3 +agent-canvas install ./local-extension +agent-canvas install ../my-extension --dev +agent-canvas install ./agent-plugin +agent-canvas install ./skills/codeguard +``` + +Detection order: + +1. `package.json` has `agentCanvas.manifest` or an `agent-canvas.extension.json` file is present → Canvas Extension. +2. `.plugin/plugin.json`, `.claude-plugin/plugin.json`, or another supported portable plugin manifest is present → portable agent plugin. +3. `SKILL.md` is present → portable skill. +4. Future MCP template manifest → MCP template. +5. Multiple markers present → the explicit Canvas Extension manifest wins for Canvas Extension installation. Any bundled plugin/skill files remain separate agent-runtime assets and are only used when the Canvas Extension manifest declares an agent contribution. + +MVP behavior: + +- Canvas Extension artifacts install into `~/.openhands/agent-canvas/installations/` and can be enabled/disabled through Canvas Extension lifecycle commands. +- Portable agent plugins and skills install into the appropriate `.agent` files and are listed/diagnosed as agent artifacts, not as Canvas Extensions. +- MCP-template artifacts can be detected but return a typed "detected but unsupported in MVP" diagnostic until their install UX is designed. + +For Canvas Extensions, install validates the manifest, shows permissions, installs with scripts denied by default, and enables the extension after consent. Non-interactive flags: + +```sh +agent-canvas install @acme/agent-canvas-github --yes +agent-canvas install @acme/agent-canvas-github --no-enable +agent-canvas install @acme/agent-canvas-github --install-scripts=allow +``` + +Default is `--install-scripts=deny` (npm `--ignore-scripts`). + +### Management + +```sh +agent-canvas list +agent-canvas list canvas-extensions +agent-canvas list agent-artifacts +agent-canvas list plugins +agent-canvas list skills +agent-canvas enable acme.github +agent-canvas disable acme.github +agent-canvas remove acme.github +agent-canvas update acme.github +agent-canvas doctor +``` + +These commands operate without starting the full UI stack. `enable`/`disable` are Canvas Extension lifecycle commands. Portable skills/plugins are active according to the `.agent` file they were installed into; the CLI should list, diagnose, update, and remove them through the agent-artifact path without projecting them into the Canvas Extension registry. + +### Process kill switch + +MVP enablement is persistent only: an artifact is active when it is installed and enabled in `config.json`. The one process-local exception is a support/debugging kill switch: + +```sh +agent-canvas --disable-extensions +``` + +This flag starts Canvas with all extension contributions ignored for that process. It does not create registry state, does not mutate `config.json`, and does not support selective run-only enablement. Per-run exact sets (`--extensions ...`), single-extension run overrides, and `install --run-only` are deferred until there is a concrete test/support workflow that needs them. + +## 9. Canvas Extension Install Store + +Canvas already stores client state at `~/.openhands/agent-canvas/` (session API keys, conversations, workspaces, automations DB, bash events). Canvas Extension packages live as a sibling so users have one obvious place to inspect and back up Canvas-specific extension state: + +```text +~/.openhands/agent-canvas/installations/ + package.json + package-lock.json + config.json + artifacts.json + logs/ + dev/ + node_modules/ +``` + +`package.json` is a private package used only to install npm-backed Canvas Extensions. `artifacts.json` records installed Canvas Extension package/path, manifest location, state, and diagnostics. `config.json` records Canvas Extension enable/disable state and non-secret settings: + +```json +{ + "schemaVersion": 1, + "enabled": { "extensions": ["acme.github"] }, + "disabled": { "extensions": [] }, + "settings": { "acme.github": { "defaultBranch": "main" } } +} +``` + +Secrets are never stored in this file. They are saved through the existing Agent Server secrets service. + +Portable skills and plugins do not live in this store as Canvas Extension artifacts. Their installer writes to `.agent` files using the agent-artifact path described in §8, and `doctor` may report on them separately. + +**Concurrency and atomic writes.** Canvas writes the store's JSON files (`config.json`, `artifacts.json`) with temp-file-then-rename and reads each in a single read, so a reader never observes a partial write. Multi-step Canvas Extension transactions — install, update, remove — take an exclusive lock at `installations/.lock` so a CLI mutation and a running Host's `rescan` cannot interleave mid-transaction; npm's own `package-lock.json` covers the dependency tree. `rescan` reads a consistent snapshot and, if the lock is held, briefly waits and retries or reports `install in progress` rather than reading torn state. This path is exercised by design: the agent-authoring loop runs a CLI install while the Host is live, then rescans. + +All installation is local to the Agent Canvas client. `agent-canvas install` never installs into a remote Agent Server, cloud sandbox, or team backend. + +## 10. Npm Package Contract + +Each extension package exposes its manifest through `package.json`: + +```json +{ + "name": "@acme/agent-canvas-github", + "version": "0.1.0", + "type": "module", + "agentCanvas": { "manifest": "./agent-canvas.extension.json" }, + "files": ["agent-canvas.extension.json", "dist", "agent"] +} +``` + +The manifest path must resolve inside the package root. Path traversal is rejected. + +### 10.1 Agent Canvas package release contract + +The host package must publish everything the global CLI and extension authors need: + +- `bin/agent-canvas.mjs`, launcher scripts, static `build/`, and any Extension Host scripts must be included by `package.json#files`. +- Extension management author types must be exported as `@openhands/agent-canvas/canvas-extensions`. Visualizer authoring types must follow the OpenHands agent-team-approved export from PR #1277 or its successor; the earlier placeholder was `@openhands/agent-canvas/canvas-visualizers`, but that name should not be frozen until the visualizer API decision is made. The release build must emit `dist/extensions/*` plus matching visualizer output and add matching subpath exports. +- If JSON Schemas are advertised by URL or package path, the schemas must either be served remotely or included in the npm package. Do not point authors at files that are excluded by `package.json#files`. +- Example extensions under `examples/` are source-repo fixtures unless `package.json#files` explicitly includes them. The acceptance path for a globally installed package should use either a packed fixture tarball or a separately published example package, not an unpublished repo-local path. +- Every release candidate should run `npm pack --dry-run` or an equivalent packed-tarball smoke so missing `build`, `scripts`, `dist/extensions`, `dist/visualizers`, or schema files are caught before publish. + +## 11. Manifest Schema + +`agent-canvas.extension.json`: + +```json +{ + "$schema": "https://schemas.openhands.dev/agent-canvas/extension.v1.json", + "schemaVersion": 1, + "id": "acme.github", + "displayName": "GitHub Workspace Tools", + "version": "0.1.0", + "description": "GitHub launch helpers, MCP setup, and PR review workflows.", + "publisher": { "name": "Acme", "url": "https://example.com" }, + "license": "MIT", + "repository": "https://github.com/acme/agent-canvas-github", + "compatibility": { + "agentCanvas": ">=1.0.0-rc.6 <2.0.0", + "extensionApi": 1 + }, + "activationEvents": ["onStartup"], + "permissions": { + "ui": ["views", "leftNavigation", "colorThemes", "settingsPanels", "conversationPanels", "toolVisualizers"], + "agent": ["plugins", "context"], + "mcp": ["templates"], + "network": ["https://api.github.com"] + }, + "browser": { + "module": "./dist/index.js" + }, + "contributes": { + "views": [ + { + "id": "dashboard", + "title": "GitHub", + "route": "/github", + "navigation": { + "location": "primarySidebar", + "slot": "afterAutomations", + "order": 300, + "icon": "./dist/github.svg" + } + } + ], + "colorThemes": [ + { + "id": "acme.github.midnight", + "label": "GitHub Midnight", + "scale": { + "--cool-grey-950": "#080B12", + "--cool-grey-900": "#111827" + }, + "heroui": { + "--heroui-background": "222 40% 5%", + "--heroui-foreground": "216 26% 82%" + }, + "tokens": { + "--oh-color-primary": "#7DD3FC" + } + } + ], + "settingsPanels": [ + { + "id": "github-settings", + "title": "GitHub", + "viewId": "dashboard", + "order": 200 + } + ], + "conversationRightPanels": [ + { + "id": "github-pr-context", + "title": "PR Context", + "viewId": "dashboard", + "icon": "./dist/github.svg", + "order": 200 + } + ], + "toolVisualizers": [ + { + "id": "github-pr-check", + "module": "./dist/github-pr-check-visualizer.js", + "actionKinds": ["MCPToolAction"], + "observationKinds": ["MCPToolObservation"] + } + ], + "agentPluginReferences": [ + { "id": "github-pr-review", "source": "./agent/github-pr-review", "autoInclude": "manual" } + ], + "mcpServers": [ + { + "id": "github", + "title": "GitHub", + "template": { "kind": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] }, + "requiredSecrets": ["GITHUB_TOKEN"] + } + ], + "launchTemplates": [ + { + "id": "review-pr", + "title": "Review a Pull Request", + "prompt": "Review the selected pull request and propose changes.", + "requiredAgentPluginReferences": ["github-pr-review"], + "requiredMcpServers": ["github"] + } + ], + "conversationContext": [ + { + "id": "github-guidance", + "title": "GitHub workflow guidance", + "autoInclude": "manual", + "content": "When working with GitHub, prefer the configured GitHub MCP server for issue and pull request metadata." + } + ] + }, + "configuration": { + "properties": { + "defaultBranch": { "type": "string", "default": "main", "title": "Default branch" } + } + }, + "secrets": [ + { + "name": "GITHUB_TOKEN", + "title": "GitHub token", + "description": "Used by the GitHub MCP server.", + "url": "https://github.com/settings/tokens", + "secret": true + } + ] +} +``` + +**Required fields:** `schemaVersion`, `id`, `displayName`, `version`, `compatibility.agentCanvas`, `compatibility.extensionApi`. + +**ID rules:** `^[a-z0-9][a-z0-9._-]*[a-z0-9]$`. Package names and extension IDs need not match. Duplicate enabled IDs are rejected. + +### 11.1 `browser` runtime modes + +The `browser` declaration accepts one of two shapes. The shape selects the runtime mode; the two are mutually exclusive and the validator rejects manifests that set both. + +| Field | Status | Runtime | +|---|---|---| +| `browser.module` | **Active (MVP).** | Browser-ready ESM module loaded into a Canvas-owned DOM container. The extension exports a `mount()` entry point and owns its rendering stack. | +| `browser.entry` | **Reserved, not yet supported.** | Future HTML document loaded into a sandboxed iframe (`sandbox="allow-scripts allow-forms allow-downloads"`, no `allow-same-origin`) with a postMessage RPC bridge mirroring the same context API. | + +The MVP implements only `browser.module`. The `browser.entry` field is reserved in the schema from day one so that a future iframe runtime can ship without a manifest schema bump, and so that extension authors can read the design intent today. + +**Why reserve `browser.entry` if the MVP only uses `browser.module`?** Same-origin browser modules are the smallest useful runtime for first-party and trusted partner extensions: they work in the globally installed CLI, avoid postMessage infrastructure, and let extension authors choose their own bundler/framework. They are **not** a sandbox. Once enabled, inline browser code should be treated as trusted local code with the same browser-origin privileges as Canvas itself (see §19). An iframe sandbox becomes valuable in two future scenarios we want to leave room for: + +1. A community extension marketplace where untrusted third-party code needs stronger isolation than install-time consent provides. +2. Extensions that intentionally ship their own framework or rendering stack and benefit from owning their own document. + +Designing the manifest, asset-serving routes, and runtime API around an async, host-mediated surface from the start means the iframe runtime is a future additive PR — not a redesign. Specifically: + +- The API in §13 is defined async-only, so the same interface is satisfiable by direct function calls (inline mode) or by a postMessage proxy (iframe mode). +- The `/canvas-extension-assets/:id/*` asset-serving route already exists in §17 and serves whichever bundle the extension declares. +- Manifest validation in PR 0 will accept `browser.entry` as a syntactically valid field but emit a validation error (`browser.entry is reserved and not yet supported`) until the iframe runtime ships, preventing accidental shipping of extensions that depend on an unimplemented mode. + +This is the minimum coverage needed today to keep the iframe option open later at low cost. + +## 12. Contribution Types + +### 12.1 `views` + +Routes rendered by Canvas at `/canvas-extensions/:extensionId/:viewId/*`. + +Views may optionally declare a navigation entry: + +```json +{ + "navigation": { + "location": "primarySidebar", + "slot": "afterAutomations", + "order": 300, + "icon": "./dist/cost-dashboard.svg" + } +} +``` + +MVP navigation locations: + +- `primarySidebar` — top-level left navigation entry. MVP supports the `afterAutomations` slot, rendered after Automations and before the conversation list. This covers dashboard-style Canvas Extensions such as a Cost Dashboard. +- `customize` — entry inside the existing Customize area. Use this for management/configuration views or lower-priority extension pages. Do not reintroduce a top-level "Extensions" hub; the old Extensions section has been renamed to Customize. + +Primary sidebar entries are for user-facing app surfaces, not transient launch templates. They must point at a Canvas Extension view route, use a static icon asset from the extension package, and remain disabled when the extension is disabled or invalid. If multiple enabled Canvas Extensions contribute to the same slot, Canvas sorts by `order`, then extension display name, then view ID for deterministic rendering. + +### 12.2 `colorThemes` + +Color themes let a Canvas Extension add one or more theme options to Settings > Application > Color Theme. A theme-only Canvas Extension is valid: it may contribute `colorThemes` and no views, settings panels, right panels, tool visualizers, or agent-runtime behavior. + +Manifest contribution shape: + +```json +{ + "contributes": { + "colorThemes": [ + { + "id": "acme.github.midnight", + "label": "GitHub Midnight", + "scale": { + "--cool-grey-950": "#080B12", + "--cool-grey-900": "#111827" + }, + "heroui": { + "--heroui-background": "222 40% 5%", + "--heroui-foreground": "216 26% 82%" + }, + "tokens": { + "--oh-color-primary": "#7DD3FC" + } + } + ] + } +} +``` + +Rules: + +- Extension color themes feed the same Settings > Application > Color Theme dropdown used by built-in themes. They should not create a separate settings panel or custom picker. +- Canvas owns theme selection, persistence, application, fallback, and validation. Extensions contribute theme definitions only. +- MVP theme definitions should align with the current `ColorThemeDefinition` shape in [src/themes/color-themes.ts](../src/themes/color-themes.ts): `label`, `scale` CSS custom property overrides, HeroUI HSL-channel overrides, and optional semantic `tokens`. +- Canvas should validate allowed CSS custom property keys rather than allowing arbitrary global CSS. This keeps theme contributions narrower than same-origin browser modules even though the extension package itself remains trusted local code. +- If the active theme belongs to a disabled, removed, or invalid extension, Canvas falls back to the default built-in color theme and records a diagnostic. +- Duplicate theme IDs are rejected. Theme IDs should be namespaced by extension ID or package domain for predictable persistence. + +### 12.3 `settingsPanels` + +Settings panels let a Canvas Extension add configuration UI without owning a full route. They are intentionally constrained to one visible location: after all built-in settings sections, under an explicit **Extensions** header inside the current settings experience. + +Manifest contribution shape: + +```json +{ + "contributes": { + "settingsPanels": [ + { + "id": "acme.github.settings", + "title": "GitHub", + "viewId": "dashboard", + "order": 200 + } + ] + } +} +``` + +Rules: + +- Canvas Extension settings panels never appear intertwined with built-in settings. They render only under the visible Extensions header after other settings. +- The manifest declares panel metadata only: ID, title, order, and optional icon/display hints. The extension browser module registers the implementation for that declared panel ID at runtime through the settings-panel registry. Missing registrations produce diagnostics and an empty/error panel body rather than breaking Settings. +- Canvas owns the settings chrome, heading, save/cancel affordances, loading state, and error boundary. +- Settings panels receive only extension metadata/settings, schema-derived values, and host APIs for `readExtensionSettings()` / `patchExtensionSettings()`. They do not receive raw global settings stores. +- If an extension is disabled or invalid, its settings panel is omitted and its persisted settings remain in the install store until removal/purge. + +### 12.4 `conversationRightPanels` + +Conversation right panels let a Canvas Extension add a work surface beside the built-in Files, Browser, and Terminal panels. They are for conversation-adjacent tools such as PR context, cost dashboards, deployment status, or domain-specific inspectors. + +Manifest contribution shape: + +```json +{ + "contributes": { + "conversationRightPanels": [ + { + "id": "acme.github.pr-context", + "title": "PR Context", + "viewId": "dashboard", + "icon": "./dist/github.svg", + "order": 200 + } + ] + } +} +``` + +Rules: + +- Panels appear in the existing conversation right-panel/tab system alongside Files, Browser, and Terminal, not as arbitrary overlays or app-root components. +- Panels receive active conversation ID/status, selected workspace summary, backend summary, and narrow host APIs for navigation/toasts/settings. +- Canvas owns panel tab placement, collapsed/expanded behavior, focus management, loading state, and error boundaries. +- Panels must degrade cleanly when no conversation is active, when the extension is disabled, or when the current runtime does not support an agent-side contribution. +- The manifest declares panel metadata only. The extension browser module registers the implementation for the declared panel ID at runtime through the conversation-panel registry. Missing registrations produce diagnostics and leave the panel unavailable. + +**MVP runtime: trusted same-origin browser module.** The extension's `browser.module` is loaded by the Canvas frontend with dynamic `import()` and mounted into a Canvas-owned DOM container at the view route. The extension exports a `mount()` function, receives the typed context (§13), and owns rendering inside that container. It may use React, Svelte, vanilla DOM, or another framework as long as the shipped module is browser-ready. It should use Canvas CSS variables/design tokens and host-provided APIs instead of importing Canvas internals. + +Inline extension code runs in the same browser origin as Canvas. That is intentional for MVP, and it means runtime capability declarations are a consent, audit, and UX contract rather than a hard browser sandbox. A trusted inline extension can technically call same-origin routes if it has access to the active session credentials, inspect browser state available to Canvas, and manipulate page DOM. The Extensions UI must communicate that trust model clearly before enablement. Strong isolation is deferred to the future `browser.entry` iframe runtime. + +**Browser module packaging contract.** + +- `browser.module` must point to a built ESM file that a browser can import directly. +- The Extension Host serves static files only; it does not transpile TypeScript/JSX or run bundlers for installed packages. +- Runtime imports must be relative URLs inside the extension asset tree or bundled into the module. Bare runtime imports such as `react`, `@heroui/react`, `@openhands/agent-canvas/canvas-extensions`, or the final visualizer authoring subpath are rejected for MVP because the packaged CLI cannot guarantee a Vite resolver or shared dependency graph. +- Type-only imports from `@openhands/agent-canvas/canvas-extensions` are allowed in author source and erased by the extension's build. Author source may import the final visualizer authoring subpath, but emitted browser modules must bundle those helpers rather than leaving a bare package import. +- The default export shape is: + +```ts +export default { + async mount({ root, context }: AgentCanvasExtensionMountParams) { + // Render into `root`. + return { + dispose() { + // Optional cleanup. + }, + }; + }, +}; +``` + +This DOM-island contract avoids React shared-instance problems in static builds while keeping the future iframe API close: the same async context can later cross a postMessage boundary. + +**Reserved future runtime: sandboxed iframe.** The manifest reserves `browser.entry` (§11.1) for a future iframe runtime intended for untrusted third-party extensions (e.g., a community marketplace). The iframe sandbox would be `sandbox="allow-scripts allow-forms allow-downloads"` with no `allow-same-origin`, communicating with the parent through a postMessage RPC bridge that exposes the same context API. This is not implemented in MVP; declaring it now keeps the option open without committing implementation cost today. + +### 12.5 `agentPluginReferences` + +Optional references from a Canvas Extension to separate reusable agent plugins. They map to Agent Server `PluginSource` values at conversation launch time; they are not Canvas Extensions, are not managed by the Extensions page as standalone artifacts, and should remain installable through the portable agent-artifact path when used on their own. Fields: `id`, `source` (package-relative path, installed-plugin ID/source, GitHub shorthand, Git URL, or absolute path for dev mode), `ref` (optional branch/tag/commit), `repoPath` (optional subdirectory for remote git/GitHub sources), `autoInclude` (`manual` | `enabled` | `always`; default `manual`). + +Resolution: + +- Package-relative paths are converted by the Extension Host to absolute paths inside the installed npm package. +- Remote sources pass through as Agent Server `PluginSource`. +- Local paths are allowed only when explicitly installed with `--dev` or through `agent-canvas install `. +- `repoPath` maps to `PluginSource.repo_path` and must be a relative path with no traversal. For package-relative/local sources, resolve `source` directly to the plugin root and omit `repo_path`; local sources should not rely on `repo_path`. +- The resolved plugin root must contain a supported portable agent plugin package shape (`.plugin/plugin.json` or `.claude-plugin/plugin.json`, plus optional `skills/`, `hooks/`, `.mcp.json`, `agents/`, and `commands/`). + +The current frontend `PluginSpec.parameters` field is collected by `/launch` but `buildConfiguredConversationSettings()` sends only `source`, `ref`, and `repo_path` in the create-conversation payload ([src/api/agent-server-adapter.ts](src/api/agent-server-adapter.ts)). MVP extension behavior must not depend on arbitrary plugin parameters. + +**ACP constraint.** Portable agent plugins can merge skills, MCP config, hooks, and plugin agents before agent initialization. MCP config and other non-ACP-compatible fields can cause `ACPAgent` initialization to fail. MVP should skip `agentPluginReferences` for ACP runtimes unless the plugin-reference smoke proves a prompt/skills-only plugin is accepted; extension context suffixes remain the compatible path for ACP. + +### 12.6 `mcpServers` + +Extensions provide MCP templates, never silent installs. The UI shows required MCPs before launch and reuses the existing `InstallServerModal` pattern. + +**ACP constraint.** [src/routes/mcp.tsx](src/routes/mcp.tsx) currently disables MCP management while an ACP agent is active because the Agent Server rejects `mcp_config` on `ACPAgent` init. Extension MCP contributions inherit this guard. + +**Current catalog shape.** The MCP marketplace now imports `IntegrationCatalogEntry` from `@openhands/extensions/integrations`, not the old `mcps` export. Extension MCP templates should either use the same `IntegrationTransport` vocabulary or provide an adapter that can project templates into the existing MCP install modal without reintroducing a parallel catalog model. The current install flow can save required secrets through the existing secrets-aware MCP form; extension templates should reuse that path rather than inventing separate secret persistence. + +### 12.7 `launchTemplates` + +Reusable launch presets shown on home, extension pages, or automation setup. May reference required MCP IDs, required agent plugin reference IDs, optional context blocks, initial prompt text, and workspace requirements. + +### 12.8 `conversationContext` + +Context blocks may be appended to `AgentContext.system_message_suffix` only after explicit user enablement. Canvas renders all extension context inside a single block, appended after the existing `` block: + +```text + +The user enabled the following Canvas Extensions for this conversation. + +* acme.github/github-guidance + When working with GitHub, prefer the configured GitHub MCP server for issue and pull request metadata. + +``` + +Extension context **must not** be merged into `conversationInstructions`; that turns extension guidance into user-message content. An explicit `extensionSystemSuffix` option will be added to the conversation adapter so runtime services and extension context compose in one place. + +### 12.9 `toolVisualizers` + +Custom event rendering is the first concrete route-less JavaScript extension surface. PR #1246 added the internal `src/components/features/chat/tool-visualizers` registry and dispatcher: `getEventContent()` asks the registry for a React body by action/observation kind, and falls back to the existing markdown renderer when no visualizer matches. Extensions should build on that work instead of introducing an unrelated event-rendering model. + +The final extension-facing visualizer contract is owned by the OpenHands agent team. Draft PR #1277 is the current lead implementation to follow where possible: a narrow registration API, custom/addon visualizers composed before built-ins, built-ins before primitive markdown/default fallback, `matches()` for narrowing, error-boundary fallthrough, and no `priority` field. If the agent team changes that API before it lands, the Canvas Extensions RFC and implementation should follow their approved shape rather than preserving the older Canvas-only proposal below. + +Manifest contribution shape: + +```json +{ + "contributes": { + "toolVisualizers": [ + { + "id": "acme.kubernetes.apply", + "module": "./dist/kubernetes-apply-visualizer.js", + "actionKinds": ["MCPToolAction"], + "observationKinds": ["MCPToolObservation"] + } + ] + } +} +``` + +Runtime registration shape (registered during `activate()`; see §13.1): + +```ts +export default { + activate(context) { + context.visualizers.registerToolVisualizer({ + id: "acme.kubernetes.apply", + actionKinds: ["MCPToolAction"], + observationKinds: ["MCPToolObservation"], + matches({ action }) { + return ( + action?.action.kind === "MCPToolAction" && + action.action.tool_name === "kubectl_apply" + ); + }, + Body({ action, observation }) { + return ; + }, + }); + }, +}; +``` + +The visualizer body shape follows the agent-team-approved API (§28); the `activate()`-based registration is the Canvas-side contract regardless of what #1277 settles on. + +Selection rules: + +- Extension visualizers are considered before built-in visualizers. +- Ordering should follow the agent-team-approved registry. Current PR #1277 uses latest registration first for custom/addon visualizers and intentionally has no `priority` field. +- `matches()` narrows within a kind so one extension can override a specific MCP tool without replacing all `MCPToolAction` rendering. +- If an extension visualizer throws, Canvas catches the error, reports a local diagnostic, and tries the next matching visualizer before falling back to the built-in renderer or markdown fallback. +- Visualizer modules use the same trusted local browser-code model as `browser.module`; no sandbox claim is made in MVP. + +Public visualizer authoring types and primitives should be exported from the OpenHands agent-team-approved visualizer subpath, separate from the broader `@openhands/agent-canvas/canvas-extensions` management contract. PR #1277 currently proposes `@openhands/agent-canvas/visualizers`; earlier Canvas planning used `@openhands/agent-canvas/canvas-visualizers`. Follow the approved PR #1277 successor rather than adding a second public visualizer API. The public surface should wrap or re-export stable versions of `defineVisualizer`, `VisualizerProps`, shared primitives such as code blocks/diff/output panes, and action/observation kind types. Do not expose internal file paths under `src/components/features/chat/tool-visualizers/*` as the authoring API. + +### 12.10 Deferred Generic Conversation Slots + +Generic conversation slots remain deferred. The concrete MVP surfaces are `conversationRightPanels` and `toolVisualizers`, because they map to existing product regions and existing renderer architecture. Header actions, footer widgets, badges, or arbitrary conversation sidebar content should not ship until there is a specific user workflow that cannot be handled by a right panel or a tool visualizer. + +### 12.11 `configuration` and `secrets` + +`configuration` uses JSON Schema; values are stored in the Extension Host's `config.json`. `secrets` are declarations only; values are stored through the existing `SecretsService`. + +## 13. Extension Runtime API + +Extensions receive a typed context object exposing host-mediated operations. A `browser.module` has one default export with two optional entry points: `activate(context)`, called once when the extension loads, and `mount(params)`, called each time one of its views opens. The API is defined async-only so a future iframe runtime can satisfy the same interface via a postMessage RPC bridge without changing extension authoring. + +MVP module shape: + +```ts +interface AgentCanvasExtensionMountParams { + root: HTMLElement; + context: AgentCanvasExtensionContext; +} + +interface AgentCanvasExtensionDisposable { + dispose(): void | Promise; +} + +interface AgentCanvasExtensionModule { + // Called once when the extension is activated. Register route-less + // contributions here (tool visualizers, color themes, settings panels, + // conversation right panels) via the context registries. The returned + // disposable, if any, is called on disable/remove/dev-remount. + activate?( + context: AgentCanvasExtensionContext, + ): + | void + | AgentCanvasExtensionDisposable + | Promise; + + // Called each time one of the extension's views is opened at its route. + // Renders into the Canvas-owned container. Omit if the extension has no views. + mount?( + params: AgentCanvasExtensionMountParams, + ): + | void + | AgentCanvasExtensionDisposable + | Promise; +} +``` + +A module must export at least one of `activate`/`mount`. A theme-only package needs **neither** — its themes come from manifest projection (§12.2) and it may ship no `browser.module` at all. + +MVP API surface: + +```ts +interface AgentCanvasExtensionContext { + extension: { id: string; version: string; settings: Record }; + theme: { colorScheme: "dark" | "light" }; + navigation: { + navigate(to: string, options?: { replace?: boolean }): Promise; + openExternal(url: string): Promise; + }; + ui: { + toast(message: string, options?: { tone?: "success" | "error" }): Promise; + }; + visualizers: { + registerToolVisualizer( + visualizer: AgentCanvasToolVisualizer, + ): Promise; + }; + colorThemes: { + registerColorTheme( + theme: AgentCanvasColorThemeDefinition, + ): Promise; + }; + settingsPanels: { + registerSettingsPanel( + panel: AgentCanvasSettingsPanel, + ): Promise; + }; + conversationPanels: { + registerRightPanel( + panel: AgentCanvasConversationRightPanel, + ): Promise; + }; + conversations: { + list(): Promise; + current(): Promise; + }; + launch: { + startConversation(options: unknown): Promise<{ conversationId: string }>; + }; + settings: { + readExtensionSettings(): Promise>; + patchExtensionSettings(value: Record): Promise; + }; +} +``` + +For the first implementation slice, the route-less registration APIs are limited to the agent-team-approved visualizer registration API, `colorThemes.registerColorTheme()`, `settingsPanels.registerSettingsPanel()`, and `conversationPanels.registerRightPanel()`. Manifest-projected `contributes.colorThemes` should be enough for most theme-only packages; the runtime API exists for the same lifecycle/disposal model as other UI registries. Settings and conversation panel manifests declare metadata; runtime registration binds that metadata to implementations by contribution ID. There is **no** supported generic `agentServer.request(path)`. The repo already routes Agent Server traffic through typed service wrappers; the extension surface follows the same pattern with named, permission-declared capabilities. For MVP inline browser modules, this is an API support boundary rather than a browser security boundary (see §19). + +### 13.1 Activation lifecycle + +Route-less contributions must register independent of whether any view is open, so activation is a global step, not part of the view host: + +- On app startup, Canvas reads the registry and, for each enabled extension whose manifest declares a `browser.module`, imports the module once and calls `activate(context)`. Registrations populate the visualizer, color-theme, settings-panel, and right-panel registries. +- `mount(params)` is called lazily by the view host when a view route is navigated to, and its disposable is called on unmount/remount. +- In live mode (§15.2), enabling an extension triggers an `activate()`; disabling calls the `activate()` disposable to unregister its route-less contributions, then unmounts any open view. +- Activation failures are caught per-extension: a throwing `activate()` records a diagnostic and disables that extension's contributions without affecting other extensions or the rest of Canvas. + +## 14. Shared Type Contract + +Initial home: `src/canvas-extensions/types.ts`, exported as `@openhands/agent-canvas/canvas-extensions` once stable. (PR 0 will collapse generic installable-artifact types and extension-specific types into a single directory; splitting them across `src/installations/` and `src/canvas-extensions/` is premature before any consumer exists.) + +Initial types: + +```ts +export interface AgentCanvasExtensionManifest { + schemaVersion: 1; + id: string; + displayName: string; + version: string; + description?: string; + publisher?: { name: string; url?: string }; + compatibility: { agentCanvas: string; extensionApi: number }; + permissions?: ExtensionPermissions; + browser?: ExtensionBrowserContribution; + contributes?: ExtensionContributions; + configuration?: ExtensionConfigurationSchema; + secrets?: ExtensionSecretDeclaration[]; +} + +export interface AgentCanvasExtensionMountParams { + root: HTMLElement; + context: AgentCanvasExtensionContext; +} + +export interface AgentCanvasExtensionDisposable { + dispose(): void | Promise; +} + +export interface AgentCanvasExtensionModule { + activate?( + context: AgentCanvasExtensionContext, + ): + | void + | AgentCanvasExtensionDisposable + | Promise; + mount?( + params: AgentCanvasExtensionMountParams, + ): + | void + | AgentCanvasExtensionDisposable + | Promise; +} + +export interface AgentCanvasColorThemeDefinition { + id: string; + label: string; + scale: Record; + heroui: Record; + tokens?: Record; +} + +export type InstallableArtifactKind = + | "canvas-extension" + | "agent-plugin" + | "skill" + | "mcp-template"; + +export interface DetectedInstallableArtifact { + id: string; + kind: InstallableArtifactKind; + source: string; + diagnostics: ExtensionDiagnostic[]; +} + +export interface CanvasExtensionRegistryEntry { + id: string; + kind: "canvas-extension"; + packageName: string; + version: string; + state: "enabled" | "disabled" | "installed" | "invalid"; + manifest: AgentCanvasExtensionManifest; + assetBaseUrl?: string; + diagnostics: ExtensionDiagnostic[]; +} +``` + +`registerTool`, arbitrary app-root components, raw store/query-client access, and Canvas-internal imports are intentionally absent from the MVP public contract. + +## 15. Permissions Model + +Permission groups: + +- `ui.views` — render trusted same-origin browser-module views. +- `ui.leftNavigation` — add declared left navigation entries that point at Canvas Extension views. +- `ui.colorThemes` — add validated color theme definitions to Settings > Application > Color Theme. +- `ui.settingsPanels` — add settings panels only under the visible Extensions header after built-in settings. +- `ui.conversationPanels` — add right-side conversation panels alongside Files, Browser, and Terminal. +- `ui.toolVisualizers` — register custom action/observation renderers in the conversation event pipeline. +- `agent.plugins` — reference separate portable agent plugins in conversation launches through Agent Server `PluginSource`. +- `agent.context` — append extension context to system suffix. +- `mcp.templates` — offer MCP server templates. +- `secrets.declare` — request secret setup. +- `network` — list of external origins the extension says it may contact (advisory; not strictly enforced by CSP in MVP). + +User consent shows: package name + version, extension ID, publisher, repository, enabled contribution types, required secrets, whether trusted same-origin browser code is included, whether agent plugin references or context blocks can affect new conversations, whether the contribution works only with a local Agent Server, and any new or expanded permissions on update. + +### 15.1 Extension States + +Every Canvas Extension in the registry has exactly one persisted state: + +| State | Active by default? | Typical entry point | +|---|---|---| +| `enabled` | Yes | Consent flow at install, or `agent-canvas enable ` | +| `disabled` | No | `agent-canvas disable `, install with `--no-enable`, or a permission-expanding update that drops the extension back to manual re-approval | +| `installed` | No | Installed but never enabled | +| `invalid` | No | Manifest failed validation; `diagnostics[]` on the entry explains why | + +**Conflict rule.** Disabled wins over enabled if a manual config edit creates both persisted entries for the same ID. The process-level `--disable-extensions` kill switch suppresses all extension loading for the current run but does not create a registry state. + +**"Needs review" is not a separate state.** The persisted `state` enum is exactly `enabled | disabled | installed | invalid`. "Needs review" is a presentation grouping for a `disabled` (or `installed`) entry carrying a `needs-review` diagnostic — produced by a permission-expanding update (§20) or a permission-expanding dev manifest change (§21). The registry `state` gains no fifth value; UI filters group by the diagnostic, and re-approval clears it so the user re-enables normally. + +### 15.2 Lifecycle Capability And Live Management + +Extension lifecycle behavior is gated by a launcher-issued capability rather than by per-extension state. The launcher emits a `liveExtensionManagement` boolean to the frontend alongside the existing `localInstallStoreReadable` flag and runtime-services metadata. It is derived from the same launch-mode signal Canvas already computes (`services.frontend.kind` in `scripts/runtime-services-info.mjs`): `liveExtensionManagement` is true only when the frontend is served by the Vite dev server (`npm run dev`, `npm run dev:minimal`), and false for the static server used by the packaged global CLI, Docker images, and `dev:static`. + +| Mode | `liveExtensionManagement` | Enable/disable | New-extension discovery | Install / update / remove | +|---|---|---|---|---| +| Packaged global CLI | false | CLI + restart | restart | CLI | +| Docker / static-served | false | CLI + restart | restart | CLI | +| `npm run dev` / `dev:minimal` | true | Extensions page or CLI, no restart | re-read store + refetch registry, no restart | CLI | + +**Live management semantics (capability true):** + +- Enable/disable from the Extensions page calls the host enable/disable routes in §17, which mutate `config.json` through the shared `extension-manager`. The frontend then invalidates the registry query so the new state reflects immediately — the same persist-then-invalidate pattern Canvas already uses for LLM profiles, not a separate live store. +- Newly installed extensions are discovered by re-reading the install store on each registry request and refetching/invalidating the registry query (on the Extensions page, on a manual rescan action, and on the dev-watch cache-bust signal). Filesystem watching of the store is an optional enhancement, not a requirement; the baseline is re-read-on-request plus query invalidation. +- Reconcilable contributions (left navigation, color themes, settings panels, conversation right panels, tool visualizers, and views) re-project from the refreshed registry and mount/unmount accordingly, reusing the dev-watch remount path. +- Non-reconcilable contributions are unchanged: agent plugin references and context suffix apply only at conversation creation, so enabling or disabling them mid-session affects future conversations only. +- Newly discovered extensions arrive `installed`/`disabled` and are never auto-enabled by discovery; enabling from the Extensions page is the explicit consent action. Permission-expanding updates still drop to `disabled` with a needs-review diagnostic. + +**Residual risk on the dev tier.** The live routes are same-origin, so an already-enabled extension on the dev stack can in principle call `/enable` for another *installed* extension — enabled browser code shares Canvas's origin and session (§19). This is bounded and accepted for a dev-only tier: the routes exist only under `liveExtensionManagement`; the caller is already enabled, hence already trusted; and install/update/remove stay CLI-only, so no new code can be introduced — only a package the user already placed on disk can be flipped. Packaged, Docker, and static runs do not expose the routes at all. We therefore do not claim the live tier is a hard consent boundary against already-trusted extension code; the boundary is the dev-only gate plus CLI-only install. + +**Restart-bounded semantics (capability false):** unchanged from the rest of this RFC. The Extensions page is read-only, the host exposes no enable/disable routes, and state changes apply on next process start. + +The `--disable-extensions` kill switch overrides both tiers and suppresses all extension loading for the run regardless of `liveExtensionManagement`. + +## 16. Conversation Runtime Awareness + +Agent Canvas is always local. The active Agent Server backend may be local, remote, OpenHands Cloud, or an ACP subprocess. The launch path computes a runtime compatibility report before applying extension contributions. + +Runtime classes: + +- `canvas-local` — browser UI and Extension Host only. Always available. +- `agent-server-local` — Agent Server process is local to this Canvas stack and can read local paths. +- `agent-server-remote` — remote Agent Server; local extension package paths are not visible. +- `cloud-runtime` — OpenHands Cloud; local paths are not visible to the sandbox. +- `acp-runtime` — agent is an ACP subprocess; MCP and context follow existing ACP rules. + +Compatibility: + +- Browser views and extension settings UI: `canvas-local`. +- Context blocks: any runtime that accepts `system_message_suffix`. +- Remote agent plugin sources: any runtime that accepts `plugins`. +- Package-relative / local agent plugin paths: `agent-server-local` only. +- MCP templates: where existing MCP settings work (ACP inherits the existing guard). +- Secrets: where the active backend's secrets service accepts them. +- ACP runtimes: extension context is allowed; extension MCP templates remain guarded by the existing MCP route; agent plugin references contributed by extensions are disabled until a smoke test proves the specific plugin does not inject ACP-incompatible MCP, hook, tool, or agent-definition state. + +Incompatible contributions are skipped with a disabled reason shown before launch. Local filesystem paths are never sent to remote/cloud runtimes. + +### 16.1 Local-filesystem detection + +`backend.kind === "local"` is **not sufficient** — a local-protocol backend can point at a remote host. The launcher will issue an explicit **`localInstallStoreReadable` capability flag** to the frontend on startup. The flag is true only when the Agent Server process was started by this same `agent-canvas` invocation and shares the local filesystem with `~/.openhands/agent-canvas/installations/`. Without that flag, package-relative agent plugin paths are disabled and the UI shows a disabled-reason chip on the affected launch template. + +### 16.2 Runtime-location note + +When extension agent-side contributions are included, Canvas appends a short runtime-location note alongside `` and ``: + +```text + +Canvas Extensions are installed in the local Agent Canvas client. +Conversation runtime: agent-server-remote. +Do not assume local extension package paths are available inside the runtime. +Only use extension-provided capabilities explicitly listed in this prompt or exposed through runtime tools/settings. + +``` + +Contains no secrets. Local absolute paths are included only when the runtime is local and the path is intentionally part of an agent plugin source. + +## 17. API Routes + +Mounted through ingress **before** `/api/*` is forwarded to the Agent Server, using the same precedence pattern already in place for `/api/automation/*` ([scripts/dev-with-automation.mjs](scripts/dev-with-automation.mjs) around the existing automation routes): + +```text +GET /api/canvas/canvas-extensions/registry +GET /api/canvas/canvas-extensions/diagnostics +GET /api/canvas/canvas-extensions/:id/settings +PATCH /api/canvas/canvas-extensions/:id/settings +GET /api/canvas/canvas-extensions/launch-contributions +GET /canvas-extension-assets/:id/*assetPath + +# Live-management routes: registered only when liveExtensionManagement is true +POST /api/canvas/canvas-extensions/:id/enable +POST /api/canvas/canvas-extensions/:id/disable +POST /api/canvas/canvas-extensions/rescan +``` + +Note the consistent `/api/canvas/canvas-extensions/*` prefix for local Canvas extension runtime APIs — including `launch-contributions`, which was previously inconsistent. Asset serving keeps its own `/canvas-extension-assets/*` prefix because it serves untrusted third-party static files and benefits from being easy to identify in logs and CSP rules. + +The host never exposes browser routes for install, update, or remove; those stay CLI-only in every mode. The `enable`, `disable`, and `rescan` routes are registered **only when the launcher reports `liveExtensionManagement` is true** (the `npm run dev` source stack). In packaged, Docker, and static modes they are absent, so the read-only restart-bounded contract is unchanged and cannot be reached from the browser. When present, `enable`/`disable` mutate `config.json` through the shared `extension-manager` and `rescan` re-reads the install store; all three require the existing session API key, exactly like `PATCH /settings`. `PATCH /settings` is retained in both modes because settings panels need a narrow persistence surface. + +**Auth and capability gating.** The launcher starts the Extension Host and hands it the session API key, so the Host validates the *same* key the frontend already holds (baked/injected via the existing session-key mechanism) on `PATCH /settings` and on the gated `enable`/`disable`/`rescan` routes; the `canvas-extensions-service` wrapper attaches it. Capability gating is enforced **server-side**: the Host decides whether to register the live routes from its own launch-mode computation, never from a client-supplied value. The `liveExtensionManagement` flag sent to the frontend is only a UX mirror controlling whether management controls render — a packaged/static build whose client flag is spoofed still reaches a Host that never registered the routes and gets 404s, not privilege. + +Frontend code must call these routes only through a dedicated `src/api/canvas-extensions-service.ts` wrapper. Because the route prefix begins with `/api/` but targets the local Extension Host rather than the Agent Server, PR 2 must update `src/api/no-direct-agent-server-calls.test.ts` with a narrow allowlist entry for that wrapper, mirroring the existing automation-service exception. This keeps the `/api/canvas/canvas-extensions/*` ingress shape while preserving the repo rule that ordinary Agent Server traffic goes through `@openhands/typescript-client`. + +Implementation note: the current guard has an axios allowlist and a separate blanket `fetch('/api/...')` check. Adding `src/api/canvas-extensions-service.ts` to the axios allowlist is not enough if the wrapper uses `fetch`; the test must explicitly allow only `/api/canvas/canvas-extensions/*` calls from that wrapper, or the wrapper must use an approved local helper. Do not weaken the guard for arbitrary `/api/*` traffic. + +**Routing requirement:** the Extension Host route table must be implemented for every launch mode in the same PR that starts the host (Vite dev proxy, automation ingress, static serving path, and packaged CLI). + +Registry response: + +```json +{ + "schemaVersion": 1, + "extensionApi": 1, + "artifacts": [ + { + "id": "acme.github", + "kind": "canvas-extension", + "packageName": "@acme/agent-canvas-github", + "version": "0.1.0", + "state": "enabled", + "manifest": {}, + "assetBaseUrl": "/canvas-extension-assets/acme.github/", + "diagnostics": [] + } + ] +} +``` + +## 18. Conversation Launch Integration + +An extension contribution merge step runs before `buildStartConversationRequestWithEncryptedSettings()`: + +1. `useCreateConversation()` receives optional extension launch selections. +2. `AgentServerConversationService.createConversation()` asks `CanvasExtensionsService.getLaunchContributions()` for globally enabled contributions (unless extensions are disabled). +3. Merge selected/auto-included agent plugin references with existing `plugins`. +4. Merge selected extension context into the new `extensionSystemSuffix` adapter option. +5. `agent-server-adapter.ts` appends extension context to `AgentContext.system_message_suffix`. +6. Existing payload creation sends `plugins` and no new server-specific fields. + +Current-main payload constraint: Canvas now builds the Agent Server start request with top-level `agent_settings`, `workspace`, `confirmation_policy`, `initial_message`, `tool_module_qualnames`, optional `plugins`, and optional runtime fields such as `hook_config`, `agent_definitions`, `security_analyzer`, `tags`, and `secrets`. Do not reintroduce the older legacy `agent` payload shape and do not drop those existing top-level fields while merging extensions. Extension context belongs in `buildAgentContext()` / the resulting `agent_settings.agent_context.system_message_suffix`; agent plugin references contributed by extensions map into the same top-level `plugins` array already emitted by `buildConfiguredConversationSettings()`. + +Merge rules: + +- Explicit `plugins` from `/launch` come first. +- Agent plugin references contributed by extensions follow in deterministic order: extension navigation order, then manifest order. +- Duplicate plugin sources are deduped by resolved `source/ref/repo_path` tuple. +- Extension context is deduped by `extensionId/contextId`. +- MCP templates are never auto-installed during conversation create. + +## 19. Security Posture + +**Threats.** Malicious browser JS in npm packages. Arbitrary code via npm install scripts. Rapidly changing unreviewed dev folders. Extension UI reading same-origin Canvas state or calling same-origin routes. Prompt injection via extension context. Portable agent plugin hooks/MCP servers in the agent runtime. Path traversal via manifests. Social engineering via agent-mediated install proposals (future). + +**Trust model for MVP browser code.** Inline `browser.module` code is same-origin trusted code. If enabled, it can exercise the same browser privileges as Canvas itself, including calling local Canvas/Agent Server routes when it has the required session credentials. The capability API is still valuable as the supported integration surface, but it is not a security boundary in MVP. The consent UI must say this plainly: enable browser-code extensions only from sources the user trusts. + +**MVP controls.** + +- `npm install --ignore-scripts` by default. +- Manifest path validation; all relative paths must stay inside package root. +- Explicit enablement; explicit permission display at install time. +- Explicit registration for dev source folders (never auto-discovered). +- Host-mediated extension API for supported integrations; permissions are consent/audit metadata, not hard same-origin browser enforcement. +- Path validation for extension asset serving. +- No automatic MCP installation. +- No repository-local extension discovery for Canvas UI extensions. +- Visible diagnostics and logs. + +**Future controls.** Sandboxed iframe runtime for untrusted/marketplace extensions (manifest support reserved as `browser.entry`; see §11.1 and §12.1). Signature/provenance verification. Lockfile integrity display. Enterprise allowlist/denylist policy. Per-extension CSP. Scanner integration before install. Trust labels for first-party extensions. + +## 20. Operational Semantics + +**Install defaults.** Interactive Canvas Extension install validates, shows permissions, enables on confirmation. `--no-enable` installs disabled. `--yes` skips prompts but still fails closed on new permissions unless paired with an explicit non-interactive policy flag. Install scripts denied by default. Browser UI install is out of scope for MVP. Portable skill/plugin install shows the target `.agent` file and resulting diff before writing unless the user supplies an explicit non-interactive approval flag. + +**Update defaults.** Canvas Extension updates preserve enabled/disabled state only when the manifest ID is unchanged and permissions do not expand. If an update adds agent-affecting contributions, new secrets, broader network origins, or new write permissions, the extension moves to `installed`/disabled until re-approval. Downgrades require explicit CLI input and are recorded in diagnostics. Portable skill/plugin updates follow `.agent` file diff/re-approval semantics and do not affect the Canvas Extension registry. + +**Uninstall defaults.** `agent-canvas remove ` disables and removes a Canvas Extension package; extension settings are preserved by default; secrets are never deleted by default; MCP servers / portable skill or plugin settings previously installed by user action are not removed silently. `--purge` removes local settings and cached package data. `--purge-secrets` is separate and explicit. + +**Collision handling.** Canvas Extension IDs must be unique. Installing a package whose manifest ID already exists updates it if the package identity matches, otherwise fails with a collision diagnostic. Two enabled extensions cannot contribute the same view route, launch template ID, or context block ID under the same namespace. Portable skill/plugin identity is handled by the `.agent` installer and should not collide with Canvas Extension IDs. + +**Failure handling.** Invalid manifest → `invalid` state with diagnostics. Missing `browser.module` → keep installed, disable affected UI contribution. Manifest declares the reserved `browser.entry` field → `invalid` state with a "reserved, not yet supported" diagnostic. Extension view error → host-level error boundary and cleanup around the DOM container, rest of Canvas stays alive. Incompatible agent plugin source → skip with preflight warning. Missing required secret → setup action surfaced, dependent contribution withheld. Extension Host unavailable → Canvas loads in degraded mode with a banner. + +**Restart-bounded lifecycle (default).** In packaged, Docker, and static modes extension enablement is loaded when the Canvas process starts. CLI install/enable/disable/update/remove commands mutate persisted state for the next run; the running app does not watch the store or guarantee live unmount/revocation. The read-only Extensions page may show the current process state and diagnostics, but it does not attempt to reconcile out-of-band CLI changes until restart. + +**Live management lifecycle (development source stack).** When `liveExtensionManagement` is true (§15.2), the running app reconciles enable/disable and newly installed extensions without a restart. Enable/disable mutate `config.json` through the shared `extension-manager` and the frontend re-reads the registry; newly installed extensions are discovered on the next registry read or rescan. Reconcilable UI contributions mount/unmount from the refreshed registry; agent plugin and context contributions still apply only to conversations created after the change. Install, update, and remove remain CLI verbs in this mode as well. This tier exists for trusted local development, including the agent-authored-extension loop, and is never enabled for packaged or Docker runs. + +**Package inspection rule.** The Extension Host never imports extension Node code to inspect a Canvas Extension. It reads only static files: `package.json`, `agent-canvas.extension.json`, and static asset paths. The CLI artifact detector may also read portable plugin/skill markers such as `.plugin/plugin.json` and `SKILL.md` before dispatching to the agent-artifact installer. Extension-authored code runs only as the inline browser module loaded into the Canvas frontend, or as agent/MCP/runtime configuration selected at conversation launch, in both cases only after explicit consent. + +## 21. Development Mode Authoring + +Dev mode lets contributors and agents iterate on extensions without publishing to npm or rebuilding Canvas. It is available only in development-capable stacks (`npm run dev`) and documented in contributor docs, not the end-user install path. + +The same development source stack also carries the `liveExtensionManagement` capability (§15.2). Dev-folder watch/remount (this section) handles *authoring an extension's code*; live management handles *enabling, disabling, and discovering installed extensions* without a restart. Together they are what makes the agent-authored-extension loop work locally: an in-Canvas agent can scaffold and install an extension, the Extensions page picks it up on the next registry read, and the user enables it from the browser — all without leaving the session. Neither capability is present in packaged, Docker, or static runs. + +### CLI + +```sh +agent-canvas install ../my-extension --dev +agent-canvas dev-extension register ../my-extension +agent-canvas dev-extension unregister local.my-extension +agent-canvas dev-extension list +``` + +### Registration store + +```text +~/.openhands/agent-canvas/installations/dev/dev-extensions.json +``` + +Each entry records absolute source path, manifest path, registration timestamp, and workspace-scoping. Auto-discovery of arbitrary repo folders is forbidden. + +### Behavior + +- Watch registered dev folders for changes (`fs.watch`; `chokidar` only if needed). +- Re-validate manifest on change. +- Serve assets directly from the source folder. +- Append a cache-busting version to the browser-module URL after changes so dynamic imports receive fresh code. +- Dispose and remount only the affected extension view when its module changes; avoid reloading the parent Canvas app. +- Surface manifest/build errors in the Extensions page and extension view. +- The extension authoring project owns its build/watch command. Canvas watches declared output files and source manifests; it never runs package install scripts automatically and must ask before any package-manager command. +- Never hot-reload agent plugin / tool state into an already-running conversation; a new conversation is required for agent-side contribution changes. + +### Safety rules + +Dev extensions run as trusted same-origin browser modules, just like installed MVP extensions. They are local code with full browser-origin access once enabled, so there is no automatic enablement from repository contents. Each new dev folder requires explicit user approval, and dev mode is visibly labeled in the UI with source path and warning copy. + +### Promotion + +```sh +agent-canvas install ../my-extension +``` + +or "Promote to Installed Extension" from the Extensions page. Promotion installs into the normal store, freezes version/provenance, and disables dev watch for that extension. + +## 22. Agent-Mediated Installation (post-MVP) + +The future flow: an agent builds an extension in the workspace, proposes installing it, the local UI asks the user for approval, and the local Extension Host performs the install. The agent never installs directly. + +**Source types** the proposal mechanism will support: + +- `npm` (`{ packageName, version }`). +- `conversation-artifact` (`{ conversationId, path, sha256? }`) — Canvas downloads via existing conversation file APIs. Safe for any backend. +- `local-path` (`{ path }`) — only when `localInstallStoreReadable` is true. + +**New pieces required.** A Canvas-facing `canvas_extension.propose_install` tool module (proposal-only, no server-side install executor); a frontend handler for proposal ActionEvents with an approval modal; an Extension Host endpoint accepting an approved proposal and re-validating; conversation-artifact download path; permission-diff UI; registry refresh and extension-view remount after install. + +**Safety rules.** Proposal alone is not sufficient — user approval required. No write-capable Extension Host key in `` or the prompt. The agent cannot call `/api/canvas/canvas-extensions/install` directly. Extension Host re-validates the downloaded artifact; proposal payload is advisory. Conversation artifacts are size-limited and checksum-verified when `sha256` is provided. Permission expansion follows the same re-approval rules as CLI updates. + +This is not part of PR 0 or PR 1. It becomes feasible after the local install store, artifact detector, Extension Host, and Extensions UI exist. + +## 23. Upstream Dependencies + +MVP requires **zero broad Agent Server changes**. Current Agent Server request shape exposes `StartConversationRequest.plugins` as `list[PluginSource] | None`, and `PluginSource` carries `source`, optional `ref`, and optional `repo_path`. The Agent Server loads plugins lazily when a local conversation first runs, then merges plugin skills, MCP config, hooks, and plugin agents into the agent. This is enough for Canvas to forward descriptors; it is also why ACP and filesystem-local preflight matter. + +Three things to confirm against the current pinned Agent Server version (`config/defaults.json` currently pins `agentServer` to `1.27.0`) before PR 4: + +1. Agent Server create-conversation accepts `plugins` in the shape of `PluginSource`. +2. `AgentContext.system_message_suffix` remains accepted for both `Agent` and `ACPAgent`. +3. Agent plugin local-path loading works from the host path Canvas passes. + +Current frontend behavior to preserve: `shouldLoadPublicSkills()` now defaults to `true`; set `VITE_LOAD_PUBLIC_SKILLS=false` to opt out. Agent plugin references and context blocks contributed by Canvas Extensions still must not depend on public marketplace skills being present, because deployments and users can disable them. + +Nice-to-have, not blockers: resolved agent plugin refs in conversation info for audit; structured plugin load diagnostics as conversation events; `PluginSource.parameters` if the Agent Server decides to support it; a validation endpoint for plugin manifests; server-owned plugin management APIs. + +Out of scope upstream: a generic server-side Canvas extension loader; server-side npm package installation; arbitrary frontend hook execution in the Agent Server; tool-override APIs for third-party Canvas packages. + +## 24. Frontend UX + +### Customize And Canvas Extensions + +Sections: + +- Skills +- MCP Servers +- Extensions + +Current navigation state: `/customize` is the Customize entry in the primary sidebar; desktop redirects to `/skills`, mobile renders the Customize hub; `/skills` and `/mcp` are live; `/plugins` is still a coming-soon page inside the current Customize navigation. MVP should replace `/plugins` with Extensions or redirect it to the new Extensions route while retaining old bookmarks. Do not rename the Customize section back to Extensions. + +Implementation note: some current files still use legacy "extensions" names, such as `src/routes/extensions-hub.tsx` and `src/components/features/skills/extensions-navigation.tsx`. Treat those as existing Customize implementation details. New Canvas Extension code should use `canvas-extensions` names, and user-facing copy should say Customize or Canvas Extensions as appropriate. + +The Extensions page is a simple stacked view of installed Canvas Extensions and their status. MVP rows should be grouped or filtered by status only as needed for scanability: Enabled, Disabled, Invalid, Needs review (a `disabled`/`installed` entry with a needs-review diagnostic — see §15.1), and Dev. Browser install, update, and remove controls are out of scope in every mode; the page points users to CLI management for those verbs. Marketplace-style browsing is out of scope. + +The page adapts to the `liveExtensionManagement` capability (§15.2): + +- **Capability false** (packaged, Docker, static): the page is read-only. It shows state and diagnostics, points users to CLI management, and notes that enable/disable changes require restarting Canvas. +- **Capability true** (`npm run dev`): each row additionally renders an enable/disable toggle wired to the host enable/disable routes, plus a manual rescan action so newly installed extensions appear without a restart. After a successful toggle or rescan the page invalidates the registry query so the change reflects immediately. Install, update, and remove still route to CLI guidance. + +Each extension row/card shows: display name, package name, version; state; contribution badges; required secrets status; dev source path when relevant; restart-required or needs-review diagnostics when relevant; and diagnostics. Management actions are limited to enable/disable and rescan, and only when the capability is present. + +### Launch UX + +For launch templates: show required MCPs; show agent plugin references to be included; show context blocks to be appended; require confirmation on first use of any agent-affecting extension. + +### Primary navigation UX + +Enabled views with `navigation.location: "primarySidebar"` render as top-level Sidebar entries. For MVP, entries appear in the `afterAutomations` slot: after Automations and before the conversation list, which is the current static order in `SidebarRailBody` (`New Chat`, `Customize`, `Automations`, then `SidebarConversationList`). Collapsed Sidebar mode shows only the icon with a tooltip. Mobile drawer mode shows the same entry in the same relative position. + +### Color theme UX + +Enabled `colorThemes` contributions appear in Settings > Application > Color Theme alongside built-in themes. Theme-only Canvas Extensions should require no extra UI surface: installing and enabling the extension makes its themes selectable, and disabling/removing the extension removes those options. If the current selected theme is no longer available, Canvas falls back to the default built-in theme and records a local diagnostic. + +### Settings panel UX + +Canvas Extension settings panels appear only in the settings experience, grouped under a visible Extensions header after built-in settings sections. Canvas owns the settings page chrome and persistence affordances; extension renderers fill only their declared panel body. + +### Conversation right-panel UX + +Enabled `conversationRightPanels` render in the existing conversation right-panel/tab area beside Files, Browser, and Terminal. Canvas owns tab placement, keyboard/focus behavior, collapsed state, loading state, and error boundaries. + +### Extension view UX + +Extension views look like regular Canvas pages but are visibly extension-owned: title from manifest; small extension badge; browser-module loading/error states; diagnostics link. + +## 25. Implementation Plan + +The multi-agent, checkable execution checklist for this plan lives in [ExtensionsSystemAgentExecutionPlan.md](./ExtensionsSystemAgentExecutionPlan.md). Use this section for PR boundaries and the execution plan for task ownership, dependencies, verification commands, and handoff notes. + +### PR 0 — Contract and types + +Files: `docs/ExtensionsSystemRFC.md`, `src/canvas-extensions/types.ts`, `src/canvas-extensions/manifest-schema.ts`, `src/canvas-extensions/manifest-validation.ts`, `src/canvas-extensions/artifact-detection.ts`, `src/themes/color-themes.ts`, `__tests__/canvas-extensions/manifest-validation.test.ts`. + +Deliverables: shared manifest and registry types; generic installable artifact kind definitions (`canvas-extension`, `agent-plugin`, `skill`, placeholder `mcp-template`); contribution types for views, left navigation, color themes, settings panels, conversation right panels, tool visualizers, launch templates, agent plugin references, MCP templates, and context; artifact detection rules for Canvas Extension packages, portable agent plugins, portable skills, and placeholder MCP templates; schema-version constant; manifest validation helpers; storage-path helper for `~/.openhands/agent-canvas/installations`; fixtures for valid/invalid manifests, including a theme-only extension; `@openhands/agent-canvas/canvas-extensions` subpath export plan plus a visualizer export plan aligned with PR #1277 or its successor; release packaging checklist. No Extension Host, no frontend UI, no agent contribution merging. + +**Iframe-runtime forward compatibility (minimal, PR 0 scope):** the manifest schema and TypeScript contract accept both `browser.module` (active) and `browser.entry` (reserved). The validator recognizes `browser.entry` as syntactically valid but emits a `reserved-not-yet-supported` diagnostic so authors can't accidentally ship extensions that depend on an unimplemented runtime. The API contract in `src/canvas-extensions/types.ts` is defined async-only, which is the only change needed so that a future iframe runtime can satisfy the same interface over postMessage without an API redesign. No iframe host, no postMessage bridge, no asset-mode switching, and no parallel runtime code lands here — that work is deferred to a post-MVP PR triggered by one of the trust-boundary conditions in §27 Decision 11. The total PR 0 cost of keeping the option open is a handful of schema fields, one validation rule, one fixture, and a short authoring note in the type comments. + +**Namespace guard:** keep new types and runtime helpers under `src/canvas-extensions/`; do not recreate the older `src/addons/` namespace. + +### PR 1 — Runtime registry and CLI + +Files: `bin/agent-canvas.mjs`, `scripts/dev-safe.mjs`, `scripts/dev-with-automation.mjs`, `scripts/extension-host.mjs`, `scripts/extension-manager.mjs`, extension config helper, tests under `__tests__/canvas-extensions/`. + +Deliverables: generic install/manage CLI parsing; `install/list/enable/disable/remove/update/doctor` dispatched before stack startup, before `--public` / `--frontend-only` / `--backend-only` stack validation, before the static `build/` existence check, and before importing stack launcher scripts; Canvas Extension install store under `~/.openhands/agent-canvas/installations/`; agent-artifact install path for portable skills/plugins that writes `.agent` files and never projects those artifacts into the Canvas Extension registry; artifact detection for all kinds with install support for Canvas Extensions, portable skills, and portable plugins, while MCP templates remain typed unsupported placeholders; npm install with `--ignore-scripts` for Canvas Extension packages; manifest validation; Extension Host startup; ingress routes for `/api/canvas/canvas-extensions/*` and `/canvas-extension-assets/*` across all launch modes (Vite dev proxy, automation ingress, static serving, packaged CLI, plus the current frontend-only/backend-only partial-stack modes); host `enable`/`disable`/`rescan` routes registered only when `liveExtensionManagement` is true; dev extension registration store and `--dev` registration; launcher-issued `localInstallStoreReadable` and `liveExtensionManagement` capability flags (the latter derived from the Vite-vs-static frontend kind in `scripts/runtime-services-info.mjs`); example Canvas Extension package fixture using context contributions, with agent plugin references tested later as optional agent-runtime contributions; `.agent` install smoke for one portable skill and one portable plugin; `npm pack --dry-run` or packed-tarball smoke for the global CLI path. + +### PR 2 — Frontend inventory UI + +Files: `src/routes.ts`, `src/routes/canvas-extensions-page.tsx`, legacy Customize navigation files such as `src/components/features/skills/extensions-navigation.tsx`, `src/api/canvas-extensions-service.ts`, `src/api/no-direct-agent-server-calls.test.ts`, `src/hooks/query/use-extensions.ts`, i18n. + +Deliverables: Extensions page under Customize replaces "Plugins coming soon"; a simple stacked installed-extensions/status view; CLI guidance for install/update/remove (and for enable/disable plus restart in restart-bounded mode); capability-gated enable/disable toggles and a rescan action wired to the host routes when `liveExtensionManagement` is true, with registry-query invalidation after a successful mutation; dedicated CanvasExtensionsService wrapper and narrow no-direct-Agent-Server test exception for `/api/canvas/canvas-extensions/*`; dev extension badges, source paths, needs-review state, diagnostics; snapshot coverage for empty/installed/enabled/invalid/needs-review states in both the read-only and live-management presentations. Do not reintroduce a top-level Extensions section. + +### PR 3 — Trusted browser-module view host + +Files: `src/routes/extension-view-host.tsx`, `src/canvas-extensions/runtime/*`, sidebar components, `scripts/extension-host.mjs`. + +Deliverables: route `/canvas-extensions/:extensionId/:viewId/*`; DOM-container host for trusted `browser.module` extensions; dynamic import with cache-busting support; browser asset serving; `mount()`/`dispose()` lifecycle; left navigation entries for views; minimal `navigation` and `ui.toast` APIs; example extension package with a bundled browser-ready module. + +### PR 3a — Extension tool visualizers + +Files: `src/components/features/chat/tool-visualizers/*`, `src/canvas-extensions/visualizers/*`, `src/api/canvas-extensions-service.ts`, `src/routes/extension-view-host.tsx` or equivalent runtime loader, `package.json`, `tsconfig.lib.json`, tests under `__tests__/canvas-extensions/` and existing visualizer tests. + +Deliverables: extension visualizer API aligned with the OpenHands agent team's approved direction, using PR #1277 as the current starting point; public visualizer authoring export name/path matched to that decision; extension visualizer registry layer that composes before built-ins and then markdown/default fallback; `matches()` support; no `priority` field unless the agent team adds one; error boundary/fallback behavior for extension renderers; manifest projection for `contributes.toolVisualizers`; `hello.canvas` or a focused fixture overriding one MCP tool visualizer; unit tests proving extension-first order, built-in fallback, markdown fallback, and throw-to-next-renderer behavior. + +### PR 3b — Color themes, settings panels, and conversation right panels + +Files: [src/themes/color-themes.ts](../src/themes/color-themes.ts), [src/components/features/settings/app-settings/theme-input.tsx](../src/components/features/settings/app-settings/theme-input.tsx), settings route/components, conversation right-panel/tab components, `src/canvas-extensions/panels/*`, `src/i18n/translation.json`, tests under `__tests__/canvas-extensions/` and focused component tests. + +Deliverables: extension color themes appear in Settings > Application > Color Theme; theme-only extensions install and work without views or panels; unavailable extension themes fall back to the default built-in theme; settings panels under a visible Extensions header after built-in settings; conversation right panels beside Files, Browser, and Terminal; stable props with conversation/backend/workspace summaries where relevant; no raw store or React Query client in the public API; per-panel error boundaries; deterministic ordering by declared order, extension display name, and contribution ID; visible examples for a color theme, settings panel, and PR/context right panel. + +### PR 3c — Dev extension watch mode + +Files: `scripts/extension-host.mjs`, `scripts/extension-manager.mjs`, `bin/agent-canvas.mjs`, `src/api/canvas-extensions-service.ts`, `src/routes/canvas-extensions-page.tsx`, contributor docs. + +Deliverables: `--dev` install and `dev-extension` subcommands; dev registration store; source folder file watching; manifest revalidation on change; permission-expanding dev manifest changes move the extension to needs-review/disabled-for-next-run until CLI re-approval and restart; browser-module cache-bust/remount after non-permission-expanding asset changes; visible dev-mode labeling; example extension and contributor guide; tests for registration, traversal rejection, invalid manifest updates, permission drift, and view remount signaling. + +### PR 4 — Agent contributions + +Files: `src/api/canvas-extensions-service.ts`, `src/api/conversation-service/agent-server-conversation-service.api.ts`, `src/api/agent-server-adapter.ts`, `src/hooks/mutation/use-create-conversation.ts`, launch/home components. + +Deliverables: optional agent plugin references contributed by enabled extensions merge into new conversation payloads as `PluginSource` entries; selected extension context blocks append to system suffix via the new `extensionSystemSuffix` option; required MCP preflight UI; cloud/local disabled reasons gated on `localInstallStoreReadable`; tests for payload shape and merge order. + +### PR 5 — Hardening and polish + +Permission consent modal; secret setup flow; `doctor` command with actionable diagnostics; logs under the install store; first-party extension example; authoring docs; e2e snapshot coverage. + +## 26. Testing Strategy + +**Unit:** manifest schema validation; package path traversal rejection; duplicate ID handling; color theme contribution validation/fallback; enable/disable precedence at process startup; registry sort order; asset route path validation; CLI arg parsing; dev extension registration and path validation; dev manifest revalidation after source changes; launch contribution merge and dedupe; extension context suffix rendering; runtime compatibility classification across all five runtime classes; permission drift / update re-approval; uninstall preserve vs purge. + +**Release packaging:** `npm pack --dry-run`; install from the packed tarball into a temp global/prefix; verify `agent-canvas list`, `agent-canvas doctor`, and `agent-canvas install --yes` work without a source checkout; verify full `agent-canvas` launch still finds `build/`, `scripts/`, and Extension Host routes. + +**Component:** Extensions page states under Customize in both the read-only (capability false) and live-management (capability true, with enable/disable toggles and rescan) presentations; permission display; CLI/restart guidance; browser-module view loading/error; extension color theme appears in Settings > Application > Color Theme and can be selected; settings panel placement under the visible Extensions header; conversation right-panel tab rendering/error boundaries; MCP required preflight; incompatible runtime warning; dev and needs-review badges. + +**E2E snapshots:** Extensions page empty state under Customize; enabled extension with one view; invalid extension diagnostics; extension color theme option in Settings > Application; settings panel under Extensions header; conversation right panel beside Files/Browser/Terminal; launch template requiring MCP; launch preflight with local-path agent plugin disabled on remote backend; dev extension view remount after file change; future: agent-mediated install proposal flow. + +**Live E2E:** not required for MVP. If added later, follow existing live E2E rules under `tests/e2e/live/`. + +**Verification:** + +```sh +npm run typecheck && npm test && npm run build +``` + +## 27. Resolved Design Decisions + +These were open questions in the earlier draft. Decisions for the RFC: + +| # | Question | Decision | +|---|---|---| +| 1 | Web UI install in MVP, or CLI-only first? | **CLI-only by default, with a development-stack exception.** Install, update, and remove are CLI-only in every mode. Enable, disable, and new-extension discovery are CLI + restart in packaged/Docker/static modes, but are available live from the Extensions page when the launcher reports `liveExtensionManagement` (the `npm run dev` source stack). Web UI install/update/remove remain deferred. See §15.2 and Decision 16. | +| 2 | Iframe sandbox vs inline browser modules for extension views? | **Trusted same-origin browser modules as MVP default** (`browser.module`); reserve `browser.entry` in the manifest schema for a future sandboxed-iframe runtime aimed at untrusted/marketplace extensions. Trust comes from explicit install/enable consent, not browser-enforced isolation. See §11.1, §12.1, and §19. | +| 3 | What signals an Agent Server is filesystem-local? | **Launcher-issued `localInstallStoreReadable` capability flag.** `backend.kind === "local"` alone is insufficient. | +| 4 | `src/installations/` vs `src/canvas-extensions/` directory split? | **Single `src/canvas-extensions/` directory** until a real consumer requires the split. | +| 5 | API route prefix consistency? | **`/api/canvas/canvas-extensions/*`** for local Canvas extension runtime APIs; `/canvas-extension-assets/*` reserved for asset serving. Browser management routes are deferred. | +| 6 | Empty `src/addons/` directory on `main`? | **No longer present.** PR 0 should avoid reviving the old `addons` namespace and use `src/canvas-extensions/`. | +| 7 | Should the first MVP independently install standalone skills/plugins, or only Canvas Extensions? | **Install all three artifact classes, but keep their destinations separate.** Canvas Extensions install into the Canvas Extension store and can contribute Canvas UI/runtime metadata. Portable skills/plugins install into `.agent` files through the agent-artifact installer and never become Canvas Extensions. MCP-template install remains deferred. | +| 8 | Should package-relative agent plugin paths be MVP? | **Yes, but only when `localInstallStoreReadable` is true** and the pinned Agent Server smoke passes. Remote/cloud runtimes get disabled reasons rather than local paths. | +| 9 | Should extension context be global per extension, selected per launch template, or both? | **Both.** Context blocks use `autoInclude` for global/default behavior and launch templates can require/select specific context IDs. | +| 10 | Should repo-provided plugins/skills ever be surfaced automatically as Canvas Extensions? | **No automatic surfacing.** Repository content remains an agent runtime concern unless the user explicitly installs/registers a Canvas Extension path. Portable skills/plugins can be installed through the CLI into `.agent` files, but that does not make them Canvas UI extensions. | +| 11 | What triggers the reserved iframe runtime? | **A trust boundary requirement:** community marketplace distribution, an untrusted third-party publisher tier, or a concrete need for browser-enforced isolation/CSP. | +| 12 | Should agent-proposed dev extension registration ship with dev watch mode? | **No.** Defer to the broader post-MVP agent-mediated install proposal flow. | +| 13 | Which create-conversation payload shape should extensions target? | **Current Agent Server settings shape:** top-level `agent_settings` plus optional top-level `plugins`; do not reintroduce legacy `agent` payloads. | +| 14 | What route-less and registry-backed UI surfaces are in scope? | **Color themes, tool visualizers, settings panels, and conversation right panels.** They map to concrete existing product regions and avoid arbitrary app-root mounting. Color themes are registry data, not arbitrary JavaScript placement. | +| 15 | Should generic conversation slots ship before these surfaces? | **No.** Header/footer/badge/sidebar slots remain deferred until a workflow cannot be handled by a right panel or tool visualizer. | +| 16 | Should the running app ever change extension state without a restart? | **Only on the development source stack.** A launcher-issued `liveExtensionManagement` capability (true only for the Vite-served `npm run dev` / `dev:minimal` stack) enables live enable/disable and discovery of newly installed extensions, so an in-Canvas agent can author an extension and the user can enable it in the same session. Packaged global installs, Docker, and static-served builds stay restart-bounded. Install, update, and remove remain CLI-only in all modes. The capability never relaxes consent, permission re-approval, or the agent-runtime boundary. See §15.2. | + +## 28. Open Questions + +The single list of decisions that would benefit from product/engineering direction. (Other sections point here rather than re-listing.) + +1. **Manifest name.** Keep `agent-canvas.extension.json`, or rename it to make "Canvas Extension" more explicit (e.g. `agent-canvas.canvas-extension.json`)? Renaming reduces ambiguity but adds churn; default is to keep the shorter name for MVP. +2. **Visualizer API.** Confirm the OpenHands agent team's PR #1277 visualizer API shape — registration order instead of `priority`, custom/addon before built-ins, error-boundary fallthrough — and the public export name (PR #1277 proposes `@openhands/agent-canvas/visualizers`). Canvas follows their decision rather than shipping a parallel API. +3. **Mobile.** Is mobile support for conversation right panels required in the first proof, or can small viewports show a disabled/unsupported reason until a mobile design lands? +4. **Provenance.** What provenance bar is required before any non-first-party/community distribution: npm integrity, signatures, first-party allowlist, enterprise policy, or a combination? This gates a public marketplace. +5. **Catalog.** Should the `@openhands/extensions` catalog eventually become a first-party Canvas Extension package, or remain a plain dependency for built-in catalogs? +6. **Agent artifact UX.** What exact `.agent` target selection UX should the first skill/plugin installer use for user-vs-project scope, and how should it present diffs before writing files? + +## 29. Recommended MVP Cut + +The build sequence for the MVP scope in §1. (What ships and what defers is defined in §1 and §5; this section is only the ordering.) + +1. CLI-managed npm extension install/enable/list. +2. Extension Host registry and asset serving. +3. Extensions inventory/diagnostics page. +4. Trusted same-origin browser-module extension views (with the iframe runtime reserved in the manifest schema). +5. Left navigation entries for Canvas Extension views. +6. Color themes in Settings > Application > Color Theme. +7. Settings panels under a visible Extensions header after built-in settings. +8. Conversation right panels beside Files, Browser, and Terminal. +9. Extension tool visualizers. +10. Dev-mode extension registration / watch / reload. +11. Capability-gated live enable/disable and new-extension discovery on the `npm run dev` source stack (`liveExtensionManagement`); restart-bounded everywhere else. +12. Optional agent plugin reference merge for local conversations. +13. Context contribution merge behind explicit permission. + +## 30. Engineering FAQ + +### 30.1 Naming And Product Scope + +**Q: What is a Canvas Extension, exactly?** + +A Canvas Extension is an Agent Canvas-specific package that extends the Canvas UI, UX, or local product behavior. It can add left navigation entries, settings panels, conversation right panels, custom tool visualizers, launch templates, optional references to separate portable agent plugins, MCP templates, and system-context blocks. It is not the same thing as a portable agent plugin, a skill, an MCP server, or the existing `@openhands/extensions` catalog. + +**Q: How is this different from the existing `@openhands/extensions` repo/package?** + +`@openhands/extensions` is an OpenHands catalog/dependency for shared assets such as integrations metadata. It is not an installable Canvas Extension format. Canvas Extensions may reuse data from that catalog, and a future first-party Canvas Extension could wrap some of it, but the catalog should remain a plain dependency until there is a product reason to expose it as a user-installed Canvas Extension. + +**Q: Why is the product name "Canvas Extensions" if the app section was renamed from Extensions to Customize?** + +The app-level section remains Customize. Inside Customize, the Canvas Extension inventory item can be named Extensions because it is specifically about installed Canvas Extensions. The RFC avoids reintroducing a top-level Extensions hub and explicitly treats legacy file names like `extensions-navigation.tsx` as Customize implementation details. + +**Q: Why not call the inventory page "Packages"?** + +"Packages" is too broad and makes this feature sound like a generic npm/package manager. The user-facing surface should be an Extensions page under Customize — read-only in packaged/static mode, with capability-gated enable/disable on the dev stack (§15.2). The page should be a simple stacked view of installed Canvas Extensions and their status/diagnostics. Package terminology stays in CLI/install mechanics where npm package behavior is actually being discussed. + +**Q: Should the manifest still be named `agent-canvas.extension.json`?** + +Keep it for MVP: it is explicit to Agent Canvas, short, and already used throughout the plan. A rename is an open question (§28, item 1). + +**Q: Are `@openhands/agent-canvas/canvas-extensions` and the visualizer authoring subpath the right public exports?** + +`@openhands/agent-canvas/canvas-extensions` is settled — intentionally explicit to avoid confusion with `@openhands/extensions`. The visualizer authoring export is still open and follows the agent team's PR #1277 decision (§28, item 2). Do not create both `./visualizers` and `./canvas-visualizers` unless the agent team wants aliases. + +### 30.2 UI Contribution Surfaces + +**Q: What UI surfaces are in MVP scope?** + +Five concrete surfaces are in scope: + +- Left navigation entries that point to Canvas Extension views. +- Color themes that appear in Settings > Application > Color Theme. +- Settings panels shown only under a visible Extensions header after built-in settings. +- Conversation right panels shown beside Files, Browser, and Terminal. +- Tool visualizers that render matched action/observation events. + +Generic header/footer/sidebar/badge slots are deferred. + +**Q: Can a Canvas Extension just register a color theme?** + +Yes. Theme-only Canvas Extensions should be a first-class supported case because color themes are likely to be an early low-risk extension type. A theme-only package can contribute `colorThemes` and no view, settings panel, right panel, tool visualizer, agent plugin reference, MCP template, or launch template. Once enabled, its themes appear in Settings > Application > Color Theme with built-in themes. + +**Q: Are extension color themes arbitrary CSS?** + +No. The proposed MVP contract is validated theme data aligned with the current `ColorThemeDefinition` model: `scale` CSS custom properties, HeroUI HSL-channel values, and optional semantic `tokens`. Canvas owns validation, persistence, application, and fallback. This should not become a general-purpose global stylesheet injection API. + +**Q: Why defer generic conversation slots and badges?** + +The concrete product surfaces already cover the known use cases without opening a generic app-root or arbitrary placement API. Right panels give extensions a substantial conversation-adjacent surface. Tool visualizers handle event-specific rendering. Header/footer/badge slots should wait for a workflow that cannot be handled by those two surfaces. + +**Q: Where do settings panels render?** + +Only in the settings experience, grouped under a visible Extensions header after built-in settings sections. They should not be interleaved with built-in settings and should not create a new top-level Extensions hub. Canvas owns the settings page chrome, persistence affordances, loading states, and error boundaries. + +**Q: Do Canvas Extension settings panels appear in settings navigation?** + +MVP decision: keep one grouped Extensions section after built-in settings. Do not add a settings nav anchor per installed Canvas Extension in the first proof; revisit only if scanning becomes poor. + +**Q: Where do conversation right panels render?** + +They render in the existing conversation right-panel/tab region beside Files, Browser, and Terminal. Canvas owns tab placement, collapsed/expanded behavior, focus handling, loading state, and error boundaries. + +**Q: How should mobile handle Canvas Extension right panels?** + +MVP recommendation: reuse the existing mobile conversation panel pattern and show extension panels as additional entries in that panel navigation, if technically straightforward. If not, hide extension right panels on small viewports with a clear disabled/unsupported reason until a mobile design is intentionally added. Whether mobile parity is required for the first proof is an open question (§28, item 3). + +**Q: What happens if multiple extensions add left-nav entries or right panels?** + +Sort deterministically by declared `order`, extension display name, then contribution ID. Disabled or invalid extensions do not render. The host should keep enough metadata in the registry for stable ordering and diagnostics. + +### 30.3 Security And Trust + +**Q: What is the MVP trust model, and what can a malicious extension do?** + +MVP browser modules are trusted local code, not sandboxed — see §19 for the full posture and threat list. In short: an enabled extension runs same-origin and can inspect browser-visible Canvas state, call same-origin routes with the session's credentials, manipulate the DOM, and reach network origins. Trust comes from explicit install/enable consent, local install provenance, permission display, diagnostics, and the kill switch — not from browser-enforced isolation, which is deferred to the reserved iframe runtime. + +**Q: Is install-time consent enough?** + +For first-party/local/dev Canvas Extensions, yes as an MVP starting point. For a community marketplace, untrusted publishers, or enterprise policy enforcement, no. Those cases should trigger the reserved iframe runtime, stronger provenance checks, and likely tighter network/CSP controls before broad distribution. + +**Q: Why reserve `browser.entry` for iframes now?** + +It keeps the manifest and runtime API compatible with a future sandboxed document model. The async API shape can later cross `postMessage` without redesigning the extension author contract. + +**Q: How does disable/revocation work?** + +By default disable/revocation is restart-bounded. Users run CLI management commands while Canvas is stopped, then restart. On the next process start, disabled extensions do not enter the registry, do not render UI contributions, and do not contribute to future conversation launches. In packaged, Docker, and static modes the browser UI does not offer live disable/uninstall controls and does not promise immediate unmount after out-of-band CLI edits. + +On the development source stack (`liveExtensionManagement` true; see §15.2), disable is live: the Extensions page toggle mutates `config.json` and the refreshed registry unmounts reconcilable UI contributions (nav, themes, panels, visualizers, views) without a restart. Agent-side contributions are the exception — disabling an agent plugin reference or context block stops it from being added to *future* conversations but cannot retract state already applied to a running conversation, so a new conversation is required. Uninstall/remove remains a CLI verb in this mode too. + +The process-level `--disable-extensions` kill switch still suppresses all extension loading for that run, in either tier. + +**Q: Can an extension impersonate built-in UI?** + +Canvas should visibly mark extension-owned pages/panels and should own shell/chrome for settings panels and conversation right panels. Extension content fills declared bodies only; it should not own placement, top-level navigation shell, or settings page chrome. + +### 30.4 Runtime And Host Architecture + +**Q: Why use a local Extension Host instead of bundling extensions at build time?** + +Global npm installs cannot rebuild Canvas for each installed extension. A local Extension Host can install packages, validate manifests, serve browser assets, and provide registry/diagnostic APIs without requiring a source checkout or rebuild. + +**Q: Does the Extension Host run in every mode?** + +It should run in packaged CLI, `npm run dev`, `dev:minimal`, and static mode when the frontend needs to inspect or render Canvas Extensions. Frontend-only can expose local inventory/assets with agent-runtime diagnostics. Backend-only should skip browser asset hosting unless a future CLI-only host mode needs it. CLI management commands must still work before full stack startup. + +**Q: Why `/api/canvas/canvas-extensions/*` for extension APIs and `/canvas-extension-assets/*` for assets?** + +The API prefix clearly identifies local Canvas-owned extension APIs and avoids confusing them with Agent Server `/api/*` traffic. The asset prefix is separate because it serves extension-authored static files and benefits from being easy to identify in logs and future CSP rules. + +**Q: What happens if the Extension Host is unavailable?** + +Canvas should degrade gracefully: built-in app behavior continues, extension contributions are absent, and the Extensions page or relevant surfaces show an Extension Host unavailable diagnostic. The Extension Host should not be a dependency for ordinary Agent Server API calls. + +**Q: How are extension registry updates invalidated?** + +MVP should use React Query for read-only registry/diagnostic/settings reads and for settings mutations. Install, update, and remove are CLI-only in every mode, so the browser never invalidates for those. Enable, disable, and rescan are CLI-only and restart-bounded in packaged/Docker/static modes, but on the development source stack (`liveExtensionManagement` true) they are browser mutations that invalidate the registry query on success — the same persist-then-invalidate pattern Canvas already uses for LLM profiles. Dev watch should bump a cache-busting token so static dynamic imports remount with new output when the manifest change does not require re-approval. + +### 30.5 Manifest And Packaging + +**Q: Is one `browser.module` per extension enough?** + +For MVP, yes. A single browser module plus contribution IDs keeps packaging simple. Views mount through the view host. Settings panels and right panels declare metadata in the manifest and register implementations by contribution ID at runtime. Visualizers follow the agent-team-approved registry API. If this becomes awkward, a later schema can allow per-contribution modules. + +**Q: Should settings panels and right panels reference `viewId`, modules, or exported component IDs?** + +MVP decision: the manifest declares metadata only and the browser module registers implementations by contribution ID at runtime. Do not require exported component names in the manifest for the first proof. The host validates metadata, the runtime validates registration, and missing implementations produce diagnostics. + +**Q: How are path traversal and package containment handled?** + +Manifest paths for browser modules, icons, package-relative agent plugin roots, and other assets must resolve inside the installed package root. Any `..` traversal or absolute path from a normal installed package should be rejected. Dev-mode local paths are allowed only through explicit `--dev` registration. + +**Q: What happens when permissions change on update?** + +Permission-expanding updates should move the extension back to a disabled or needs-review state until the user re-approves. The registry should preserve diagnostics and show the permission diff. + +### 30.6 Tool Visualizers + +**Q: Can an extension override built-ins, and what wins when several match?** + +Yes, overriding is intentional. The selection order and fallthrough are defined in §12.9 (per the agent team's PR #1277): custom/addon visualizers before built-ins before markdown/default, `matches()` narrows overrides, latest registration wins among custom visualizers, and there is no `priority` field. A thrown extension visualizer is caught by a failure boundary, records a diagnostic, and falls through to the next matching renderer. Diagnostics should expose which visualizer was selected when debugging is enabled. + +**Q: Can visualizers fetch data or read settings while rendering?** + +MVP recommendation: visualizers should receive narrow props and host helpers, but should avoid arbitrary store or React Query access. If a visualizer needs extension settings, it should receive stable settings data through the extension context or a host-provided hook/API, not import Canvas internals. + +### 30.7 Agent Runtime Boundary + +**Q: Do Canvas Extensions load code into the Agent Server?** + +No. Canvas does not patch or load Canvas Extension code inside the Agent Server. Agent-side behavior is forwarded only through existing Agent Server-supported surfaces: `plugins`, MCP settings/templates, and `AgentContext.system_message_suffix`. + +**Q: Which contributions affect conversation creation?** + +Agent plugin references, selected MCP requirements/templates, launch templates, and context blocks can affect new conversation payloads. Pure UI contributions such as left nav, color themes, settings panels, right panels, and visualizers do not by themselves change the agent runtime. + +**Q: How do local-path agent plugin references work with remote/cloud/ACP runtimes?** + +Package-relative or local agent plugin paths are allowed only when `localInstallStoreReadable=true`, meaning the Agent Server process was launched by the same local Canvas stack and can read the install store. Remote/cloud runtimes get disabled reasons instead of local paths. ACP runtimes should skip agent plugin references unless a smoke test proves the exact plugin shape is compatible. + +**Q: What is the audit trail for extension contributions?** + +MVP should include extension IDs and contribution IDs in launch preflight UI and in the composed extension system suffix. Recommended follow-up: persist extension IDs/versions and selected contributions on conversation metadata when the backend exposes an appropriate field. + +**Q: What if an extension is disabled after a conversation starts?** + +After restart, disabling stops future UI contributions and future conversation launches from using the extension. Existing conversations may already contain context suffixes or agent plugin behavior from creation time; disabling cannot reliably remove already-applied agent runtime state from an active conversation. + +### 30.8 Testing And Proof + +**Q: What is the smallest useful fixture extension?** + +`hello.canvas` should prove the full vertical path: install, registry, Extensions page row/status, left nav entry, browser-module view, color theme, settings panel, conversation right panel, one tool visualizer, launch preflight/context contribution, and dev remount. Agent plugin reference compatibility is a separate optional conversation-contribution check because plugins are not Canvas Extensions. + +**Q: Should all UI surfaces land in one PR?** + +No. The RFC already splits the work. PR 3 covers view host and left navigation, PR 3a covers tool visualizers, and PR 3b covers color themes, settings panels, and conversation right panels. PR 2 can land the read-only Extensions inventory page independently once the registry shape is stable. + +**Q: What tests are required before sharing broadly?** + +At minimum: manifest validation, route/path containment tests, no-direct-Agent-Server API guard tests, Extension Host route tests, visualizer ordering/fallback tests, component tests for Extensions page/settings/right-panel surfaces, and a packaged/static dynamic-import smoke. Full `npm run typecheck && npm test && npm run build` remains the final verification tier. + +### 30.9 Clarifications Requested + +See §28 Open Questions — the manifest name, visualizer API shape, mobile right-panel support, and provenance bar are tracked there. The extension-management subpath stays `@openhands/agent-canvas/canvas-extensions` (intentionally explicit to avoid confusion with `@openhands/extensions`). diff --git a/examples/extensions/README.md b/examples/extensions/README.md new file mode 100644 index 000000000..558696d72 --- /dev/null +++ b/examples/extensions/README.md @@ -0,0 +1,63 @@ +# Example Canvas Extensions + +Three minimal Canvas Extensions that each exercise one contribution surface from the +[Canvas Extensions RFC](../../docs/ExtensionsSystemRFC.md). They are repo fixtures +(`private: true`), not published packages, and ship hand-written browser ESM so they +install and run with **no build step**. + +| Example | Surface(s) | Browser module | RFC | +|---|---|---|---| +| [`sunset-theme`](./sunset-theme) | A color theme in Settings > Application > Color Theme | none (manifest-only) | §12.2 | +| [`hello-page`](./hello-page) | A primary-nav page with placeholder content | `mount()` (view) | §12.1, §12.4 | +| [`app-panel`](./app-panel) | An "App" panel in the conversation view | `activate()` (route-less) | §12.4, §13.1 | +| [`shell-toolkit`](./shell-toolkit) | **Multi-surface:** a tool visualizer + a conversation panel + a nav view | `activate()` + `mount()` | §12.9, §12.4, §13.1 | + +The first three cover the three module shapes in isolation — **no module** (theme-only), +**`mount()` only** (a view), and **`activate()` only** (a route-less registration). +`shell-toolkit` then combines them: one extension, one module, where `activate()` +registers a visualizer and a panel and `mount()` renders a view. + +## Install + +```sh +agent-canvas install ./examples/extensions/sunset-theme --yes +agent-canvas install ./examples/extensions/hello-page --yes +agent-canvas install ./examples/extensions/app-panel --yes +agent-canvas install ./examples/extensions/shell-toolkit --yes +agent-canvas list canvas-extensions +``` + +Lifecycle depends on launch mode (RFC §15.2): + +- **Packaged / Docker / static** — restart-bounded. Enable/disable from the CLI, then + restart `agent-canvas`. The Extensions page is read-only. +- **Dev source stack** (`npm run dev` / `dev:minimal`) — live management. Enable/disable + from the Extensions page and pick up newly installed extensions without a restart. + Use `--dev` to register a source folder and iterate: + + ```sh + npm run dev + agent-canvas install ./examples/extensions/hello-page --dev + ``` + +## Verify each one + +- **sunset-theme** — Settings > Application > Color Theme shows **Sunset**; selecting it + recolors the app. Disabling/removing the extension falls back to the default theme. +- **hello-page** — a **Hello** entry appears in the left sidebar after Automations and + opens the placeholder page; the "Say hello" button fires a host toast. +- **app-panel** — open a conversation; an **App** tab appears beside Files / Browser / + Terminal and renders the panel (showing the active conversation id). +- **shell-toolkit** — a **Shell** nav entry, a **Commands** conversation tab, and a + terminal-style visualizer for shell-like MCP tool calls — all from one extension. + +## How these map to the spec + +- All three carry a `package.json` with `agentCanvas.manifest` and an + `agent-canvas.extension.json` manifest (RFC §10, §11). +- Browser modules are plain browser-ready ESM with no bare runtime imports; the only + imports are type-only JSDoc references that are erased (RFC §12.4, PoC §2.2). +- They use Canvas design tokens (`--oh-color-primary`, `--cool-grey-*`) instead of + importing Canvas internals, and only touch host-mediated context APIs. +- They are **trusted same-origin** code once enabled — not sandboxed (RFC §19). Install + only extensions you trust. diff --git a/examples/extensions/app-panel/README.md b/examples/extensions/app-panel/README.md new file mode 100644 index 000000000..f7e84aca7 --- /dev/null +++ b/examples/extensions/app-panel/README.md @@ -0,0 +1,44 @@ +# App Panel (example Canvas Extension) + +Adds an **App** panel to the conversation work area, beside the built-in Files, Browser, +and Terminal panels. + +This demonstrates the `conversationRightPanels` surface (RFC §12.4) and the route-less +registration lifecycle (RFC §13.1): the manifest declares only panel *metadata*, and the +browser module registers the panel *implementation* by contribution id during +`activate(context)` — independent of any view route being open. + +## What it does + +- Contributes a `conversationRightPanels[]` entry: `id: "example.app-panel.app"`, + `title: "App"`, an icon, and `order: 200`. +- On activation, calls `context.conversationPanels.registerRightPanel({ id, mount })`. +- The panel's `mount({ root, conversation })` renders placeholder content and shows the + active conversation id, degrading gracefully when no conversation is active. +- Returns a `dispose()` that unregisters the panel when the extension is disabled or + removed (so on the dev source stack it can disappear live, without a restart). + +Canvas owns tab placement, collapsed/expanded behavior, focus, loading state, and the +per-panel error boundary; the extension only fills the panel body. + +## Install + +```sh +# packaged/restart-bounded: +agent-canvas install ./examples/extensions/app-panel --yes + +# dev source stack (live): +npm run dev +agent-canvas install ./examples/extensions/app-panel --dev +``` + +Open a conversation and select the **App** tab among the right-hand panels. + +## Assumed API shape + +The RFC fixes the *manifest* contribution shape and the `registerRightPanel` entry point +but leaves the exact panel-mount params to implementation. This example assumes +`registerRightPanel({ id, mount({ root, conversation }) })` returning a disposable, +consistent with the DOM-island `mount()` contract used by views (RFC §12.4, §13). +If the implemented contract differs, only `dist/index.js` needs to change — the manifest +stays the same. diff --git a/examples/extensions/app-panel/agent-canvas.extension.json b/examples/extensions/app-panel/agent-canvas.extension.json new file mode 100644 index 000000000..f1a18387c --- /dev/null +++ b/examples/extensions/app-panel/agent-canvas.extension.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://schemas.openhands.dev/agent-canvas/extension.v1.json", + "schemaVersion": 1, + "id": "example.app-panel", + "displayName": "App Panel", + "version": "0.1.0", + "description": "Adds an 'App' panel to the conversation view, beside Files, Browser, and Terminal.", + "publisher": { "name": "Agent Canvas Examples" }, + "license": "MIT", + "compatibility": { + "agentCanvas": ">=1.0.0-rc.6 <2.0.0", + "extensionApi": 1 + }, + "permissions": { + "ui": ["conversationPanels"] + }, + "browser": { + "module": "./dist/index.js" + }, + "contributes": { + "conversationRightPanels": [ + { + "id": "example.app-panel.app", + "title": "App", + "icon": "./dist/app-panel.svg", + "order": 200 + } + ] + } +} diff --git a/examples/extensions/app-panel/package.json b/examples/extensions/app-panel/package.json new file mode 100644 index 000000000..e0a94c50b --- /dev/null +++ b/examples/extensions/app-panel/package.json @@ -0,0 +1,9 @@ +{ + "name": "@example/agent-canvas-app-panel", + "version": "0.1.0", + "type": "module", + "private": true, + "description": "Adds an 'App' panel to the conversation view beside Files, Browser, and Terminal.", + "agentCanvas": { "manifest": "./agent-canvas.extension.json" }, + "files": ["agent-canvas.extension.json", "dist"] +} diff --git a/examples/extensions/hello-page/README.md b/examples/extensions/hello-page/README.md new file mode 100644 index 000000000..85f19ac11 --- /dev/null +++ b/examples/extensions/hello-page/README.md @@ -0,0 +1,40 @@ +# Hello Page (example Canvas Extension) + +Adds a **Hello** entry to the primary left navigation (after Automations) that opens a +full page rendering placeholder "Hello, world" content. + +This demonstrates the `views` + `leftNavigation` surfaces (RFC §12.1) and the +trusted same-origin `browser.module` view runtime (RFC §12.4, §13): Canvas mounts the +extension's `mount({ root, context })` into a Canvas-owned route container at +`/canvas-extensions/example.hello-page/hello`. + +## What it does + +- Contributes a `views[]` entry with a `primarySidebar` / `afterAutomations` navigation + slot, an icon, and `order: 300`. +- Ships a browser-ready ESM module (`dist/index.js`) that renders into the provided + `root` element using vanilla DOM — no React, no bundler, no bare imports. +- Uses Canvas design tokens (`var(--oh-color-primary)`, `var(--cool-grey-*)`) so it + follows the active color theme, and reads `context.theme.colorScheme`. +- Calls the host API `context.ui.toast(...)` from a button to show a live host call. +- Returns a `dispose()` so Canvas can clean up on unmount/remount. + +## Install + +```sh +# packaged/restart-bounded: +agent-canvas install ./examples/extensions/hello-page --yes + +# dev source stack (live), recommended for iterating: +npm run dev # in one terminal +agent-canvas install ./examples/extensions/hello-page --dev +``` + +After install (and enable), look for **Hello** in the left sidebar, just below +Automations. + +## Notes + +`dist/index.js` is hand-written browser ESM, so **no build step is required**. A real +extension would author in TS/JSX and bundle to a single browser-ready ESM file; the +emitted module must still avoid bare runtime imports (RFC §12.4, PoC §2.2). diff --git a/examples/extensions/hello-page/agent-canvas.extension.json b/examples/extensions/hello-page/agent-canvas.extension.json new file mode 100644 index 000000000..03c46d3bd --- /dev/null +++ b/examples/extensions/hello-page/agent-canvas.extension.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://schemas.openhands.dev/agent-canvas/extension.v1.json", + "schemaVersion": 1, + "id": "example.hello-page", + "displayName": "Hello Page", + "version": "0.1.0", + "description": "Adds a Hello page to the left navigation, rendering placeholder content.", + "publisher": { "name": "Agent Canvas Examples" }, + "license": "MIT", + "compatibility": { + "agentCanvas": ">=1.0.0-rc.6 <2.0.0", + "extensionApi": 1 + }, + "permissions": { + "ui": ["views", "leftNavigation"] + }, + "browser": { + "module": "./dist/index.js" + }, + "contributes": { + "views": [ + { + "id": "hello", + "title": "Hello", + "route": "/hello", + "navigation": { + "location": "primarySidebar", + "slot": "afterAutomations", + "order": 300, + "icon": "./dist/hello-page.svg" + } + } + ] + } +} diff --git a/examples/extensions/hello-page/package.json b/examples/extensions/hello-page/package.json new file mode 100644 index 000000000..de91236ac --- /dev/null +++ b/examples/extensions/hello-page/package.json @@ -0,0 +1,9 @@ +{ + "name": "@example/agent-canvas-hello-page", + "version": "0.1.0", + "type": "module", + "private": true, + "description": "Adds a 'Hello' page to the primary navigation with placeholder content.", + "agentCanvas": { "manifest": "./agent-canvas.extension.json" }, + "files": ["agent-canvas.extension.json", "dist"] +} diff --git a/examples/extensions/shell-toolkit/README.md b/examples/extensions/shell-toolkit/README.md new file mode 100644 index 000000000..11c27c595 --- /dev/null +++ b/examples/extensions/shell-toolkit/README.md @@ -0,0 +1,53 @@ +# Shell Toolkit (example Canvas Extension) + +A **multi-surface** Canvas Extension: one package, one browser module, three +contribution surfaces. This is the case the single-surface examples don't show — +it demonstrates the unified module contract from RFC §13.1, where one module's +`activate()` registers route-less surfaces and `mount()` renders a view. + +## Surfaces (all from `dist/index.js`) + +| Surface | Contribution | Registered in | +|---|---|---| +| Tool visualizer | `toolVisualizers` — renders shell-like MCP tool calls as a terminal card | `activate()` | +| Conversation panel | `conversationRightPanels` — a **Commands** panel beside Files/Browser/Terminal | `activate()` | +| Navigation view | `views` — a **Shell** page in the left nav (after Automations) | `mount()` | + +The visualizer and panel are route-less, so they register during the startup +`activate(context)` pass and exist regardless of whether the Shell view is open. The +returned disposable unregisters both on disable/remove — so on the dev source stack the +visualizer and panel can appear/disappear live without a restart (RFC §15.2/§13.1). + +## Install + +```sh +# packaged/restart-bounded: +agent-canvas install ./examples/extensions/shell-toolkit --yes + +# dev source stack (live): +npm run dev +agent-canvas install ./examples/extensions/shell-toolkit --dev +``` + +## Verify + +- A **Shell** entry appears in the left sidebar after Automations and opens the view. +- Open a conversation: a **Commands** tab appears beside Files / Browser / Terminal. +- When the agent runs a shell-like MCP tool (`tool_name` of `shell` / `bash` / + `execute_bash`), the event renders as a terminal card instead of the default body; + other tools and events fall through to the built-in renderers (RFC §12.9). + +## Provisional contracts ⚠️ + +Two parts depend on contracts the RFC has not finalized; both are flagged inline in +`dist/index.js`: + +1. **Visualizer body** — follows the not-yet-final agent-team API (PR #1277, RFC §12.9). + The real `Body` is a React component returning a React node from + `@openhands/agent-canvas/visualizers` primitives; this example returns a DOM node for + illustration. The relationship between the manifest `toolVisualizers[].module` field + and `activate()`-registration also needs to be pinned. +2. **Right-panel mount** — same assumed `registerRightPanel({ id, mount })` / + `mount({ root, conversation })` shape as [`app-panel`](../app-panel), pending §12.4/§13.1. + +When those land, only `dist/index.js` should need to change; the manifest stays the same. diff --git a/examples/extensions/shell-toolkit/agent-canvas.extension.json b/examples/extensions/shell-toolkit/agent-canvas.extension.json new file mode 100644 index 000000000..ac7771c09 --- /dev/null +++ b/examples/extensions/shell-toolkit/agent-canvas.extension.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://schemas.openhands.dev/agent-canvas/extension.v1.json", + "schemaVersion": 1, + "id": "example.shell-toolkit", + "displayName": "Shell Toolkit", + "version": "0.1.0", + "description": "Multi-surface example: a tool visualizer for shell commands, a 'Commands' conversation panel, and a 'Shell' nav page — all from one extension.", + "publisher": { "name": "Agent Canvas Examples" }, + "license": "MIT", + "compatibility": { + "agentCanvas": ">=1.0.0-rc.6 <2.0.0", + "extensionApi": 1 + }, + "permissions": { + "ui": ["toolVisualizers", "conversationPanels", "views", "leftNavigation"] + }, + "browser": { + "module": "./dist/index.js" + }, + "contributes": { + "toolVisualizers": [ + { + "id": "example.shell-toolkit.shell", + "actionKinds": ["MCPToolAction"], + "observationKinds": ["MCPToolObservation"] + } + ], + "conversationRightPanels": [ + { + "id": "example.shell-toolkit.commands", + "title": "Commands", + "icon": "./dist/shell-toolkit.svg", + "order": 250 + } + ], + "views": [ + { + "id": "shell", + "title": "Shell", + "route": "/shell", + "navigation": { + "location": "primarySidebar", + "slot": "afterAutomations", + "order": 350, + "icon": "./dist/shell-toolkit.svg" + } + } + ] + } +} diff --git a/examples/extensions/shell-toolkit/package.json b/examples/extensions/shell-toolkit/package.json new file mode 100644 index 000000000..d6e36a9e0 --- /dev/null +++ b/examples/extensions/shell-toolkit/package.json @@ -0,0 +1,9 @@ +{ + "name": "@example/agent-canvas-shell-toolkit", + "version": "0.1.0", + "type": "module", + "private": true, + "description": "Multi-surface Canvas Extension: a tool visualizer, a conversation panel, and a nav view from one module.", + "agentCanvas": { "manifest": "./agent-canvas.extension.json" }, + "files": ["agent-canvas.extension.json", "dist"] +} diff --git a/examples/extensions/sunset-theme/README.md b/examples/extensions/sunset-theme/README.md new file mode 100644 index 000000000..c44fe90e0 --- /dev/null +++ b/examples/extensions/sunset-theme/README.md @@ -0,0 +1,31 @@ +# Sunset Theme (example Canvas Extension) + +A **theme-only** Canvas Extension. It contributes one color theme — "Sunset" — and +nothing else: no view, settings panel, right panel, tool visualizer, agent plugin reference, or +browser module. + +This demonstrates the first-class theme-only case from RFC §12.2 / §13: a package that +ships **no `browser.module`** at all and still works, because color themes are projected +from manifest data (validated by Canvas), not from running extension code. + +## What it does + +Adds a "Sunset" option to **Settings > Application > Color Theme**. The theme overrides: + +- `scale` — the `--cool-grey-*` semantic palette (warm dark tones). +- `heroui` — a few HeroUI HSL-channel variables (background/foreground/content/default). +- `tokens` — the `--oh-color-primary` / `--oh-accent` brand tokens (amber). + +Canvas owns selection, persistence, application, and fallback. If the extension is +disabled or removed while "Sunset" is active, Canvas falls back to the default built-in +theme and records a diagnostic (RFC §12.2). + +## Install + +```sh +agent-canvas install ./examples/extensions/sunset-theme --yes +``` + +Then open Settings > Application > Color Theme and pick **Sunset**. + +No build step: the extension is pure manifest data. diff --git a/examples/extensions/sunset-theme/agent-canvas.extension.json b/examples/extensions/sunset-theme/agent-canvas.extension.json new file mode 100644 index 000000000..81f0e2d7a --- /dev/null +++ b/examples/extensions/sunset-theme/agent-canvas.extension.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://schemas.openhands.dev/agent-canvas/extension.v1.json", + "schemaVersion": 1, + "id": "example.sunset-theme", + "displayName": "Sunset Theme", + "version": "0.1.0", + "description": "Adds a warm 'Sunset' color theme to Settings > Application > Color Theme.", + "publisher": { "name": "Agent Canvas Examples" }, + "license": "MIT", + "compatibility": { + "agentCanvas": ">=1.0.0-rc.6 <2.0.0", + "extensionApi": 1 + }, + "permissions": { + "ui": ["colorThemes"] + }, + "contributes": { + "colorThemes": [ + { + "id": "example.sunset-theme.sunset", + "label": "Sunset", + "scale": { + "--cool-grey-50": "#FBF6F2", + "--cool-grey-100": "#F4E9E1", + "--cool-grey-200": "#E7D3C4", + "--cool-grey-300": "#D4B49E", + "--cool-grey-400": "#B98C70", + "--cool-grey-500": "#9A6A4E", + "--cool-grey-600": "#7C5038", + "--cool-grey-700": "#5E3B29", + "--cool-grey-800": "#462C20", + "--cool-grey-900": "#34211A", + "--cool-grey-925": "#271913", + "--cool-grey-950": "#1A0F0B", + "--cool-grey-975": "#0F0805" + }, + "heroui": { + "--heroui-background": "18 30% 7%", + "--heroui-background-foreground": "28 40% 92%", + "--heroui-foreground": "28 30% 85%", + "--heroui-content1": "18 25% 12%", + "--heroui-default": "20 20% 30%" + }, + "tokens": { + "--oh-color-primary": "#FB923C", + "--oh-accent": "#F97316" + } + } + ] + } +} diff --git a/examples/extensions/sunset-theme/package.json b/examples/extensions/sunset-theme/package.json new file mode 100644 index 000000000..f4c5d69b1 --- /dev/null +++ b/examples/extensions/sunset-theme/package.json @@ -0,0 +1,9 @@ +{ + "name": "@example/agent-canvas-sunset-theme", + "version": "0.1.0", + "type": "module", + "private": true, + "description": "Theme-only Canvas Extension that adds a 'Sunset' color theme.", + "agentCanvas": { "manifest": "./agent-canvas.extension.json" }, + "files": ["agent-canvas.extension.json"] +}