Skip to content

feat(agent_api_keys): one-call webhook-trigger setup endpoint#329

Merged
danielmillerp merged 5 commits into
mainfrom
dm/agentex-webhook-trigger
Jun 23, 2026
Merged

feat(agent_api_keys): one-call webhook-trigger setup endpoint#329
danielmillerp merged 5 commits into
mainfrom
dm/agentex-webhook-trigger

Conversation

@danielmillerp

@danielmillerp danielmillerp commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What

Adds POST /agent_api_keys/webhook-trigger — wires a webhook trigger in one call:

  • registers a github/slack signature-verification key for the agent (auto-generates the signing secret if not provided),
  • returns the ready-to-paste forward webhook URL + the secret (shown once).
POST /agent_api_keys/webhook-trigger
{ "agent_name": "golden-agent", "source": "github",
  "name": "<owner/repo>", "forward_path": "github-pr/<config-id>" }
→ { key_id, secret, webhook_path, webhook_url }

Why

The pieces to trigger an agent from a webhook already exist (the /agents/forward ingress verifies the signature against an agent key, and POST /agent_api_keys registers keys). This bundles key-create + webhook-URL composition so a UI (or a curl) can set up a trigger in a single step instead of two — the backend for the self-serve "Add trigger" button. The webhook then flows through the existing forward ingress unchanged.

Before — wiring a trigger was two manual steps, and the caller had to know the agent's internal id, invent a secret, and hand-compose the forward URL:

# 1. register the signing key (need the agent's internal id + your own secret)
POST /agent_api_keys
{ "agent_id": "<look-up-first>", "api_key": "<generate-yourself>",
  "name": "owner/repo", "api_key_type": "github" }

# 2. hand-build the forward URL from the convention you have to know
webhook_url = f"{public_url}/agents/forward/name/{agent_name}/github-pr/{config_id}"

After — one call, by agent name, returns the URL + secret ready to paste:

POST /agent_api_keys/webhook-trigger
{ "agent_name": "golden-agent", "source": "github",
  "name": "owner/repo", "forward_path": "github-pr/<config-id>" }

→ { "key_id": "...", "secret": "ab3f…",            # generated for you, shown once
    "webhook_path": "/agents/forward/name/golden-agent/github-pr/<config-id>",
    "webhook_url":  "https://<host>/agents/forward/name/golden-agent/github-pr/<config-id>" }

No new ingress, no migration — built on the existing agent_api_keys + forward mechanism.

On the signing secret

This matches how GitHub webhooks actually work. A GitHub webhook's Secret is a value you supply (GitHub doesn't generate one) — you type a high-entropy string into the webhook's Secret field, and GitHub uses it to HMAC each payload into X-Hub-Signature-256: sha256=…, which the receiver re-computes and compares. It's a shared secret that must exist on both sides (GitHub's config + the verifying server), and it's optional-but-recommended (no secret → no signature header at all). Refs: GitHub docs validating-webhook-deliveries, creating-webhooks.

Given that, the endpoint's secret handling is deliberate:

  • omit secret (default) → the endpoint generates a strong one (secrets.token_hex(32)), stores it on the agent's verification key, and returns it once to paste into GitHub's Secret field — so the caller never has to invent entropy.
  • supply secret → for when the GitHub webhook already has a secret set and the agent side just needs to match it.

Testing

  • 4 unit tests (URL composition, provided vs generated secret, 409 on duplicate, 400 on non-webhook source). ruff clean.

🤖 Generated with Claude Code

Greptile Summary

This PR adds POST /agent_api_keys/webhook-trigger, a single-call convenience endpoint that registers a GitHub or Slack signature-verification key for an agent and returns the ready-to-paste forward webhook URL and signing secret. It builds on the existing agent_api_keys store and /agents/forward ingress without any new migration or routing.

  • The handler correctly validates source type, enforces Slack callers supply their own Signing Secret (vs. auto-generating for GitHub), URL-encodes agent name and forward path via urllib.parse.quote, and checks for control characters in forward_path before the DB write — all issues from earlier review rounds have been addressed.
  • Eight unit tests cover URL composition, provided vs. generated secret, empty-string secret preservation, URL-encoding of reserved characters, control-char rejection, duplicate-key 409, invalid-source 400, and Slack-without-secret 400.

Confidence Score: 5/5

Safe to merge — all validation (source check, Slack secret requirement, control-char rejection) runs before the DB write, URL components are correctly percent-encoded, and the 409 response no longer leaks internal identifiers.

All issues raised in earlier review rounds have been addressed: input validation precedes the key creation call, the is not None guard preserves empty-string secrets without silent coercion, Slack requires the caller to supply the Signing Secret, and the forward path is properly encoded. The unit tests exercise all meaningful branches including the fixed edge cases.

No files require special attention.

Important Files Changed

Filename Overview
agentex/src/api/routes/agent_api_keys.py New create_webhook_trigger handler with correct validation ordering (all input checks precede the DB write), explicit None-check for secret, URL-encoding for agent name and forward path, and a clean 409 message that avoids leaking internal UUIDs.
agentex/src/api/schemas/agent_api_keys.py Adds CreateWebhookTriggerRequest and CreateWebhookTriggerResponse Pydantic models; secret field description now accurately documents the Slack requirement (must be supplied) and GitHub behaviour (auto-generated if omitted).
agentex/tests/unit/api/test_webhook_trigger.py Eight focused unit tests covering happy path, provided/generated/empty secrets, URL-encoding of reserved characters, control-char rejection (with create mock asserting it was never called), duplicate 409, invalid source 400, Slack-without-secret 400, and Slack-with-secret success.
agentex/openapi.yaml Adds the new endpoint and both request/response schemas; documents 200 and 422 consistent with all existing endpoints in the file.

Reviews (7): Last reviewed commit: "Merge branch 'main' into dm/agentex-webh..." | Re-trigger Greptile

@danielmillerp
danielmillerp requested a review from a team as a code owner June 22, 2026 05:16
Comment thread agentex/src/api/routes/agent_api_keys.py Outdated
Comment thread agentex/src/api/routes/agent_api_keys.py Outdated
@danielmillerp
danielmillerp force-pushed the dm/agentex-webhook-trigger branch from fdadd80 to 6ca6f70 Compare June 22, 2026 05:27
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

✱ Stainless preview builds

This PR will update the agentex-sdk SDKs with the following commit messages.

openapi

feat(api): add webhook trigger endpoint to agent_api_keys

python

chore(internal): regenerate SDK with no functional changes

typescript

chore(internal): regenerate SDK with no functional changes
agentex-sdk-openapi studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅

⚠️ agentex-sdk-typescript studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️build ⏭️lint ⏭️test ✅

agentex-sdk-python studio

Your SDK build had a "fatal" conclusion, and no code was generated.


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-06-23 20:36:31 UTC

Comment thread agentex/src/api/routes/agent_api_keys.py
@declan-scale

Copy link
Copy Markdown
Collaborator

Will we want a rotate path as well should we want to rotate the api key?

Comment thread agentex/src/api/routes/agent_api_keys.py Outdated
@danielmillerp

Copy link
Copy Markdown
Collaborator Author

Will we want a rotate path as well should we want to rotate the api key?

Good call — deferring a dedicated rotate path to a follow-up, keeping this PR to the create flow. Today rotation works by delete + recreate: the forward URL/path is derived from agent name + forward_path (not the secret), so deleting the key and re-creating the trigger yields a new secret to paste while the webhook URL stays the same. A dedicated rotate (regenerate-secret-in-place, returned once, URL unchanged) is cleaner and avoids the brief gap between delete and recreate — worth adding alongside the M2M owner-key work where key rotation matters more. Tracking it as a follow-up.

Comment thread agentex/src/api/routes/agent_api_keys.py Outdated
@danielmillerp
danielmillerp force-pushed the dm/agentex-webhook-trigger branch from f3a13f8 to 949cc2d Compare June 23, 2026 02:15
Comment thread agentex/src/api/routes/agent_api_keys.py Outdated
Comment thread agentex/src/api/schemas/agent_api_keys.py
@declan-scale

Copy link
Copy Markdown
Collaborator

@danielmillerp can you address the greptile comments, then good to go

danielmillerp and others added 3 commits June 23, 2026 15:34
Add POST /agent_api_keys/webhook-trigger: registers a github/slack signature key
for an agent and returns the ready-to-paste forward webhook URL + secret in one
call. Bundles the existing key-create with webhook-URL composition so a UI (or a
curl) can wire a trigger in a single step; the webhook then flows through the
existing /agents/forward ingress that verifies the signature against this key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slack signs requests with the app's existing Signing Secret, not a per-webhook
secret we can generate. Auto-generating one for a Slack trigger stored a random
value that would never match, so every real Slack delivery failed signature
verification. Now Slack requires the caller to supply 'secret'; GitHub still
auto-generates. Addresses Greptile P1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@danielmillerp
danielmillerp force-pushed the dm/agentex-webhook-trigger branch from 949cc2d to de85c65 Compare June 23, 2026 19:58
@danielmillerp

Copy link
Copy Markdown
Collaborator Author

Addressed the latest Greptile comments in de85c65.

  • Preserves an explicitly provided empty GitHub secret by using an explicit None check instead of truthiness.
  • URL-encodes agent_name and forward_path when composing the returned forward URL, while preserving subpath slashes.
  • Rejects control characters in forward_path.
  • Updates the request schema docs so Slack clearly requires the app Signing Secret, while GitHub may omit secret to generate one.
  • Added unit coverage for empty GitHub secret, reserved URL chars, control-char rejection, and the Slack secret schema text.

Comment thread agentex/src/api/routes/agent_api_keys.py Outdated
@danielmillerp
danielmillerp force-pushed the dm/agentex-webhook-trigger branch from be8a6ab to 88fcebd Compare June 23, 2026 20:21
@danielmillerp
danielmillerp enabled auto-merge (squash) June 23, 2026 20:29
@danielmillerp
danielmillerp merged commit 9cc71fb into main Jun 23, 2026
30 of 31 checks passed
@danielmillerp
danielmillerp deleted the dm/agentex-webhook-trigger branch June 23, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants