feat(extensions): local-directory dev source (~/path) with runtime registry#1815
Draft
jpshackelford wants to merge 3 commits into
Draft
feat(extensions): local-directory dev source (~/path) with runtime registry#1815jpshackelford wants to merge 3 commits into
jpshackelford wants to merge 3 commits into
Conversation
…ource
Add a Node-only LocalExtensionRegistry (expanduser -> realpath -> assert-dir
-> confine, deterministic id from the resolved path, persisted to a gitignored
.agent-canvas/dev-extensions.json) and wire a dev-only Vite plugin
(serve-local-extensions, apply: serve) that exposes:
- POST /__ext-local/register { path } -> { id }
- GET /__ext-local/<id>/<file> (traversal-confined, no-cache, CSP-nonce
stamped HTML)
The file-serving logic is factored into a shared serveExtensionFile() helper
reused by the existing serve-example-extensions middleware, so local .html is
never served without a host-stamped nonce.
Co-authored-by: openhands <openhands@all-hands.dev>
Recognize a local filesystem path in the Add-extension box (~/path, /abs, and file:///abs), reject the structurally invalid file://~ form with an actionable message, and rewrite it to the localhost dev URL by POSTing the RAW path to the register endpoint. The browser never expands ~ or touches disk. Local sources are represented as url-kind ArtifactDescriptors whose sourceRef is the raw path (mutable/unpinned), so restart re-resolution and a new reloadExtension() action re-register and re-fetch the current bytes. The AddExtensionModal accepts local paths (Local badge) and the installed-extension card gains a Reload button for local sources. Adds i18n keys (EXTENSIONS$LOCAL_BADGE, RELOAD_BUTTON, RELOADING, RELOAD_SUCCESS, LOCAL_FILE_TILDE_INVALID) across all locales and updates SOURCE_HELP. Co-authored-by: openhands <openhands@all-hands.dev>
Add a 'Local Development (recommended: add a directory)' section to the author guide (accepted/rejected input forms, edit->rebuild->Reload loop, restart resilience via the persistent registry, dev-only/localhost/consent guardrails), a Local source row + two-layer design note in the architecture doc, and a pointer in feature-flag.ts. Co-authored-by: openhands <openhands@all-hands.dev>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
✅ Mock-LLM E2E Tests60/60 passed Commit: Details
Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
Contributor
|
| Status | Test | Duration |
|---|
Posted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses)
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.
What & why
Adds a local development workflow to the experimental UI-extensions system: a developer running Agent Canvas locally can type a path to a directory on their own machine into the "Add extension" box and have that extension load and hot-reload — with no config edits and no server restart to add a new directory. Previously only remote sources (
github:,npm:,https://bundle URL) were installable.Feature stays gated behind
VITE_ENABLE_EXTENSIONS; the whole local path is dev-only.Two-layer design (matches the existing architecture)
A. Browser (resolver) stays dumb —
src/extensions/sources/local-path.ts~/…,/abs,file:///abs), does light normalization, and rewrites it to a localhost dev URL served by the dev middleware.~(the server knows$HOME).file://~/…with an actionable message.url-kindArtifactDescriptors whosesourceRefis the raw path; servedno-cache, so every Reload re-fetches.B. Node dev middleware does the real work —
src/extensions/dev/local-extension-registry.ts+serve-local-extensionsplugin invite.config.ts(apply: "serve")/__ext-local/<id>/*over a runtime registry (this is what makes "no restart" true — registering a new directory is just an API call that mutates in-memory state).POST /__ext-local/register { path }→expanduser → realpath → assert-is-directory → confine→ stores{ id → resolvedRoot }→ returns a deterministic id (hash of the resolved path, so re-registration is idempotent).GET /__ext-local/<id>/<file>→ registry lookup → same traversal guard + content types + CSP-nonce-stamped.htmlas the existingserve-example-extensionsmiddleware (factored into a sharedserveExtensionFile()helper)..agent-canvas/dev-extensions.json, loaded + re-validated on boot; entries whose directory no longer exists are silently dropped. The dev server is disposable; the registry is durable.Invariants preserved
detectSource→ capability-consent card as remote ones (they resolve to a fetchable URL)..htmlis served with a host-stamped nonce via the handler; no bare static mount.~expands server-side only — the browser never resolves home or touches disk.~/../../etc/passwd(register) and/__ext-local/<id>/../../secret(file request) are both rejected./__ext-local/handler exist solely in the Vite serve build; never in a deployed/library build.Accepted / rejected input forms
~/rel/to/home, bare absolute/abs/path, Windows absolute, andfile:///abs/path.file://~/…— tilde-as-host is invalid URL grammar; the user is told to use~/pathwithout thefile://prefix, orfile:///absolute/path.SDK grammar parity
Mirrors
software-agent-sdk::parse_extension_source'sLOCALhandling:~,/abs, and (server-side)Path(url).expanduser().resolve(). The dot-relative/bare-relative SDK cases are intentionally omitted in the UI because they are ambiguous withnpm:/github:refs.Design judgment calls
url-kind descriptors (not a newBundleSource/parsed kind). A local dir is naturally aurldescriptor whosebaseUrlishttp://<dev-host>/__ext-local/<id>, sodetectSource/consent/toBundleSourcework unchanged. "Mutable/unpinned" is expressed byno-cacheheaders + a raw-pathsourceRefthat re-resolves on Reload/restart (no version/SHA).~-expansion and confinement live entirely server-side inLocalExtensionRegistry; the browser half is filesystem-blind by construction.Testing
npm run typecheck✅eslint src✅, prettier clean on all changedsrc/test files ✅~NOT expanded in browser,file://~rejected, registry register/lookup/persist/prune, traversal confinement (both~/..register and/__ext-local/<id>/../..request rejected), modal local-path acceptance +file://~badge, provider install/reload of a local source, Reload button UI.npx vitedev server with a throwaway bundle: registered a/tmppath and a~/…path (confirmed~expanded to/home/openhandsserver-side); fetchedextension.json(200) andpanel.html(CSP header +<script nonce=…>stamped,Cache-Control: no-cache); traversal escapes → 403; unknown id → 403; bad paths → 400 with actionable messages;GETon register → 405; editedpanel.htmland re-fetched without restart → new bytes; restarted the dev server → same ids still serve (no re-add); removed a directory + restart → stale entry dropped. Throwaway bundle deleted before committing.This PR was created by an AI agent (OpenHands) on behalf of the user.
@jpshackelford can click here to continue refining the PR