feat(examples): pi extension surface for the pi harness on Workers - #2229
feat(examples): pi extension surface for the pi harness on Workers#2229mattzcarey wants to merge 11 commits into
Conversation
…onEnv to the pi harness Phase 1: scripted verbatim vendor of pi coding-agent's extension runtime (earendil-works/pi@c4b0e35a) with stubs at upstream paths, a loader patch that drops jiti/fs, MANIFEST.json and pnpm vendor:pi:check. Phase 2: pi ExecutionEnv over @cloudflare/shell Workspace + just-bash, so pi's own read/write/edit/bash tools run in the Durable Object.
…tension UI protocol Phases 3, 4 and 6: PiExtensionRuntime loads inline pi extensions through the vendored ExtensionRunner, binds them to AgentHarness.hooks, forwards harness events as extension events, surfaces handler_error, and adds the WebSocket extension UI bridge, protocol frames, client hook state and dialogs.
…xtensions for the pi harness Phases 5 and 7: resolve /commands to extension handlers, prompt templates or skills before durable enqueue; in-memory ResourceLoader; per-lane extension UI bridges wired to the WebSocket transport; example memory-guard and notes extensions over a Shell Workspace; README, examples index and design/pi-extensions.md.
- before_tool fails closed when an extension tool_call handler throws - prompt() resolves for handled and command receipts instead of polling forever - lane state is scoped per emit and restored, so notifications hit their own lane - dialogs with no subscriber reject with NoExtensionUiError instead of a fake decline - sendMessage with triggerTurn writes one message, not two - idempotency check runs before any extension side effect - set_flag only accepts registered flags with the registered type - Workspace exec no longer deletes /tmp and other sandbox-root content - partial shell output reaches onStdout before a timeout is reported - flags reach connecting clients; settled dialogs are announced and stale answers rejected - in-stream handler_error and lane event frames reach the client notices - vendor:pi:check runs as part of pnpm run check
|
There was a problem hiding this comment.
Note
This report is out of date. Scroll down for Devin Review's latest report on this PR.
Devin Review found 12 potential issues.
3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
There was a problem hiding this comment.
🟥 Agent sessions lack access control
Agent routes expose model, shell, command, flag, and dialog actions without authorization. Permissive CORS lets arbitrary origins drive known sessions.
(Refers to this code)
Was this helpful? React with 👍 or 👎 to provide feedback.
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
- lane scoping via AsyncLocalStorage; extension tools run on their operation's lane - out-of-band submissions (handled input, extension commands) get a durable disposition keyed by operationId, so retries do not rerun side effects - handled input drains queued extension writes before returning - before_agent_start receives the assembled system prompt - agent_end carries the run's messages; session_compact projects the real entry - dialog answers are accepted only from the lane that owns the request - client frames validated with typebox schemas - shell sync failures become ExecutionError; total snapshot byte cap
|
Pushed fixes for the Devin review (commit 3884a88):
Not addressed here: the example has no auth and permissive CORS. That is the pre-existing example access model shared with the other examples/next harnesses and out of scope for this PR. |
⚪ agents import sizesMeasured 336 runtime imports as minified bundles. The primary size is gzip; raw minified size is included for diagnosis. An existing import growing by more than 10% is marked red. This report is informational.
Compared No import sizes changed. All 336 current runtime imports
Reported by agent-think[bot]. |
- extension tools drain their lane's queued writes before returning a result - snapshot count and per-file limits refuse to run instead of truncating - native provider registrations can be unregistered (PiModelRegistry.deleteProvider) - dialog answers are validated against the requesting method
|
Second round pushed (7af288b): extension tools now drain queued writes before returning; snapshot count/per-file limits refuse rather than truncate; native providers can be unregistered; dialog answers are validated against the request's method. |
- tool-registry reconciliation preserves an extension's active tool selection - ctx.signal carries the hook/tool invocation's abort signal through the lane scope - provider ids displaced by an extension are restored on unregister - workspace symlinks are snapshotted into the shell and synced back as links - frame, prompt, image and text size limits on the wire - disposition table keeps a bounded newest window
|
Third round pushed (645e5da): active-tool selection survives registry refresh; ctx.signal reflects the real invocation abort; displaced host providers are restored on unregister; symlinks are represented in the shell snapshot and synced back; wire size limits (frame/prompt/images/text); bounded disposition retention (newest 1024). |
- workspace sync handles entry type transitions (symlink/file/directory) - agent_end collects the run's entry ids synchronously at dispatch - per-lane tool-registry baseline is durable so deploy-time tools activate - custom resourceLoader skills/prompts feed harness resources and commands - dynamic registerCommand triggers a commands broadcast - compaction firstKeptEntryId derived from the retained tail
|
Fourth round pushed (225efec): sync handles symlink/file/directory type transitions; agent_end uses entry ids captured synchronously at dispatch; durable per-lane tool baseline so deploy-time tools activate; custom resourceLoader prompts/skills feed resources and commands; dynamic registerCommand broadcasts commands; firstKeptEntryId is derived from the retained tail. |
There was a problem hiding this comment.
Note
This report is out of date. Scroll down for Devin Review's latest report on this PR.
Devin Review found 5 new potential issues.
10 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| // The configuration form resolves no models, so leaving the pi-ai | ||
| // provider in place would keep serving the registration it replaced. | ||
| removeNative(providerOrName); | ||
| overlay.set(providerOrName, config); |
There was a problem hiding this comment.
🟡 Configured providers leave hosts active
Registering configuration over a host provider ID makes registerProvider leave the host active. Model lookup still serves the replaced host provider.
Learn more
A configuration-form registration occupies a provider name but intentionally cannot create a pi-ai provider itself. The adapter therefore needs to hide any provider already installed under that name and restore it when the extension unregisters or switches registrations. removeNative only removes providers previously registered through this extension surface, so a host provider is never displaced by the configuration form.
Example: The host configures provider shared, then an extension calls registerProvider("shared", config). registrations() reports the extension registration, but models.getModel("shared", ...) still returns the host's models.
Recommended fix: Track and remove a host provider when the configuration form first takes its ID. Preserve that displaced provider until the overlay is unregistered or replaced, then restore it consistently for both registration forms.
Was this helpful? React with 👍 or 👎 to provide feedback.
- sync removes workspace directories deleted under a sandbox root - config-form provider registration displaces and later restores a host provider - tool refresh reruns while registrations arrive mid-pass - shell execs on one environment are serialized - demo destructive-command gate tokenizes the command line
|
Fifth round pushed (4fec249): sandbox-root descendants deleted by a script are removed on sync; config-form provider registration displaces and restores host providers like the native form; tool refresh loops while dirty; exec is serialized per environment; the demo destructive gate tokenizes command words (documented as a UI demo, not a security boundary). |
- usePiSession resets lane-local state when the lane prop changes - ctx.hasPendingMessages counts durable submissions waiting for the lane driver
|
Sixth round pushed (f3885a3): the React hook resets lane-local state on lane change; hasPendingMessages includes intake submissions waiting for the lane driver. |
…ions Compactions and navigations waiting in intake are operations without a message, so they no longer flip ctx.hasPendingMessages().
|
Pushed 0f12e25: hasPendingMessages now counts only prompt/skill/prompt_template submissions waiting in intake; compaction and navigation are excluded, with a test for the compaction case. |
|
making draft. this feels like vendoring too much. |
Summary
Brings pi's coding-agent extension API into the Workers pi harness example, with pi holding as much of the code as possible.
ExtensionRunner,ExtensionAPI, the factory loader, slash-command and prompt-template helpers are copied byte-for-byte from earendil-works/pi @c4b0e35aintoexamples/next/harnesses/pi/vendor/pi-coding-agent-src/. Stubs live at upstream's relative import paths, a single patch strips jiti/fs from the loader, andpnpm vendor:pi:check(now part ofpnpm run check) guards against drift. No upstream changes needed.AgentHarness.hooksthrough one adapter (tool_call→before_tool,context→transform_context,before_agent_start→before_run, …). Harness events feed the extension notification events. Handler failures surface ashandler_erroron the durable stream.ExecutionEnv(FileSystem & Shell) is implemented over@cloudflare/shell's Workspace with just-bash in the isolate, so pi's ownread/write/edit/bashtools run in the Durable Object with no containers.extension_ui_request/extension_ui_settled/extension_ui_response,commands,flagsandhandler_errorframes. The React client renders dialogs, notices, flag toggles and/command autocomplete./notewith a confirm gate behind a flag), a prompt template, README updates, and a living design doc atdesign/pi-extensions.mdincluding the checklist for folding this intopackages/agents.src/harness/extensions/andsrc/harness/env/never importpi-harness.ts, so they lift into the package later as-is.Deliberately unsupported
project_trust,user_bash,session_before_switch/fork;newSession/fork/switchSession/reloadreturn cancelled (one durable session per DO). Extension slash commands run out of band, not as durable operations. Shell is just-bash built-ins over a whole-workspace snapshot; containers slot in behind the sameExecutionEnvport.Test plan
pnpm testinexamples/next/harnesses/pi: 50 tests across 6 files (harness eviction, execution env, built-in tools, extensions incl. UI round-trip over a real WebSocket, transport, ui-bridge)pnpm run checkat root (sherif, exports, vendor drift, oxfmt, oxlint, typecheck of all projects)pnpm buildof the examplepnpm run start, try/note remember this, a bash write + read, toggleconfirm_destructiveand ask forrm, reload mid-turnFollow-ups
vendor/pi-devtarballs).packages/think's bash tool has the same sandbox-root deletion bug fixed here inworkspace-execution-env.ts(#syncfinal directory pass); left untouched in this PR.