feat(server): add web (REST) hook provider - #389
Open
tbrandenburg wants to merge 3 commits into
Open
Conversation
added 3 commits
August 14, 2026 21:46
POST /api/hooks/:providerId accepted and validated a providerId in the URL, but HookEventHandler.handleEvent(_providerId, event) never read it -- every hook POST reached the same hardcoded Claude provider instance regardless of the id in the URL. This adds a real registry and makes providerId meaningful: - server/src/providers/index.ts: Map<string, HookProvider> + getProvider(id), falling back to the default (Claude) for any unregistered id so existing callers keep working exactly as before. getAllProviders() for future cross-provider concerns (e.g. capability unions). - HookEventHandler now resolves the provider per event by providerId instead of a constructor-captured singleton. protocolVersion mismatches are now checked per-provider-id with warn-once logging (was constructor-time, single-provider only). - AgentRuntime wires HookEventHandler through the shared providerRegistry; its own constructor still takes one 'primary' HookProvider for the Claude-specific file-fallback/Agent-Teams singleton wiring, which stays a single-provider concern until a provider with a file fallback or team extension actually exists. - Tests: providerRegistry.test.ts (resolution + fallback), and a 'provider registry routing' suite in hookEventHandler.test.ts proving two distinct providerIds reach two distinct provider instances end-to-end (via a minimal synthetic second provider, standing in until a real second provider lands) and that unknown-provider / protocol-mismatch cases are dropped with a single warning. No behavior change for the only provider that exists today (Claude): all 383 server tests pass, lint/types/build/asyncapi/e2e-inventory all clean with zero drift.
Adds a second HookProvider (id: 'web') driven purely by HTTP requests, so an agent character can be created and animated with curl -- no CLI, no transcript file, no terminal. Built on top of the provider-registry refactor (previous PR): both 'claude' and 'web' are always live simultaneously at their own /api/hooks/:providerId routes. - server/src/providers/hook/web/: the HookProvider. normalizeHookEvent covers all 8 AgentEvent kinds reachable via hooks in a non-team CLI (sessionStart, sessionEnd, toolStart, toolEnd, turnEnd, permissionRequest, subagentStart, subagentEnd); subagentTurnEnd (Agent-Teams-only) and progress (JSONL-fallback-only) have no equivalent here by design. installHooks/ uninstallHooks are no-ops and areHooksInstalled is always true -- there's nothing to install, the HTTP call itself is the hook. Free-form tool vocabulary (empty readingTools/subagentToolNames/permissionExemptTools Sets): unlike Claude's closed set of built-in tools, any external script/tool can be the caller here. A caller-supplied 'status' field overrides the default 'Using <toolName>' display text. - fix(hookEventHandler): subagentStart/subagentEnd dispatch was gated behind provider.team, which silently dropped sub-agent events for any team-less provider (the web provider has no team extension). Added an explicit-id fast path: providers that supply a real parentToolId/toolId on the normalized event (unlike Claude's 'current' sentinel, which requires JSONL-populated activeToolNames to resolve) create/clear the sub-agent character directly. Claude's existing team-gated logic is unchanged -- still reached exactly as before via the 'current' sentinel branch. - clientMessageHandler: providerCapabilities now unions readingTools/ subagentToolNames across every registered provider, not just Claude's, so a future provider's own tool vocabulary (if any) still gets classified correctly by the webview. - Tests: server/__tests__/web.test.ts (normalizeHookEvent per event kind, malformed input, no-op installer), providerRegistry.test.ts extended for the web provider, hookEventHandler.test.ts extended with the multi-provider routing proof (claude vs web reach different code) and the explicit-id subagent fast-path test. e2e/tests/standalone/web-provider.spec.ts drives a full agent lifecycle over real HTTP (sendHookEvent against /api/hooks/web) covering all 8 in-scope AgentEvent kinds, observed via Playwright against a real browser -- no VS Code, no mocked CLI. - Dev ergonomics: scripts/web-agent.sh (reads port/token from ~/.pixel-agents/server.json, wraps the curl call) and server/manual-web-events.http (REST-Client sample mirroring the existing Claude one). 414/414 server tests, 52/52 webview tests pass. lint/check-types/build clean. No AsyncAPI drift (no new message types). e2e/README.md regenerated for the new standalone spec.
1 task
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.
Stacked on #388 (
refactor(server): resolve hook provider by providerId) — please merge that one first; this PR's diff will shrink to just what's below once #388 lands and this branch is rebased.What
Adds a second
HookProvider(id: 'web'), driven purely by HTTP requests, so an agent character can be created and animated withcurl— no CLI, no transcript file, no terminal. Bothclaudeandwebare always live simultaneously at their own/api/hooks/:providerIdroutes once #388's registry is in place.Why
core/src/provider.tsexplicitly defers provider-type taxonomy work until "a real second provider... actually lands, derived from that provider's needs rather than speculation." This is that provider, exercising the abstraction at its thinnest: no file fallback, no team, no terminal.Changes
server/src/providers/hook/web/: the provider.normalizeHookEventcovers all 8AgentEventkinds reachable via hooks for a non-team CLI (sessionStart,sessionEnd,toolStart,toolEnd,turnEnd,permissionRequest,subagentStart,subagentEnd);subagentTurnEnd(Agent-Teams-only) andprogress(JSONL-fallback-only) have no equivalent here, by design.installHooks/uninstallHooksare no-ops,areHooksInstalledis alwaystrue— there's nothing to install, the HTTP call itself is the hook. Free-form tool vocabulary (emptyreadingTools/subagentToolNames/permissionExemptToolssets): unlike Claude's closed set of built-in tools, any external script/tool can be the caller here. A caller-suppliedstatusfield overrides the defaultUsing <toolName>display text.hookEventHandler.tsfix:subagentStart/subagentEnddispatch was gated behindprovider.team, which silently dropped sub-agent events for any team-less provider (this one has noteamextension). Added an explicit-id fast path — providers that supply a realparentToolId/toolIddirectly on the normalized event (unlike Claude's'current'sentinel, which requires JSONL-populatedactiveToolNamesto resolve) create/clear the sub-agent character directly. Claude's existing team-gated logic is byte-for-byte unchanged, still reached via the'current'sentinel branch — verified via the full pre-existing Agent-Teams test suite staying green.clientMessageHandler.ts:providerCapabilitiesnow unionsreadingTools/subagentToolNamesacross every registered provider instead of reading only Claude's.web.test.ts(normalizeHookEventper event kind + malformed input),providerRegistry.test.ts/hookEventHandler.test.tsextended with real multi-provider coverage (claude vs. web reach different code) and the explicit-id subagent fast path.e2e/tests/standalone/web-provider.spec.tsdrives a full agent lifecycle over real HTTP (sendHookEventagainst/api/hooks/web, the same sanctioned direct-hook-call patternstandalone/hooks.spec.tsalready uses for Claude) covering all 8 in-scope kinds, observed via Playwright against a real browser — no VS Code, no mocked CLI.scripts/web-agent.sh(reads port/token from~/.pixel-agents/server.json, wraps the curl call) andserver/manual-web-events.http(REST-Client sample mirroring the existing Claude one).Testing
npm run lint/npm run check-types— clean (0 errors; 1 pre-existing unrelated webview warning)npm run asyncapi:validate/npm run asyncapi:generate— valid, zero diff (no new client/server message types)npm run e2e:inventory— regenerated, diff is exactly the new spec's entrynpm run build— succeedsnpm test(server + webview) — 414/414 server tests pass, 52/52 webview tests passnpm run e2e -- e2e/tests/standalone/hooks.spec.ts e2e/tests/standalone/web-provider.spec.ts— both pass (Claude's existing standalone hooks spec has no regression; the new web-provider spec passes end-to-end)scripts/web-agent.sh/curl: spawned multiple agents, drove tool/sub-agent/permission/turn events, watched them animate and reseat in a live browser session.