Repository navigation
feat: agent onboarding: remote MCP with OAuth, lasting agent connections, sandbox in every SDK and the CLI (web 0.48.0) - #557
Merged
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
Dependency ReviewThe following issues were found:
License Issuesapps/web/package.json
OpenSSF Scorecard
Scanned Files
|
…1.19.0)
WebhooksCC.sandbox() keeps the sandbox in ~/.config/whk/agent.json and
reuses it; waitForClaim() saves the account connection, connected() and
fromSavedConnection() read it back, and connect({ email }) connects an
account without a sandbox. The file's rules are in
conformance/agent-credentials.md with vectors every port runs. Adds a fetch
option to the client and deprecates the auth.md v0.1 helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1.15.0) Without an API key the server starts with the full tools when the agent credential file holds a live connection, and swaps back to the sandbox tools when the human disconnects it. setup works without a key. registerTools takes a small ToolServer interface and has a remote mode, exported as @webhooks-cc/mcp/tools for the hosted server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…0.3.0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….6.0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…here guide (web 0.48.0) A remote MCP server at /mcp (MCP 2026-07-28 with the 2025 fallback) behind an OAuth 2.1 authorization server (authorization code with PKCE, Client ID Metadata Documents and dynamic client registration, rotating refresh tokens, iss in responses); grants are agent registrations of kind oauth (migration 00066). /auth.md starts with the shortest way in for each kind of agent. Agent Skills index, RFC 9727 API catalog, OAuth in openapi.yaml, and markdown twins of the homepage and docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntial file Consent errors are shown, never redirected, and redirect URIs and client metadata documents on webhooks.cc's own hosts are refused; app schemes are an allowlist plus reverse-domain ones. Hosted sends to a capture URL go through the notify proxy. Remote waits are bounded (50 s, polls 1 s to 50 s apart, 8 running tools per account, no batches). A refresh retry inside the grace window supersedes the token it lost. Writes that follow from using a credential slot only apply while it holds the same registration, in every port; an unreadable file is never written over; claim tokens never print. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s short The hosted server advertises no listChanged, so a subscriptions/listen stream completes at once. Poll loops never sleep past the deadline, test_webhook_flow polls once a second, and wait_for_email reads the endpoint before it starts waiting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The token endpoint never answers 401 or invalid_client; jwt-bearer takes /mcp as a resource; dynamic registration takes app schemes and always registers public clients. Go: a refused assertion fails locally from then on, sandbox_closed drops the slot, emails are normalised, a dead pending claim starts a new registration, claim polls wait out Retry-After, and the protocol structs never print their secrets. Client ids on IP literals or localhost are refused before any fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python and the CLI now match the TS and Go behaviour: sandbox_closed drops the slot, emails are normalised, a finished service_auth registration starts a new one (a rate limit or an approval in flight does not), and connected() checks the saved connection. The CLI's conditional credential writes are decided on the same read they write from, device login no longer needs a working agent connection, a transient token failure takes the stream's reconnect backoff, and sandbox wait honours Retry-After. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The theme toggle and the floating nav it sat in are gone since the one anchored header landed; the consent page now uses SiteHeader like the agent claim page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kroqdotdev
force-pushed
the
feat/agent-onboarding
branch
from
October 28, 2026 22:54
769a738 to
bd37c9d
Compare
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.
Makes it as easy as possible for AI agents to find, register for and use webhooks.cc, ideally without their human touching anything. One PR for the whole effort, as asked.
What changes
1. What made agents fail
~/.config/whk/agent.json(XDG, macOS~/Library/Application Support/whk/agent.json, Windows%APPDATA%\whk\agent.json,WHK_AGENT_FILEoverride), mode 0600, atomic writes. The MCP server, the TypeScript, Python and Go SDKs and thewhkCLI all read and write it. Spec:packages/sdk/conformance/agent-credentials.md; every implementation runs the same vectors (agent-credentials.json, 15 path, 6 issuer, 31 read and 16 write cases). The claim token is never written, as/auth.mdasks.invalid_grantremoves the slot; the MCP server then swaps back to the sandbox tools.npx @webhooks-cc/mcp setup <tool>works without an API key (the VS Code and Codex configs it writes now use their current formats).registerAnonymous,registerWithEmail,confirmEmailOtp,registerWithIdJag, theirpollClaim/waitForClaim) are@deprecatedand warn once; removal needs a major version, so it waits for G after 2026-11-30. npm shows about 7 downloads a week across MCP versions older than 1.11.0 (the ones that used v0.1). The 410 and verified_email answers now carry an upgrade notice in their body.WebhooksCC.sandbox(): register with proof of work, reuse the saved sandbox, claim,connect(email),connected().whk sandbox create|list|get|delete|requests|request|wait|status|connect|forget, all with--json;connectprints one JSON line with the link and code, waits, then prints the outcome.2. /auth.md points agents at the tools
A "Start here" section above Step 1 lists the shortest way in for each kind of agent: the remote MCP server, the local MCP server, the Claude Code plugin, the Python/Node/Go SDKs and the CLI, plain HTTP; then the credential file and the claim email. The agents docs, llms.txt, the plugin skill and the MCP and SDK READMEs say the same.
3. Remote MCP server at https://webhooks.cc/mcp
Design note
app/mcp/route.ts,lib/mcp/hosted.ts), not a separate service. It uses the v2 MCP TypeScript SDK (@modelcontextprotocol/server2.4, which implements 2026-07-28) withcreateMcpHandler: stateless, noinitializeorMcp-Session-Idfor 2026-07-28 clients (per-request_meta,server/discover,Mcp-Method/Mcp-Nameheaders,ttlMs/cacheScopeontools/list), and the SDK's stateless fallback for clients still on the 2025 revisions, which is most hosted clients today. JSON responses only.registerTools()frompackages/mcp/src/tools.tsnow takes a smallToolServerinterface; the stdio server keeps the v1 SDK unchanged, the remote one adapts the v2 server. A new@webhooks-cc/mcp/toolsentry exports the definitions without the stdio server. Remote mode leaves outsend_toandreplay_request(they post to a URL the caller names, which would run from our network and cannot reach the caller's machine anyway), refusestargetUrlonrender_emailandtest_webhook_flow, and caps waits at 50 s (Cloudflare ends requests at 100 s): 44 tools.MCP_INTERNAL_API_URL, defaulthttp://127.0.0.1:$PORT) with the caller's own bearer and address (cf-connecting-ip), so the API routes authenticate, rate-limit and audit exactly as for any client.whcc_rows inapi_keys(1 hour), so every route accepts them unchanged; a dashboard API key also works as a bearer at/mcp. Refresh tokens arewhrt_, rotate on every use, and stop after 90 days unused; a rotated token presented again within 60 s gets a new pair (a client that lost a response) and supersedes the one it lost; later, or a superseded token at any time, revokes the grant. Codes arewhac_, 5 minutes, single use; a replayed code revokes what the first use created.oauth, claimed at once (migration 00066), so they share Connected agents, the cap of 10 and disconnecting with agents connected by code.webhooks:read webhooks:write, as for claimed agents. Like every agent token they are advisory: routes do not enforce them per call, so a client asking for less still gets the account API, and the consent page says so. Enforcing scopes per route is a separate piece of work.client_id_metadata_document_supported) are preferred, and Dynamic Client Registration is supported too, because most clients still use it. Documents are fetched through the notify proxy like forwarding, so the box's IP stays hidden, and are cached for 5 minutes; the proxy returns the first 1 KB, which is the size limit (real documents from Claude, Claude Code and VS Code are 300 to 500 bytes). Public clients only (PKCE S256); a DCR request for a secret-based method is registered asnone(RFC 7591 3.2.1). Loopback redirect URIs match on any port (RFC 8252).iss(RFC 9207;authorization_response_iss_parameter_supported), and DCR client ids exist only at this issuer./oauth/authorizereuses the web session (sign in, then approve once in the client's connect dialog, no code). It posts with the session's bearer token, never a cookie, and navigates to the redirect itself, because the CSP'sform-action 'self'would block a redirect after a form post. A refused request is shown, never redirected: a dynamically registered redirect URI proves nothing, so the page navigates only after the human clicks Connect or Decline. Redirect URIs must be https (not on webhooks.cc's own hosts), http on loopback, or an app's own scheme (an allowlist plus reverse-domain schemes); client ids on our own hosts are refused, since anyone can serve a document from a capture URL.send_webhookandtest_webhook_flowpost to a capture URL through the notify proxy, so a capture never shows the box's IP./mcp300 requests a minute per token, 120 a minute per address without one, one JSON-RPC message per request (no batches), and 8 tool calls running at once per account (counted around the handler, since a 2025-era client's answer streams back before the tool finishes); waits capped at 50 s with polls 1 s apart at least; DCR 20 an hour per address; the authorization_code and refresh_token grants 600 a minute per address (hosted clients refresh for many users from few addresses), the existing agent grants keep 60./.well-known/oauth-protected-resource/mcpfor the resourcehttps://webhooks.cc/mcp, which 401s point to; the authorization server metadata gainsauthorization_endpoint,registration_endpoint,code_challenge_methods_supported,response_types_supportedand the rest.packages/mcp/server.jsongains aremotesentry.4. Discovery
/.well-known/agent-skills/index.json(Agent Skills Discovery RFC v0.2.0) serving the webhook-testing skill with its digest./.well-known/api-catalog(RFC 9727 linkset), advertised in theLinkheader next torel="auth.md"on every response.openapi.yaml: the OAuth2 security scheme and the agent identity, sandbox and token paths./index.md,/docs/<page>.md, andAccept: text/markdownon/and/docs/*.register_uri/claim_uriare kept for the agent-readiness scanners noted in the code.Versions
web 0.48.0 (0.46.0 and 0.47.0 belong to #555 and #556), SDK 1.19.0, MCP 1.15.0, CLI 1.6.0, Python 0.3.0, Go 0.3.0. Migration 00066 (00065 is #555's).
Rollout (needs your OK at each production step)
00066_oauth_mcp.sqlto production, thenNOTIFY pgrst, 'reload schema'./mcp,/openapi.yaml,/docs/*.mdand/install.sh.sdk-v1.19.0, thenmcp-v1.15.0once the SDK is on npm (the MCP Registry entry with the remote goes out with it),sdk-python-v0.3.0,packages/sdk-go/v0.3.0, and the CLI release.Cloudflare rule (not applied)
Zone webhooks.cc, Rules, Configuration Rules, rule "Browser Integrity Check off for receiver, API and agent discovery". Replace its expression with:
Check afterwards with
curl -s -o /dev/null -w '%{http_code}' -A 'Python-urllib/3.12' https://webhooks.cc/openapi.yaml(should be 200).Verified
-race, staticcheck, Windows vet of the file code), CLI 145 (clippy clean);pnpm typecheck,pnpm lint, Prettier andnext buildpass./mcpin both protocol eras with real tool calls over loopback (create_endpoint,send_webhookcaptured by the local receiver,wait_for_request,list_endpoints,delete_endpoint); the app is listed under Connected agents.whk sandbox connectplus a human completing the claim left a claimed slot (no claim token in the file);WebhooksCC.connected()and a fresh stdio MCP server then started connected (46 tools); after the human disconnected the agent, the MCP server swapped back to the 11 sandbox tools and the slot was removed.code,stateandiss.WebhooksCC.connected()with an existing claimed assertion exchanged it, listed the account's endpoints and cached the token in the file.Review
CodeRabbit skips PRs over 100 files, so two fresh subagent reviews (OAuth and
/mcp; the credential file across the five implementations) and a verification round were the gate. Round one found five blocking issues, all fixed: an open redirect from the consent page's error handling, client metadata documents served from a capture URL, the hostedsend_webhookexposing the origin IP, unbounded wait loops, and the TS store removing slots another process had replaced (writes that follow from using a slot are now conditional on the registration, in the spec too). The verification round confirmed those and found the wait-loop bounds still incomplete (the cap counted HTTP requests, which a 2025-era streamed answer or a batch slipped past; the poll floor had no ceiling and did not apply to a left-out interval); both are fixed as described above. A third, focused round confirmed the bounds hold (no blocking issues); its four optional points are in too (nolistChangedon the hosted server so listen streams close at once, polls never sleep past the deadline,test_webhook_flowpolls once a second,wait_for_emailreads the endpoint before waiting). A fresh review on a different model (fable, with sub-reviews of the docs and OpenAPI and of the Go port) found no blocking code issues; it corrected the OpenAPI description of the token and registration endpoints, and its optional points for the Go, Python and Rust ports are fixed (refusals remembered,sandbox_closeddrops the slot, emails normalised, dead pending claims re-registered,Retry-Afterhonoured, secrets never printed, the CLI's conditional writes made atomic, device login independent of the agent connection).CodeQL alert 207 (
js/request-forgeryon the forwarding module's direct send, now reachable from the OAuthclient_id) was researched separately and dismissed as a false positive: the source is the signed-in consent route; client ids must be https, not on our hosts, not IP literals or localhost; production fetches through the notify-proxy Worker; the direct path refuses private and reserved addresses after resolution, pins the checked addresses, follows no redirects and reads 1 KB.Decisions for you
WebhooksCC.sandbox()now reads and writes the file by default, like the other tools (credentialsFile: falseopts out). Test suites that create sandbox endpoints without deleting them will share one sandbox per machine for 24 hours.Vary: Accepton the HTML homepage and docs needs Caddy (header +Vary Acceptfor/and/docs*); Next replaces it on rendered pages. Cloudflare does not cache HTML here, so nothing breaks meanwhile.🤖 Generated with Claude Code