Repository navigation
feat(api): Add generic webhook source for arbitrary event-driven task spawning - #902
Conversation
f3ed1e9 to
957eb9b
Compare
957eb9b to
2d7e08c
Compare
There was a problem hiding this comment.
2 issues found across 17 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="internal/manifests/charts/kelos/templates/crds/taskspawner-crd.yaml">
<violation number="1" location="internal/manifests/charts/kelos/templates/crds/taskspawner-crd.yaml:1011">
P2: CRD says fieldMapping must include an `id` key, but schema doesn’t enforce it; generic webhook handling will accept missing `id` and leave parsed.ID empty, contradicting the documented requirement.</violation>
</file>
<file name="api/v1alpha1/taskspawner_types.go">
<violation number="1" location="api/v1alpha1/taskspawner_types.go:488">
P2: XValidation does not enforce the documented “set exactly one” requirement for value/pattern; filters with neither field set pass validation.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
2d7e08c to
b9475ed
Compare
|
/kelos review |
|
🤖 Kelos Task Status Task |
There was a problem hiding this comment.
🤖 Kelos Reviewer Agent @gjkim42
Review Summary
Verdict: REQUEST CHANGES
Scope: Adds generic webhook source type to TaskSpawner with path-based routing, JSONPath field extraction, HMAC signature validation, and Helm chart support
Findings
Correctness
-
internal/webhook/generic_filter.go:39/internal/webhook/handler.go:482— SharedFieldsmap leaks state across spawner iterations (blocking)
ExtractFieldsadds keys toparsed.Generic.Fieldsbut never clears the map between spawner iterations. InprocessWebhook(line 357–414), the loop callsmatchesSpawner→ExtractFieldsfor each spawner. If Spawner A hasfieldMapping: {id: "$.a.id", custom_a: "$.a.val"}and Spawner B hasfieldMapping: {id: "$.b.id"}, after processing Spawner A theFieldsmap contains{id: "...", custom_a: "..."}. When Spawner B runs, it overwritesidbut leavescustom_afrom Spawner A. ThencreateTask→ExtractGenericWorkItemfor Spawner B includes the stalecustom_aas a template variable.
Fix: ResetFieldsto a fresh map at the start ofExtractFields. -
internal/webhook/handler.go:563-581—getGenericWebhookConfiglists ALL TaskSpawners on every request (non-blocking)
Every incoming generic webhook triggers aclient.Listof all TaskSpawners to determine signature/delivery headers, in addition to the existinggetMatchingSpawnersList call further down. This doubles the API server load per webhook delivery. Consider combining these into a single List, or caching the config per source with a short TTL. -
internal/webhook/handler.go:576—getGenericWebhookConfigreturns first-match only
If multiple TaskSpawners listen to the same source with differentSignatureHeaderorSignaturePrefixvalues, only the first match's config is used for validation. The second spawner's signature config is silently ignored. Worth documenting in the CRD field comments.
Tests
- Test coverage is comprehensive: parsing, field extraction, filters (AND semantics, exact/regex, nested fields, numeric/boolean coercion), path extraction, signature validation (with/without prefix), HTTP handler tests for matching, non-matching, wrong source, idempotency, and hyphenated source names. Well done.
make verifypasses — generated files (deepcopy, CRD YAML, install-crd.yaml) are up to date.make testwas still compiling from scratch in this container environment; no failures observed at the time of review.
Cubic's prior review — rebuttal
The two P2 violations flagged by cubic are both incorrect:
-
"CRD says fieldMapping must include an
idkey, but schema doesn't enforce it" — Wrong. The CRD YAML (taskspawner-crd.yaml:1084) contains an x-kubernetes-validation rule:'id' in self.fieldMapping. This enforces the requirement at the schema level. -
"XValidation does not enforce the 'set exactly one' requirement for value/pattern" — Wrong. The rule
has(self.value) != (has(self.pattern) && size(self.pattern) > 0)uses XOR semantics that correctly rejects both-omitted and both-set cases.
Security
- No hardcoded secrets — per-source secrets loaded from env vars at runtime
- HMAC-SHA256 signature validation with timing-safe comparison via existing
validateHMACSignature - Payload size limit (10 MB) inherited from existing handler
- Source name from URL path is safe:
os.Getenvtreats the string literally, no injection risk - Optional no-secret mode (skip validation) is clearly logged — appropriate for sources that don't support signing
Conventions
- Logging messages start with capital letters, no trailing punctuation ✓
- Generated files included and match
make updateoutput ✓ - PR description follows template with all sections filled ✓
workspaceRefvalidation rule intentionally excludeswebhooksource — correct per design (generic webhooks may not be git-repo-driven)
Suggestions (optional)
internal/webhook/generic_filter.go:68—regexp.MatchStringrecompiles the regex on every filter evaluation. For high-throughput sources, consider compiling once. Not blocking for v1 but worth a TODO.- Consider adding validation that prevents two spawners from claiming the same
sourcename with conflictingSignatureHeader/SignaturePrefixconfigurations. - Minor:
extractSourceFromPathrejects paths with >3 segments but returns the generic "Missing source" message. A more descriptive error could help debugging.
/kelos needs-input
b9475ed to
9ff4175
Compare
|
@gjkim42 I have implemented review notes and suggestions. Let me know if there's anything else :) |
822abfd to
187500d
Compare
|
Will take a look. Thanks for the PR! |
|
@gjkim42 bump :) |
ab13e94 to
491db8e
Compare
|
I am generally positive about the feature. My only concern is API is hard to fix, so I'd like to find the best way to design it. |
| // Must be lowercase alphanumeric with optional hyphens. | ||
| // +kubebuilder:validation:Required | ||
| // +kubebuilder:validation:Pattern=`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$` | ||
| Source string `json:"source"` |
There was a problem hiding this comment.
The source should be unique in cluster scope, right?
There was a problem hiding this comment.
Yes. The source is unique in the cluster, but we aren't spawning a new webhook server per source (at least not currently).
The current implementation spawns a single GenericWebhook taskSpawner that then listens for everything that isn't routed to one of our specific handlers. This means on the taskSpawner you have to declare which source you care to listen too.
For example, if you are listening to both notion and salesforce on the taskspawners for those you need to declare what source you care about.
There was a problem hiding this comment.
@gjkim42 Any concerns here? Otherwise I'll rework the other thread and push for final review
There was a problem hiding this comment.
Is there a validation that prevents duplicate sources?
There was a problem hiding this comment.
No, because it isn't declared anywhere as a universal.
You are allowed to have multiple task spawners listening to the same source.
And on the webhook server side it just sees the path and reads for the secret. There's no ability for duplication anywhere.
Because once deployed you can send to webhook/test, webhook/notion, webhook/drata without any further configuration on the server side.
Once the generic source is deployed it then just checks for an existing taskSpawner with source that matches what it received.
So there's nothing to ensure isn't duplicated.
52d0314 to
e7f2ffc
Compare
e7f2ffc to
c0c132b
Compare
|
@gjkim42 Updated based on review comments and ensured verify passes. |
c0c132b to
488a20e
Compare
… spawning Adds a new `webhook` source type to TaskSpawner that accepts arbitrary HTTP POST payloads and maps them to task template variables via JSONPath expressions. This enables integration with any webhook-capable system (Notion, Sentry, Drata, etc.) without Kelos-side code changes. Key design: - Path-based routing: /webhook/<source> determines both the URL and the HMAC secret env var (<SOURCE>_WEBHOOK_SECRET) - Configurable JSONPath field extraction via fieldMapping - AND-semantics filters with exact match and regex support - Plugs into the existing kelos-webhook-server with --source generic - No workspaceRef required for generic webhook spawners - Helm chart adds generic source with envFrom-based multi-secret support - If a secret doesn't exist accept json in Implements kelos-dev#687
488a20e to
f960432
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds five conventions distilled from recent PR reviews and applies them across AGENTS.md, the shared and worker AgentConfigs, and the reviewer prompts. 1. Never use os.Getenv() for secrets as Go flag defaults (PR #971) — flag prints defaults in --help output, leaking secret values. 2. Fail fast on invalid configuration (PR #971) — do not silently fall back to unauthenticated/degraded behavior when credentials or config are missing. 3. Keep API surfaces minimal (PR #902) — only fields immediately needed, no speculative additions; API is hard to change once shipped. 4. Docs must match implementation, not aspiration (PRs #1035, #1056) — describe only what the code actually does; verify enforcement before documenting a contract. 5. TaskSpawner conventions (PRs #965, #974): - Prefer webhook-based triggers over poll-based. - {{.Branch}} is empty for issue-only events; use the {{if .Branch}}{{.Branch}}{{else}}main{{end}} fallback form. - issue_comment fires for both issues and PRs; design prompts to detect and handle both contexts. - Do not include manual PR branch checkout instructions — Kelos checks out the PR branch automatically. Also fixes the kelos-reviewer TaskSpawner branch field to use the safe fallback form (was using bare {{.Branch}}, which is empty for issue events; flagged P2 in PR #786 review). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds five conventions distilled from recent PR reviews and applies them across AGENTS.md, the shared and worker AgentConfigs, and the reviewer prompts. 1. Never use os.Getenv() for secrets as Go flag defaults (PR #971) — flag prints defaults in --help output, leaking secret values. 2. Fail fast on invalid configuration (PR #971) — do not silently fall back to unauthenticated/degraded behavior when credentials or config are missing. 3. Keep API surfaces minimal (PR #902) — only fields immediately needed, no speculative additions; API is hard to change once shipped. 4. Docs must match implementation, not aspiration (PRs #1035, #1056) — describe only what the code actually does; verify enforcement before documenting a contract. 5. TaskSpawner conventions (PRs #965, #974): - Prefer webhook-based triggers over poll-based. - {{.Branch}} is empty for issue-only events; use the {{if .Branch}}{{.Branch}}{{else}}main{{end}} fallback form. - issue_comment fires for both issues and PRs; design prompts to detect and handle both contexts. - Do not include manual PR branch checkout instructions — Kelos checks out the PR branch automatically. Also fixes the kelos-reviewer TaskSpawner branch field to use the safe fallback form (was using bare {{.Branch}}, which is empty for issue events; flagged P2 in PR #786 review). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Distills thirteen recurring conventions from recent PR review feedback and propagates them across AGENTS.md, the shared and worker AgentConfigs, and the reviewer prompts. All changes are backed by specific PR review findings. 1. Never use os.Getenv() for secrets as Go flag defaults — PR #971 P1 (cmd/kelos-webhook-server, cmd/ghproxy leaked GITHUB_TOKEN via --help). 2. Fail fast on invalid configuration — PR #971 multiple P1/P2 (silent degradation when GitHub App credentials or token resolvers failed). 3. Keep API surfaces minimal — PR #902 (maintainer asked repeatedly to defer speculative fields and ship the minimum viable API). 4. API changes must preserve backward compatibility — PR #1058 (BodyContains scalar->array would break in-cluster manifests; resolved by deprecating the old field rather than tightening validation). 5. Docs must match implementation, not aspiration — PRs #1035, #1056, #1082 (docs claimed enforcement that the code never performed). 6. TaskSpawner conventions — PRs #965, #974 (webhook over poll, empty .Branch for issue events, dual issue/PR context, no manual checkout). 7. Don't use Gomega's global Expect() inside Eventually polling blocks — PR #1084 (transient API errors short-circuited the poller). 8. /kind cleanup for self-development/-only PRs — PR #1100 P2 (agent classified by problem nature; rule is to classify by file location). 9. CLI error messages must name the resource — PR #1101 P2 (errors.New ("task failed") loses the name when batched). 10. Test the happy path, not only early-return guards — PR #1096 (both new tests covered only the no-op branches because production code lacked a posting seam; rule now requires adding the seam). 11. Avoid vacuous substring assertions in printer tests — PR #1106 P2 (a "sentry" check passed because the fixture's Name was "sentry-fixer"). 12. Keep CRD enum docstrings consistent with the Enum marker — PR #1081 P2 (commentOn godoc said "empty matches both" but the marker rejected ""). 13. Qualify cross-CRD field references with the owning kind in docs — PR #1077 P2 (Workspace section cited bare podOverrides.env without noting the field lives on Task.spec). Also: kelos-reviewer branch field uses the safe with-index fallback form, kelos-squash-commits gates the issue_comment trigger to PullRequest comments (preventing /kelos squash-commits typed on a plain issue from running git push --force-with-lease against an empty branch fallback), and the README's .Branch description matches runtime behavior.
Distills thirteen recurring conventions from recent PR review feedback and propagates them across AGENTS.md, the shared and worker AgentConfigs, and the reviewer prompts. All changes are backed by specific PR review findings. 1. Never use os.Getenv() for secrets as Go flag defaults — PR #971 P1 (cmd/kelos-webhook-server, cmd/ghproxy leaked GITHUB_TOKEN via --help). 2. Fail fast on invalid configuration — PR #971 multiple P1/P2 (silent degradation when GitHub App credentials or token resolvers failed). 3. Keep API surfaces minimal — PR #902 (maintainer asked repeatedly to defer speculative fields and ship the minimum viable API). 4. API changes must preserve backward compatibility — PR #1058 (BodyContains scalar->array would break in-cluster manifests; resolved by deprecating the old field rather than tightening validation). 5. Docs must match implementation, not aspiration — PRs #1035, #1056, #1082 (docs claimed enforcement that the code never performed). 6. TaskSpawner conventions — PRs #965, #974 (webhook over poll, empty .Branch for issue events, dual issue/PR context, no manual checkout). 7. Don't use Gomega's global Expect() inside Eventually polling blocks — PR #1084 (transient API errors short-circuited the poller). 8. /kind cleanup for self-development/-only PRs — PR #1100 P2 (agent classified by problem nature; rule is to classify by file location). 9. CLI error messages must name the resource — PR #1101 P2 (errors.New ("task failed") loses the name when batched). 10. Test the happy path, not only early-return guards — PR #1096 (both new tests covered only the no-op branches because production code lacked a posting seam; rule now requires adding the seam). 11. Avoid vacuous substring assertions in printer tests — PR #1106 P2 (a "sentry" check passed because the fixture's Name was "sentry-fixer"). 12. Keep CRD enum docstrings consistent with the Enum marker — PR #1081 P2 (commentOn godoc said "empty matches both" but the marker rejected ""). 13. Qualify cross-CRD field references with the owning kind in docs — PR #1077 P2 (Workspace section cited bare podOverrides.env without noting the field lives on Task.spec). Also: kelos-reviewer branch field uses the safe with-index fallback form, kelos-squash-commits gates the issue_comment trigger to PullRequest comments (preventing /kelos squash-commits typed on a plain issue from running git push --force-with-lease against an empty branch fallback), the kelos-squash-commits branch field uses the same with-index fallback form for defensive consistency with the documented convention, and the README's .Branch description matches runtime behavior (PR head branch or pushed branch; empty for issue events). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Propagates eleven recurring conventions, distilled from prior PR review feedback, across AGENTS.md, the shared and worker AgentConfigs, and the reviewer prompts. Also fixes two existing self-development spawner configurations that were flagged in adjacent reviews. Conventions added to AGENTS.md, agentconfig.yaml, and kelos-workers.yaml: 1. Never use os.Getenv() for secrets as Go flag defaults — PR #971 P1 (cmd/kelos-webhook-server, cmd/ghproxy leaked GITHUB_TOKEN via --help). 2. Fail fast on invalid configuration — PR #971 multiple P1/P2 (silent degradation when GitHub App credentials or token resolvers failed). 3. Keep API surfaces minimal — PR #902 (maintainer asked repeatedly to defer speculative fields and ship the minimum viable API). 4. API changes must preserve backward compatibility — PR #1058 (BodyContains scalar->array would break in-cluster manifests; resolved by deprecating the old field rather than tightening validation). 5. Docs must match implementation, not aspiration — PRs #1035, #1056, #1082 (docs claimed enforcement that the code never performed). 6. Don't use Gomega's global Expect() inside Eventually polling blocks — PR #1084 (transient API errors short-circuited the poller). 7. CLI error messages must name the resource — PR #1101 P2 (errors.New("task failed") loses the name when batched). 8. Test the happy path, not only early-return guards — PR #1096 (tests covered only the no-op branches because production code lacked a posting seam; rule now requires adding the seam). 9. Avoid vacuous substring assertions in printer tests — PR #1106 P2 (a "sentry" check passed because the fixture's Name was "sentry-fixer"). 10. Keep CRD enum docstrings consistent with the Enum marker — PR #1081 P2 (commentOn godoc said "empty matches both" but the marker rejected ""). 11. Qualify cross-CRD field references with the owning kind in docs — PR #1077 P2 (Workspace section cited bare podOverrides.env without noting the field lives on Task.spec). Reviewer/api-reviewer checklists gain matching items so kelos-reviewer and kelos-api-reviewer flag these patterns on future PRs. Other fixes in the same sweep: - kelos-reviewer.spec.taskTemplate.branch uses the safe '{{with index . "Branch"}}{{.}}{{else}}main{{end}}' fallback form so a /kelos review typed on a plain issue lands on main rather than an empty branch. - kelos-squash-commits gates the issue_comment trigger to commentOn: PullRequest and uses the same with-index fallback for the branch field — /kelos squash-commits typed on a plain issue would otherwise fire the spawner with empty {{.Branch}} and the prompt then runs git push --force-with-lease. - self-development/README.md template-variable table corrected from "Usually PR head branch" to "PR head branch or pushed branch; empty for issue events" to match runtime behavior, and the bodyContains example was updated to bodyPattern to match the spawner configs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
What type of PR is this?
/kind feature
What this PR does / why we need it:
Adds a generic webhook source type (
spec.when.webhook) to TaskSpawner that accepts arbitrary HTTP POST payloads with JSON bodies and maps them to task template variables via configurable JSONPath expressions. This enables integration with any webhook-capable system (Notion, Sentry, Drata, Salesforce, Aikido, etc.) without requiring Kelos-side code changes for each new integration.Key design decisions:
/webhook/<source>) and derives its HMAC secret from the<SOURCE>_WEBHOOK_SECRETenvironment variable (e.g.,/webhook/notion→NOTION_WEBHOOK_SECRET)fieldMappingmaps JSONPath expressions to template variables available inpromptTemplateandbranchkelos-webhook-serverwith--source genericrather than creating per-spawner DeploymentsworkspaceRefsince they may not be git-repo-drivenwebhookServer.sources.genericwithenvFrom-based multi-secret injection so all per-source secrets come from a single Secret resourceWhich issue(s) this PR is related to:
Fixes #687
Special notes for your reviewer:
envFromin the helm chart (notvalueFrom) since users will have multiple<SOURCE>_WEBHOOK_SECRETkeys in one SecretSignaturePrefixfield defaults to"sha256="but can be set to""for providers that send raw hex digests (e.g., Linear-style)/webhook/as a catch-all prefix for generic, which is placed after the more specific/webhook/githuband/webhook/linearruleshub.lumenfield.work/PaesslerAG/jsonpathdependency for JSONPath evaluationDoes this PR introduce a user-facing change?
Summary by cubic
Adds a generic
webhooksource to TaskSpawner to spawn tasks from any JSON POST at/webhook/<source>using JSONPath field mapping and AND-filters, with id-based idempotency whenfieldMapping.idis set (fallback to body‑hash). Implements #687.New Features
POST /webhook/<source>routes to the generic handler; delivery IDs usefieldMapping.idwhen present, else body‑hash.fieldMappingpopulates template vars (idrequired);ID,Title,Body,URLdefault to empty;.Payloadalways available.source.kelos-webhook-server --source=genericserves under/webhook/; adds generic Deployment/Service and/webhook/routes to ingress/gateway; controller treats generic as webhook‑driven; CRD validations added.github.com/PaesslerAG/jsonpath.Migration
webhookServer.sources.generic.enabled, optionally setwebhookServer.sources.generic.secretNamefor env injection.spec.when.webhook(source,fieldMappingwithid, optionalfilters), then send POSTs to/webhook/<source>.Written for commit f960432. Summary will update on new commits.