Extension system with server-backed panels - #397
Merged
Conversation
End-to-end extension system that adds panels to Cate via a web frontend
and an optional local server, served through a token-injecting proxy.
- runtime capabilities: HTTP/WS tunneling + a managed server host
(src/runtime/capabilities/{server,tunnel}.ts)
- main: ExtensionManager, ExtensionServerManager, proxy server, catalog
download/install, and the cateHost reverse-API dispatch + storage
- preload: cateHost bridge injected into extension webview guests
- renderer: ExtensionPanel, ExtensionsSettings, useCateHostActionResponder
- shared: manifest/types, cate-host API typings, IPC channels
- docs/extensions.md describing the system
The extension catalog lives in its own repo (0-AI-UG/cate-extensions);
it's gitignored here and only checked out locally for dev/tests, which
skip when that checkout is absent.
Anton-Horn
force-pushed
the
feat/extension-system
branch
from
June 16, 2026 19:10
b6f994b to
778d1cd
Compare
- ci.yml: check out + build 0-AI-UG/cate-extensions before unit tests so the distribution and kitchensink-server suites run against the real artifact instead of skipping. - kitchensinkServer.test: derive the server entry from the manifest command (works for server.js or compiled dist/server.js), guard the manifest read so the file imports without the catalog, and annotate the WebSocket socket 'data' chunk as Buffer (fixes the CI typecheck failure under a stricter @types/node). - distribution.test: read the built dist/catalog index and assert the manifest's declared server entry exists, layout/language-agnostic.
- eslint: ignore examples/ and the gitignored cate-extensions/ checkout (their own JS/TS projects), plus generated dist-runtime/ and local .cate/ worktrees, so lint only covers app source. This was the ubuntu CI lint failure (examples/ sample extensions tripping no-undef). - ci.yml: run the catalog checkout + build only on non-Windows. The catalog repo's bash build.sh resolves a manifest path that breaks under Git-Bash/MSYS on Windows; the extension tests skipIf the catalog is absent, so the Windows leg stays green.
Add cate.agent.* so an enabled extension can run one agent turn through the bundled pi. Gated by a dedicated `agent` scope, first-use user consent, and one run per extension at a time. The run is an owner-bound, visible agent session that resolves with the final assistant text (on pi's terminal agent_end). Reachable from both server-backed extensions (CATE_API) and frontend panels (cateHost bridge). Also lands the production-readiness fixes from the review: manifest scope enforcement, sender-derived guest identity, workspace path confinement, verified and traversal-safe artifact extraction, and quit/crash process hygiene.
…missions Remove documentation for cate.* methods that aren't implemented yet (workspace/theme.onChange, panel lifecycle + setBadge, editor getters, the commands namespace, and canvas methods beyond createPanel). They return 'unsupported' today; re-document if/when implemented. Settings: list each extension's declared cateApi scopes as readable permission chips (catalog + sideload rows), with the `agent` scope highlighted as the most sensitive.
Replace the one-shot cate.agent.run with a turn-based session API: open() returns a handle (pi's session file, so a conversation resumes with no Cate-side state), send() runs one turn and returns the full assistant message, dispose() tears down the live client. run() stays as open->send->dispose sugar. Drives the same create()/PiRpcClient/dispose() path a panel uses, so pi owns all conversation state and Cate only holds the live handle. One live session per extension, one turn in flight, the anti-runaway guard. Also surface failed turns: a turn that ends on stopReason error (an unsupported model, auth, bad request) now rejects with pi's reason instead of returning silent empty text, and the handler passes that reason to the extension. This was the cause of the persistent "(no text)".
… API
kitchensinkPanel.test.tsx runs the real shipped panel script in jsdom against
a mock cate bridge (mirrors frontendkitPanel) and drives the full surface,
including the agent flow: open-on-first-send, handle persisted to storage,
resume from a stored handle, error rendering, and dispose-on-end.
cateHost.test.tsx locks the preload wire contract: each cate.agent.* call maps
to the right { method, args } and guest identity on CATE_HOST_INVOKE. This was
the one untested layer between an extension calling cate.* and dispatch.
The real extensions live in the separate cate-extensions repo, not in-tree.
A canvas node persisted in `.cate` with a missing or invalid `size`/`origin` (corrupt, half-written, or older-schema data) was seeded straight into the store, so CanvasNode/useNodeResize crashed the whole canvas with an opaque "Cannot read properties of undefined (reading 'width')". loadWorkspaceCanvas now sanitizes loaded geometry: repair origin/size/counters where safe, drop the unrecoverable (no panelId), guard viewport/zoom, and log what changed. The next save persists the repaired state, healing the file.
…creen Add a `files.drop` extension scope: an extension's isolated webview guest can't see Cate's in-renderer file-explorer drags, and an HTML overlay can't sit reliably above a webview. So while a Cate drag is in flight the host makes the webview pointer-events:none, hit-tests the drop against the panel, reads each file host-side (the user's drag authorises it, the guest never touches the filesystem), and forwards the content on the same cate:event channel the preload listens on. OS-file drops reach the guest's own DOM natively. Content is byte-capped (truncated flag); guests subscribe via cate.files.onDrop. Also exempt webview-backed panels (extensions) from viewport culling: keepMountedOffscreen on the panel definition keeps their canvas nodes mounted even when scrolled off-screen, so panning away no longer destroys the guest process and resets in-page session state. selectVisibleNodeIds is split out as a pure, unit-tested core with a cached keep-alive set.
distribution.test.ts hardcoded kitchensink/frontendkit, now dev-only and excluded from the user-facing catalog; make it catalog-agnostic (pick a server-backed and a frontend-only entry from the built index). frontendkit panel test asserted a --fk-accent var the shipped app.js never sets; check --fk-fg instead.
Security/reliability fixes from a production-readiness audit of the extension system: - validate manifest id/version as filesystem-safe slugs at the normalizeManifest chokepoint (blocks path traversal into stage/ storage/install paths) - tear down extension server sessions on runtime disconnect via a new runtimes.onDisconnected hook + ExtensionServerManager.disposeForRuntime and provisioned-cache invalidation (fixes permanent 502 after a daemon drop/reconnect); keyed on runtime.id, no local/remote branch - require remote-sourced catalog artifacts to be http(s) + sha256; the local-artifact sha256 exemption now keys on the source's trust class, not the artifactUrl scheme - add AbortSignal timeouts + streaming byte caps to catalog and artifact fetches - bound cate.agent.open resume paths to the caller's workspace - harden daemon tarball extraction: bare-name listing (spaces-safe) plus a post-extraction realpath containment check - destroy the proxy tunnel Duplex on response close (per-request leak) - add a Settings warning to only add trusted catalog sources Full extension + runtime suite green (301 tests); each fix ships with regression coverage.
Node and Panel error boundaries duplicated getDerivedStateFromError / componentDidCatch / reset-on-key-change logic. Pull the common lifecycle into BaseErrorBoundary and let each keep only its fallback + logging.
Guard against corrupt/missing persisted geometry and reused slots when restoring canvas nodes.
Pin the extension guest preload to the canonical cateHost bundle instead of trusting the renderer-supplied path; make per-runtime storage stores disposable so watchers aren't stranded on disconnect; add coverage for subscribe, proxy identity/start, storage, and webSecurity.
The isolated extension guest never sees the host stylesheet, so every extension rendered Chromium's default light scrollbars inside the dark app. Inject the app's scrollbar treatment at dom-ready with the current theme's thumb colors baked in. Exported guestScrollbarCss for tests.
The official catalog now ships as assets on the cate-extensions `catalog` GitHub Release instead of GitHub Pages. Update the default source URL.
A local catalog entry (sourceIsLocal) now re-stages and re-extracts on every provision instead of short-circuiting on the host .ok marker, so source edits (including manifest scope changes) land on reload without a version bump or a manual reinstall. Remote installs keep the .ok idempotency so their bytes upload at most once per version.
…g + cleanups Correctness: - proxyServer: reject backslash-delimited traversal and re-check path containment after the host join (Windows arbitrary-file-read) - ExtensionServerManager: reuse the live server during GRACE instead of spawning a duplicate and orphaning the old process - ExtensionManager: on force re-provision serve the pinned staged bytes rather than downloading latest into the pinned version dir; reinstall repairs the installed version in place without bumping it - storage: fire onChange for in-app writes; flush pending writes synchronously on quit and on dispose so the last set() is not lost - canvasStore: key the keep-alive cache on dock membership, not node count, so a cross-node tab move cannot cull a kept-mounted webview - useCateHostActionResponder: normalize the remote-workspace locator before the containment check so absolute paths resolve - cate-host-api: align createPanel options with what the responder honors Cleanup and perf: - ExtensionServerManager: unify the three teardown loops - cateApiHandlers: extract guest-auth and once-destroyed helpers - ExtensionsSettings: single row component, drop the double-fetch - parallelize provisioning and staged-version scans
…ills - skills/cate-extension: self-contained guide for building, testing, and publishing Cate extensions (manifest, scopes, host API, kit, server contract, sideload/local-catalog dev loops, catalog submission) - registry/sources.json: add 0-AI-UG/cate (path skills/) as a curated source - build-skills-index: firstParty flag exempts our own sources from the 10k-star floor; description floor still applies - docs/extensions.md: sync reverse-API block with cate-host-api.d.ts (createPanel options, session-based agent API)
…cel pool The pool reused any covering ancestor tree for a new subscriber, but a workspace-root tree natively prunes hidden dirs, so a subscriber under .cate attached to it and silently never received an event. findCovering now skips trees whose ignore globs prune the prefix; such subscribers get their own tree rooted at the prefix, where the relative globs are inert. Extension storage watched storage.json by file path, which parcel cannot root a watcher at. It now watches the parent dir (created through the runtime first, so local and remote arm identically), filters events by basename, and drops same-content reloads.
…Windows CI Windows runners hand out 8.3 short paths (C:\Users\RUNNER~1) in TEMP, so roots got registered in short form while validatePathStrict resolves paths through the native realpath, which expands to the long form. The long form matched neither the tmpdir carve-out nor any registered root, so every strict check under temp failed on Windows. Same class of mismatch exists on macOS where /tmp resolves to /private/tmp. Store each allowed root under both its lexical and native-realpath form and check the tmpdir carve-out the same way. Also fix storage.test.ts to build its fake paths with path.join: the store uses hostJoin (path.join for local), so hardcoded /proj/... forward-slash keys never matched on Windows.
…ored paths install.test expected path.join dest for the remote runtime, but hostJoin uses posix joins for remote hosts, so on a Windows client the dest tail is forward-slashed. cateApiHandlers.test expected the raw posix resume handle, but boundedResumePath canonicalizes via the local host's path flavor and returns backslashes on Windows.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the extension system end to end: extensions add panels to Cate via a web frontend and an optional local server, served through a token-injecting proxy.
What's in it
src/runtime/capabilities/{server,tunnel}.ts)ExtensionManager,ExtensionServerManager, proxy server, catalog download/install, and thecateHostreverse-API dispatch + per-extension storagecateHostbridge injected into extension webview guestsExtensionPanel,ExtensionsSettings,useCateHostActionRespondercate-hostAPI typings, IPC channelsdocs/extensions.mdcate-extensions
The extension catalog is its own repo (
0-AI-UG/cate-extensions). It's gitignored here and only checked out locally for offline dev/tests. The two suites that use it (distribution.test.ts,kitchensinkServer.test.ts) skip when that checkout is absent. The Kitchen Sink demo's TS conversion is in 0-AI-UG/cate-extensions#1.Tests
typecheckclean; extension + runtime-capabilities suites green.