AI Agents

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

  1. A GitHub App installed on the target organization with webhook events enabled
  2. The ai-agents chart deployed (charts/ai-agents-main/ — API/operator + proxy)
  3. At least one AiModel available (auto-populated by model discovery, spec 0010) so agents have a backend to run on
  4. 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 fieldPurpose
metadata.nameUnique 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.creationTimestampWhen the key was created
data.keyThe 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:

FieldRequiredDescription
typeyesSource type. Currently only github.
credentialsRef.nameyesSecret with webhookSecret, appId, privateKey, installationId.
apiKeyRef.namenoSecret containing an API key for downstream integrators.
webhookHostnamenoHost the auto-provisioned Ingress should serve.
webhookIngress.classNamenoOverride the IngressClass for the provisioned Ingress.
webhookIngress.annotationsnoExtra annotations on the provisioned Ingress.
allowPublicReposnoIf true, accept events from public repos. Default: deny.
allowedPublicReposnoWhitelist of public-repo owner/names.
defaults.modelnoDefault model used when no other override applies.
defaults.effortnoDefault effort.
repoOverridesnoPer-repo model/effort overrides.
aiAgentsUrl, dataReponoReserved 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 EventTrigger ConditionJob TypeAgent
issuesLabel ai-{role} addedimplement-issueMatched role
issue_commentMention @ai-{role} on PRpr-feedbackMatched role
issue_commentMention @ai-{role} on issueissue-commentMatched role
issue_commentMention @agents-assemblepr-feedback or issue-commentALL enabled
pull_request_reviewChanges requestedaddress-reviewdeveloper
pull_request_review_commentMention @ai-{role}review-commentMatched role
workflow_runCompleted with failureevent:workflow_run.completedqwen-triage
check_runCompleted with failureevent:check_run.completedqwen-triage

Failure-only routing uses the trigger's filters.conclusion (see Event Trigger Filters).

Examples:

  • Add label ai-developer to an issue → triggers developer agent to implement it
  • Comment @ai-reviewer please review on a PR → triggers reviewer agent
  • 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:

  1. All global AiInstructions, sorted by priority ascending.
  2. Each local AiInstruction named in AiAgent.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 kubectl and 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 messageCauseFix
No WebhookSource CR of type 'github' foundNo WebhookSource CRCreate the CR (step 2)
Public repos not allowedPublic-repo filter blockedSet allowPublicRepos or add to allowedPublicRepos
No matching agentsNo agent's eventTriggers matchUpdate agents' triggers / mentions
Bot senderEvent from a [bot] accountExpected — 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:

ErrorCauseFix
No idle accountsAll accounts busy/paused/errorAdd more accounts or wait
Auth failureAccount credentials expiredRe-authenticate via terminal or update API key
Clone failureGitHub token invalidCheck credentialsRef secret values

Verify end-to-end

  1. Check pods are running:

    kubectl -n ai-agents get pods
    
  2. 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
    
  3. Check agents exist and are enabled:

    kubectl -n ai-agents get aiagents.labrats.work
    
  4. Check a model is available (idle) for agents to run on:

    kubectl -n ai-agents get aimodels.labrats.work
    
  5. Test by adding label ai-developer to a GitHub issue in an installed repo.