feat(gatekeeper-webhook): inbound webhook endpoints for workspaces - #85
Closed
MartinRoberts-Fountain wants to merge 1 commit into
Closed
Conversation
Adds a Webhooks gatekeeper: each workspace gets its own inbound HTTP endpoints that third-party services POST to, delivered to a persistent workspace callback as an observation. There was no way to get an external event into a workspace. The Workshop already exports ExternalMessageGateway for this, but nothing binds it, and it takes a caller email it trusts unconditionally -- unusable as a public surface. This gatekeeper is the missing piece. Design notes worth reviewing: * Ack fast, deliver async. The receiver answers 202 and queues; an alarm delivers with eight attempts and backoff. An agent turn outlasts any sender's timeout, so handling inline would make senders retry or disable the endpoint. * Tokens follow the share-key discipline: 256-bit random, only the HMAC-SHA-256 digest is stored, returned exactly once by the call that mints it. * EndpointIndex is a DO per endpoint ID that maps it to its account, so an endpoint URL carries no account identity -- two endpoints handed to two vendors stay uncorrelated. * Disable pauses an endpoint but keeps its URL and token. Unlike a schedule, the URL already lives in a third party's configuration, so a pause has to be resumable. * authorization/cookie/proxy-authorization are stripped before delivery; service signature headers are deliberately kept, since a gadget verifying x-hub-signature-256 needs them and they grant no access. * The observation description names source, timestamp and size only. The body is untrusted third-party input and must not land in the action log. Endpoints can be created two ways. WEBHOOKS.register() on the ambient binding mints endpoint and token together. Or "Connect resource" -> "Webhook endpoint" in a workspace creates one as a per-endpoint binding, which is what lets a workspace run several independent flows: each endpoint delivers to its own gadget and therefore reaches only that gadget's connections. Verified end to end against a local instance: an agent registered an endpoint, a curl'd Alertmanager payload arrived, and the gadget spawned a triage agent chat in a clean context. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
I have read the CLA Document and I hereby sign the CLA You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot. |
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
packages/gatekeeper-webhook: a gatekeeper that gives each workspace inbound HTTP endpoints a third-party service can POST to. A delivery wakes a persistent workspace callback with the request's headers, query and body.There is currently no way to get an external event into a workspace.
ExternalMessageGatewayexists inworkshop-backendfor roughly this, but nothing binds it and it takes acallerEmailit trusts unconditionally, so it can't be a public surface on its own. This is the missing piece in front of it.Shape
webhook.tsendpoint-registry.tsendpoint-index.tsreceiver.tsfetchhandlerendpoint-core.tsapp/,src/configurator/Decisions worth a reviewer's attention
Ack fast, deliver async. The receiver answers
202and queues; an alarm delivers with eight attempts and backoff. An agent turn outlasts any sender's timeout, so handling inline would make senders retry or disable the endpoint.Tokens follow the share-key discipline — 256-bit random, only the HMAC-SHA-256 digest stored, returned exactly once by the call that mints it. See the open question below.
EndpointIndexexists to keep URLs uncorrelated. It maps an endpoint ID to its account so the URL itself carries no account identity; two endpoints handed to two different vendors can't be linked. The cost is one extra DO hop per delivery, which is cheap at webhook volumes.Disable pauses, it does not invalidate. Unlike a schedule, the URL already lives in a third party's configuration, so a pause has to be resumable. Disabling drops queued deliveries and the capability but keeps the endpoint's ID and token.
Credential headers are stripped, signature headers are not.
authorization,cookieandproxy-authorizationnever reach the workspace.x-hub-signature-256and friends deliberately do, since a gadget verifying a payload needs them and they grant no access.The observation names source, timestamp and size only. The body is untrusted third-party input; putting it in the action log would echo secrets into a record the workspace's collaborators can read.
Two creation paths
WEBHOOKS.register()on the ambient binding mints endpoint and token together and binds the hook in one step.Connect resource → Webhook endpoint in a workspace creates one as a per-endpoint binding. This is what lets a workspace run several independent flows: each endpoint delivers to its own gadget and therefore reaches only that gadget's connections. An alerts endpoint bound to a gadget holding ClickHouse cannot reach GitHub, and an agent-spawner's
envnarrows a spawned triage agent further.An endpoint belongs to the first workspace that binds it; a second bind is refused rather than silently redirecting a live URL.
Verification
67 worker tests + 8 app tests.
pnpm lintandpnpm types:checkclean. Golden manifest regenerated.Also exercised end to end against a local instance: an agent registered an endpoint,
curlof an Alertmanager payload returned202, the gadget received it, and it spawned a triage agent chat in a clean context. Both spawned agents independently refused to act on arunbookURL planted in the payload, which is the prompt-injection fencing doing its job.Open question for reviewers
Configurator-created endpoints currently have no token until one is generated in the Webhooks app.
ConfiguratorUISpec.renderis synchronous and the form closes on submit, so there is nowhere to reveal a secret exactly once. I split creation from issuance rather than weaken hash-only storage.In use this reads as broken — you create a webhook and get no credential. The fix I'd make is reveal-once storage: mint the token at creation, keep the plaintext until the first reveal, then drop it and keep only the hash. Steady state stays hash-only, and it matches how the platform already stores OAuth access tokens for other vendors. Not done here; flagging it rather than shipping the decision silently.
Two other things left undone:
spawnCallable()for accumulating one agent per incident is untested, and submitting the configurator form was verified only as far as render (the sandboxed frame doesn't take synthetic clicks in the tooling I had).