GitHub Integration
How GitHub events become InferenceRequest CRs. Webhook handling is built in — there is no separate github-ai-agents middleware in the current architecture.
Overview
GitHub App ──webhook──► Webhook pod (per WebhookSource)
│
▼ (HMAC verify, normalize, agent match)
InferenceRequest CR in `ai-agents`
│
▼
Operator → executor Job
Each WebhookSource CR provisions its own webhook Deployment + Service + Ingress. The webhook pod runs src/webhook.ts and routes incoming GitHub events through src/sources/github/.
Prerequisites
- A GitHub App installed on the target organization with webhook events enabled
- The ai-agents chart deployed (
charts/ai-agents-main/— API/operator + proxy) - At least one AiModel available (auto-populated by model discovery, spec 0010) so agents have a backend to run on
- At least one AiAgent CRD created (agents define the roles that handle events)
Setup
1. Create an API Key
The github-ai-agents service authenticates to ai-agents using a Bearer API key.
Via dashboard:
Navigate to https://ai-agents.hcl.labrats.work → API Keys → Generate.
Via API:
curl -X POST https://ai-agents.hcl.labrats.work/api/api-keys \
-H "Content-Type: application/json" \
-d '{"name": "GitHub Webhook Handler"}'
Response (201):
{
"id": "api-key-c03a6d64",
"name": "GitHub Webhook Handler",
"key": "sk-abc123..."
}
The full key is only shown once — save it securely.
How API keys are stored:
Each API key is a K8s Secret in the ai-agents namespace with label labrats.work/type=ai-api-key. The ApiKeyWatcher informer watches these Secrets and keeps an in-memory index for fast validation.
| Secret field | Purpose |
|---|---|
metadata.name | Unique ID (e.g., api-key-c03a6d64) |
metadata.labels["labrats.work/type"] | Always ai-api-key — used by label selector |
metadata.annotations["labrats.work/api-key-name"] | Display name |
metadata.creationTimestamp | When the key was created |
data.key | The hashed/encoded key value |
Listing keys:
# Via API (shows prefix only)
curl https://ai-agents.hcl.labrats.work/api/api-keys
# Via kubectl (shows metadata)
kubectl -n ai-agents get secrets -l labrats.work/type=ai-api-key \
-o custom-columns='NAME:.metadata.name,DISPLAY:.metadata.annotations.labrats\.work/api-key-name,CREATED:.metadata.creationTimestamp'
Revoking a key:
# Via API
curl -X DELETE https://ai-agents.hcl.labrats.work/api/api-keys/api-key-c03a6d64
# Via kubectl
kubectl -n ai-agents delete secret api-key-c03a6d64
Revocation is immediate — the informer detects the Secret deletion and removes the key from the in-memory index. Any subsequent requests using that key will receive 403 Forbidden.
2. Create the WebhookSource CRD
The WebhookSource CR is the central configuration for the integration. Creating it triggers WebhookSourceWatcher to provision a Deployment + Service + Ingress for the webhook receiver.
apiVersion: labrats.work/v1alpha1
kind: WebhookSource
metadata:
name: github
namespace: ai-agents
spec:
type: github
credentialsRef:
name: github-app-credentials # Secret with GitHub App creds + webhookSecret
apiKeyRef:
name: ai-agents-api-key # Secret consumed by external integrators
webhookHostname: github.ai-agents.hcl.labrats.work
webhookIngress:
className: traefik
annotations: {}
allowPublicRepos: false
allowedPublicRepos:
- labrats-work/apps.public-demo
defaults:
model: sonnet
effort: medium
repoOverrides:
- repo: labrats-work/apps.my-diet
model: opus
effort: high
Spec fields:
| Field | Required | Description |
|---|---|---|
type | yes | Source type. Currently only github. |
credentialsRef.name | yes | Secret with webhookSecret, appId, privateKey, installationId. |
apiKeyRef.name | no | Secret containing an API key for downstream integrators. |
webhookHostname | no | Host the auto-provisioned Ingress should serve. |
webhookIngress.className | no | Override the IngressClass for the provisioned Ingress. |
webhookIngress.annotations | no | Extra annotations on the provisioned Ingress. |
allowPublicRepos | no | If true, accept events from public repos. Default: deny. |
allowedPublicRepos | no | Whitelist of public-repo owner/names. |
defaults.model | no | Default model used when no other override applies. |
defaults.effort | no | Default effort. |
repoOverrides | no | Per-repo model/effort overrides. |
aiAgentsUrl, dataRepo | no | Reserved for external integrators; not used by the built-in handler. |
3. Create the Credentials Secrets
GitHub App credentials:
apiVersion: v1
kind: Secret
metadata:
name: github-app-credentials
namespace: ai-agents
stringData:
webhookSecret: "whsec_..." # GitHub webhook HMAC secret
appId: "123456" # GitHub App ID
privateKey: | # GitHub App private key (PEM)
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
installationId: "789012" # GitHub App installation ID
API key for external integrators:
apiVersion: v1
kind: Secret
metadata:
name: ai-agents-api-key
namespace: ai-agents
stringData:
key: "sk-abc123..." # The API key from step 1
In production, encrypt these with SOPS before committing to the Flux repo.
4. Register Agent Roles
Agent roles are AiAgent CRDs in the ai-agents namespace. They define the named agents that can be triggered by GitHub events.
apiVersion: labrats.work/v1alpha1
kind: AiAgent
metadata:
name: developer
namespace: ai-agents
spec:
displayName: Developer
provider: claude
defaultModel: sonnet
defaultEffort: medium
enabled: true
The default agents are seeded on startup. View them:
kubectl -n ai-agents get aiagents.labrats.work
Supported Events
| GitHub Event | Trigger Condition | Job Type | Agent |
|---|---|---|---|
issues | Label ai-{role} added | implement-issue | Matched role |
issue_comment | Mention @ai-{role} on PR | pr-feedback | Matched role |
issue_comment | Mention @ai-{role} on issue | issue-comment | Matched role |
issue_comment | Mention @agents-assemble | pr-feedback or issue-comment | ALL enabled |
pull_request_review | Changes requested | address-review | developer |
pull_request_review_comment | Mention @ai-{role} | review-comment | Matched role |
workflow_run | Completed with failure | event:workflow_run.completed | qwen-triage |
check_run | Completed with failure | event:check_run.completed | qwen-triage |
Failure-only routing uses the trigger's filters.conclusion (see Event Trigger Filters).
Examples:
- Add label
ai-developerto an issue → triggersdeveloperagent to implement it - Comment
@ai-reviewer please reviewon a PR → triggersrevieweragent - Comment
@agents-assemble→ triggers ALL enabled agents in parallel
Event Trigger Filters
An eventTrigger may carry a filters object that further gates whether the
event fires the agent — evaluated by the GitHub webhook matcher before a job
is created, so filtered-out events cost nothing. This is pure CR config; no code
change is needed to add a filter to any agent.
Currently supported filter key:
conclusion— a string or list of strings, matched against the conclusion of completion events (workflow_run,check_run,check_suite).
eventTriggers:
- source: github
events:
- workflow_run.completed
- check_run.completed
filters:
conclusion: failure # only fire on failed runs; ignore success
enabled: true
A trigger with no filters fires on every matching event. A conclusion filter
on an event that carries no conclusion (e.g. issues.opened) never matches.
Instruction Injection
Instructions live in-cluster as AiInstruction CRDs and are resolved at job time by the operator's prompt builder. There is no external data repo. See Architecture › Operator › Prompt Builder.
Each agent receives, in order:
- All
globalAiInstructions, sorted bypriorityascending. - Each
localAiInstructionnamed inAiAgent.spec.instructions[].
Both are wrapped as <instruction name="…" scope="…">…</instruction> blocks and prepended to the prompt.
Public Repo Filtering
Public repositories are blocked by default. To enable a specific public repo, set:
spec:
allowPublicRepos: false
allowedPublicRepos:
- owner/repo-name
Or set allowPublicRepos: true to allow all public repos. Filtered events return 200 with { "matched": 0, "reason": "..." }.
Per-Repo Configuration
Override model and effort for specific repositories via the repoOverrides field in the WebhookSource CR:
spec:
repoOverrides:
- repo: labrats-work/apps.my-diet
model: opus
effort: high
- repo: labrats-work/apps.ai-agents
model: sonnet
effort: low
Per-job config resolution (see Architecture › Config Defaults Resolution) is independent of WebhookSource overrides. The webhook handler uses repoOverrides to populate IR.spec.config at IR creation time.
Managing API Keys
API keys control who can submit jobs to the ai-agents engine. Both env-based keys and CRD-managed keys are supported.
Key Lifecycle
Generate (POST /api/api-keys)
│
▼
Active ── K8s Secret exists with label labrats.work/type=ai-api-key
│ metadata: name, displayName, createdAt
│
▼
Revoke (DELETE /api/api-keys/:id)
│
▼
Deleted ── Secret removed, key immediately invalid
Env-Based Keys (Fast Path)
For static keys that don't need CRD management, set the SUBMIT_API_KEYS environment variable:
env:
- name: SUBMIT_API_KEYS
valueFrom:
secretKeyRef:
name: ai-agents-secret
key: SUBMIT_API_KEYS
These are checked first (array lookup) before CRD-managed keys.
CRD-Managed Keys (Dashboard)
Keys created via the API or dashboard are stored as labeled K8s Secrets and watched by the ApiKeyWatcher informer. This means:
- Keys can be created and revoked without restarting the pod
- The informer detects changes within seconds
- Key metadata (name, creation time) is visible via
kubectland the API
Validation Flow
Request arrives with Authorization: Bearer <token>
│
├─► Check env var keys (SUBMIT_API_KEYS) ─── match? ─── allow
│
├─► Check CRD-managed keys (ApiKeyWatcher) ── match? ─── allow
│
└─► No match ── 403 Forbidden
If no keys are configured and no watcher is active (local dev), auth is skipped entirely.
Troubleshooting
Webhooks received but skipped
Check logs on the per-source webhook pod (auto-provisioned per WebhookSource):
kubectl -n ai-agents logs deployment/ai-agents-webhook-github --tail=50
Common skip reasons:
| Log message | Cause | Fix |
|---|---|---|
No WebhookSource CR of type 'github' found | No WebhookSource CR | Create the CR (step 2) |
Public repos not allowed | Public-repo filter blocked | Set allowPublicRepos or add to allowedPublicRepos |
No matching agents | No agent's eventTriggers match | Update agents' triggers / mentions |
Bot sender | Event from a [bot] account | Expected — prevents loops |
Jobs submitted but failing
# Check recent jobs
curl https://ai-agents.hcl.labrats.work/api/jobs?limit=5
# Check specific job logs
curl https://ai-agents.hcl.labrats.work/api/jobs/<id>
Common failures:
| Error | Cause | Fix |
|---|---|---|
No idle accounts | All accounts busy/paused/error | Add more accounts or wait |
| Auth failure | Account credentials expired | Re-authenticate via terminal or update API key |
| Clone failure | GitHub token invalid | Check credentialsRef secret values |
Verify end-to-end
-
Check pods are running:
kubectl -n ai-agents get pods -
Check the WebhookSource and its provisioned Deployment:
kubectl -n ai-agents get webhooksources.labrats.work kubectl -n ai-agents get deploy -l app.kubernetes.io/component=webhook -
Check agents exist and are enabled:
kubectl -n ai-agents get aiagents.labrats.work -
Check a model is available (idle) for agents to run on:
kubectl -n ai-agents get aimodels.labrats.work -
Test by adding label
ai-developerto a GitHub issue in an installed repo.