# Cirdan configuration. `cirdan setup` writes this file interactively; every key # is optional — defaults are safe, and env vars override secrets. project: payments-platform environment: production # Which setup pathway this install took (`cirdan setup`, first screen): # cli — "Use the CLI": runs with the tools already logged in on this machine — # inherited `gh` auth, a logged-in coding-agent CLI (Claude Code / Codex) # spawned headless inside the isolated workspace, local Docker. No stored # keys and no GitHub App required. # api — "Use the API": headless/managed — GitHub App auth + encrypted stored # provider keys. Derives security.profile=enterprise, which forbids # inherited gh auth and host-spawned CLI agents. # Legacy spellings (single-user/business) still load and migrate on the next save. deployment_mode: cli # The service registry: the single source of truth for what Cirdan can repair. # An alert resolves to an entry (via name, aliases, or match_labels), the entry # names the repo the fix PR targets, and `deploy` tells the isolated runner how # to rebuild the service to reproduce the failure. Readiness requires at least # one entry with a resolvable repo plus a passing routing proof (the Services # step's "Prove routing" panel, or: # cirdan preview-intake --source grafana --service checkout-api services: - name: checkout-api # lowercase, node-id-safe; what alerts resolve to repo: acme/checkout # "org/name" — the PR/repair target aliases: [checkout, checkout-svc] # extra alert-label names that resolve here deploy: kind: compose # compose | k8s | helm compose_service: checkout-api # exec/probe target; defaults to the entry name compose_files: [docker-compose.yml] # repo-relative (or source_root-anchored) # compose_project: acme-prod # PROD compose project name (isolation collision guard) deploy_workflow: deploy-checkout.yml # per-service override of source_hosts.github.deploy_workflow telemetry_service: checkout # Loki/PromQL service label; defaults to the name # source_root: ~/src/checkout # local checkout (cli installs only; api mode rejects it). # # When `repo` is unset, it derives from the checkout's # # GitHub git origin. # remote_url: https://github.com/acme/checkout.git # advanced clone-URL override (YAML-only) - name: payments-api repo: acme/payments match_labels: {app: payments, tier: backend} # exact-match alert labels (declaration order wins) deploy: kind: k8s namespace: payments # defaults to "default" kubernetes_kind: Deployment # defaults to Deployment workload: payments-api # defaults to the entry name manifest_files: [deploy/k8s.yaml] # repo-relative (kind: helm uses helm_chart_dirs instead) output: dir: cirdan-out storage: graph: sqlite # the single SQLite store (incidents, alerts, agent runs, audit state) # path: cirdan-out/cirdan.db # default telemetry: # prometheus_url: http://prometheus.internal:9090 # loki_url: http://loki.internal:3100 # logs backend (query_range, ns timestamps) # tempo_url: http://tempo.internal:3200 # traces backend (search, unix-second window) log_tail_lines: 200 error_window_seconds: 600 # Env-backed auth headers for telemetry backends (Grafana Cloud, etc.): map an HTTP # header NAME to the env var NAME holding its value — a secret never lives in YAML. # prometheus_headers_env: {Authorization: GRAFANA_CLOUD_TOKEN} # loki_headers_env: {Authorization: GRAFANA_CLOUD_TOKEN} # tempo_headers_env: {Authorization: GRAFANA_CLOUD_TOKEN} # Templated PromQL pulled into the per-alert telemetry snapshot. label -> PromQL with a # {service} placeholder (escaped at render; literal braces must be doubled, e.g. [5m]). # snapshot_promql: # error_rate: 'rate(http_requests_total{{service="{service}",code=~"5.."}}[1m])' ingest: # Inbound push ingestion. Run the daemon with --http, then external systems POST # to /v1/ingest/ (e.g. /v1/ingest/grafana) to wake the daemon immediately. enabled: true # Generic bearer auth for every source. Prefer the env var CIRDAN_INGEST_TOKEN. # token: changeme grafana: {} # all keys below are optional; {} keeps this a mapping (not null) # Grafana webhook contact point → POST /v1/ingest/grafana. # Optional HMAC over the raw body (prefer env CIRDAN_GRAFANA_HMAC): # hmac_secret: changeme # hmac_signature_header: X-Grafana-Alerting-Signature # hmac_timestamp_header: X-Grafana-Alerting-Timestamp # opt-in replay protection # hmac_max_age_seconds: 300 # Deferred: base URL + datasource UIDs for constructing Loki/Tempo Explore links. # url: https://grafana.internal:3000 # logs_datasource_uid: loki # traces_datasource_uid: tempo # Shared body cap for ALL /v1/ingest/* receivers (bytes), enforced before the # body is fully read → 413. # max_body_bytes: 1048576 alertmanager: # Native Prometheus Alertmanager webhook → POST /v1/ingest/alertmanager. # Grafana's payload is Alertmanager-compatible, so this one endpoint also serves # vmalert, Mimir/Cortex ruler, Thanos Ruler — anything with an "Alertmanager # webhook" receiver. Auth is the generic ingest bearer token above (no HMAC). enabled: true # acknowledged: true # set by setup when you choose Alertmanager as # # your source (completes the sources step) slack: {} # official Slack push ingestion (Events API / Socket Mode); off by default # enabled: true # mode: socket_mode # socket_mode (no public endpoint; needs the # # cirdan[slack] extra) | events_api (needs # # `cirdand serve --http` + a reverse proxy # # forwarding ONLY /v1/ingest/slack/events) # Secrets are env var NAMES or "stored:" refs — never values in YAML. # signing_secret_env: CIRDAN_SLACK_SIGNING_SECRET # events_api request signing # app_token_env: CIRDAN_SLACK_APP_TOKEN # socket_mode xapp- token (connections:write) # bot_token_env: CIRDAN_SLACK_BOT_TOKEN # channels:history/:read + chat:write # team_ids: [T0ABCDEF] # workspace allowlist — required when enabled # channel_ids: [C123456789] # channel allowlist — required when enabled; # # invite the bot to every listed channel # private_channels: false # also subscribe message.groups # allow_subtypes: [] # re-admit NON-bot subtypes (bot/self always dropped) # require_mention: false # max_message_chars: 4000 # max_payload_bytes: 65536 # oversized Slack deliveries are acked + audit-dropped # allowed_repos: [] # payload repos honored ONLY if listed ("owner/name" # # or "owner/*"); empty = never trust a payload repo # slack_workspace_domain: acme.slack.com # permalink fallback when no bot token # events_request_url: https://cirdan.example.com/v1/ingest/slack/events # # events_api only: the public request URL baked # # into the generated app manifest. MUST be https # # and non-loopback (Slack verifies it over SSL, # # server-to-server). # field_mapping: # extractor or fallback list per field; forms: # severity: ["label:severity", "label:sev"] # path:, label:, # service: ["label:service"] # regex:, literal: # repo: ["label:repo"] # summary: "event.text" # environment: ["label:env", "label:environment"] # slack_thread_ts: ["event.thread_ts", "event.ts"] # severity_mapping: {sev1: critical, sev2: high, sev3: warning, sev4: low, # critical: critical, high: high, medium: warning, low: low} # default_severity: warning # unknown severity -> this, never fail-open upward # on_missing_field: triage_only # triage_only | reject slack_issue_intake: {} # pre-normalized JSON intake (TeserDone-style); off by default # Endpoint is FIXED: POST /v1/ingest/slack_issue_intake (bearer + optional HMAC). # Requires ingest.enabled above. # enabled: true # source: teserdone # source_provider LABEL — not a URL segment # channel_ids: [C123456789] # allowed_repos: [] # hmac_secret_env: CIRDAN_TESERDONE_HMAC # optional HMAC atop the bearer token # hmac_signature_header: X-Cirdan-Signature # sha256=HMAC-SHA256(secret, raw_body) # hmac_timestamp_header: X-Cirdan-Timestamp # hmac_max_age_seconds: 300 # field_mapping: {severity: severity, service: service, repo: repo, summary: text, # environment: environment, github_issue_url: github_issue_url, # slack_thread_ts: slack_thread_ts} # severity_mapping / default_severity / on_missing_field: same as ingest.slack # (on this route, on_missing_field: reject MAY return HTTP 400 to the caller) # Route alerts to a pipeline depth (every source flows through this policy; # first matching rule wins). Pipelines: # full_remediation — replicate + reproduce + fix + verified PR (+ deliver-mode ship/deploy) # reproduce_and_pr_only — replicate + reproduce + fix + verified PR; never merge/deploy # lightweight_pr_only — isolated clone only, no repro; [lightweight] PR; never merge/deploy # triage_only — brief + explain + notify; zero code changes # Default when no rule matches: an alert that resolves to a REGISTERED service # with a repairable repo routes to reproduce_and_pr_only — at qualifying severity # only (responder.severities, default high/critical; lower severities downgrade # to triage_only unless a rule opts them in). Unresolved/unregistered alerts fail # safe to triage_only. Only full_remediation can ever reach auto-merge/deploy, # and still only under remediation.mode=deliver + auto_merge/auto_deploy. # Code-changing pipelines require mode develop/deliver/unrestricted — otherwise # they downgrade to triage_only (audited). routing_policy: {} # rules: # - when: {source: slack, severity: critical} # pipeline: full_remediation # - when: {severity: warning, service: checkout-api} # pipeline: reproduce_and_pr_only # - when: {source: slack, severity: low} # pipeline: lightweight_pr_only # `when` fields: source, source_provider, severity, service, repo, environment # (string or OR-list) + labels (exact-match map). notifications: # Outbound lifecycle notifications (alert -> PR -> CI -> deploy -> recovery trail). slack: {} # enabled: true # webhook_url_env: CIRDAN_SLACK_WEBHOOK # env NAME — the webhook URL is a secret # events: default = the full lifecycle (alert opened -> PR -> CI checks -> # deploy -> delivered / manual_required -> resolved); narrow to taste: # events: [opened, pr_opened, ci_checks_failed, delivered, manual_required] # Bot-token mode (one root card per incident, updated in place + threaded replies; # falls back to ingest.slack's bot token, then to the webhook broadcast): # bot_token_env: CIRDAN_SLACK_BOT_TOKEN # channel: "#incidents" # default channel for non-Slack-originated incidents # channel_by_severity: {critical: "#incidents-critical", high: "#incidents"} # severities: [high, critical] # unset -> inherit responder.severities; # # routed (intake) incidents always notify # PR-checks watcher: post ci_checks_passed/failed/timeout for open Cirdan PRs. # Read-only; the loop runs only when a notification channel is enabled. pr_checks: true responder: enabled: true # incidents always get an on-disk brief + hooks # command is what turns briefs into automatic agent response; normally set by # `cirdan install --project`. Placeholders: {brief_file} {incident_id} {title} # command: 'claude -p "Respond to the Cirdan incident brief at {brief_file}"' # notify_command: './scripts/page-me.sh {incident_id}' # webhook_url posts RAW incident JSON ({"transition": ..., "incident": ...}) — it does # NOT render in Slack. For Slack cards use notifications.slack above instead. # webhook_url: https://internal-hook.example/cirdan # severities is the auto-repair threshold: the registered-service routing default # applies only at these severities (lower ones need an explicit routing rule), # and non-alert incidents (error clusters) qualify for response by it too. severities: [high, critical] cooldown_seconds: 600 # per incident condition timeout_seconds: 900 max_concurrent: 1 # Telemetry snapshot: embed a ±window view of logs/metrics/traces around the alert # timestamp into the agent brief (uses telemetry.{loki,tempo,prometheus,snapshot_promql}). snapshot_enabled: true snapshot_window_minutes: 2 review: enabled: true # read-only console at /review on cirdand --http capture_agent_io: redacted # v1 stores only redacted/capped traces max_step_chars: 8000 max_output_tail_chars: 4000 retention_days: 30 daemon: always_on: true mcp: false api: false api_writes_enabled: false # operational HTTP write routes off by default host: 127.0.0.1 port: 8090 incident_interval: 15 # seconds access_interval: 300 delivery_interval: 30 # merge-watch→deploy→prod-verify (auto_deploy only) slack_inbox_interval: 5 # Slack durable-inbox drain fallback cadence (ingest.slack only) pr_checks_interval: 120 # PR-checks watcher (notifications.pr_checks + a channel) github_comments_interval: 60 # @cirdan PR comment poller (comments_enabled only) # --- Integrations + remediation (configure interactively via `cirdan setup`) --- remediation: mode: develop # diagnose|recover|operate|hotpatch|develop|deliver|unrestricted # develop (default) = durable fix: isolated sandbox → reproduce → fix → verified PR. # deliver = develop PLUS optional merge/deploy/prod-verify, gated by # source_hosts.github.auto_merge / auto_deploy below. A failed # production verification stops at manual_required with evidence. isolation_prefix: cirdan-repair # The agent that drives develop runs. Unset = falls back to agent_providers.default. # May name an anthropic-api/openai-api/ollama-api provider, or — outside the # enterprise profile (i.e. "Use the CLI" installs) — a logged-in CLI type # (claude-cli / codex-cli), spawned headless inside the isolated workspace. # develop_provider: claude-cli # Per-phase bound (seconds) on each blocking offload (baseline probe, agent run, post-fix # probe). A stuck/never-resuming offload becomes an audited, abandoned attempt (no PR/deploy) # instead of hanging the responder. NOTE: this releases the responder; it does not kill the # worker thread (see max_inflight_workers). # step_timeout_seconds: 1800 # Responder thread-pool size = the leak budget (a timed-out worker holds its slot until it # finishes). Unset -> max(4, max_concurrent + 2). When full, new develop attempts go # manual_required instead of wedging. # max_inflight_workers: 4 # Worst-case event-loop block during synchronous workspace creation: the git clone / # origin-retarget timeout (seconds). Other short git commands keep their own defaults. # workspace_clone_timeout: 120 # Separate budget for improving an invalid original-revision regression proof. # This never consumes max_repair_attempts. Bounded to 0..3. resolution_attempts: 1 verification: # Run the pinned repository suite on the pristine revision before the fixer, then # compare it with the post-fix run. "advisory" allows a historically red suite when # no new failure is confirmed; "off" preserves raw post-fix test behavior only. # "required" blocks when trustworthy baseline evidence is unavailable. pre_fix_tests: advisory # Run changed tests against the original revision and fixed candidate. "advisory" # records proof and may use the resolution budget to improve an invalid test. # "balanced" blocks an invalid test; "required" also blocks absent, # inconclusive, or unavailable proof. regression_proof: advisory # Findings-only semantic reviewers are opt-in because they add model cost and # latency. "required" blocks unresolved blocking findings and unavailable or # invalid reviews. An unset provider follows develop_provider, then the default. specialists: enabled: false policy: advisory # provider: anthropic-api kinds: [tests, persistence, contracts, configuration, deployment, documentation] max_parallel: 2 # bounded to 1..4 timeout_seconds: 600 # per reviewer; bounded to 30 minutes # API providers are dispatch-enforced read-only. In the non-enterprise CLI # posture a logged-in CLI may run against a disposable copy: edits are # discarded, but this cwd isolation is not an OS security boundary and the # process inherits the host access available to the existing CLI posture. agent_providers: default: claude-cli # a providers key, or a bare provider type # "Use the CLI": a logged-in claude-cli/codex-cli drives develop runs inside the # workspace. "Use the API" (enterprise) requires an API/Ollama provider instead. # API keys are env-only: name the env var (or a "stored:" ref), never the value. # providers: # anthropic-api: {type: anthropic-api, api_key_env: ANTHROPIC_API_KEY, model: claude-opus-4-8, effort: high} # openai-api: {type: openai-api, api_key_env: OPENAI_API_KEY} # # Local model for air-gapped/on-prem via Ollama's OpenAI-compatible endpoint. No API # # key; `model` is required (must be pulled locally). Tool-calling is model-dependent. # ollama-api: {type: ollama-api, base_url: http://localhost:11434/v1, model: qwen2.5} source_hosts: github: # none | github-app | inherited-gh. # github-app — official App auth: REST PR creation, short-lived installation # tokens, no host `gh` dependency (the "Use the API" pathway; # needs the `cirdan[github-app]` extra for JWT signing). # inherited-gh — inherited `gh` CLI login, no stored secret (the "Use the # CLI" pathway default). # none — diff-only, no PR writes (readiness reports not ready). mode: inherited-gh default_base_branch: main # require_verified_pr (default true): the core safety gate — a PR opens only when the # incident was reproduced (baseline_failed) AND the fix verified (postfix passed). Set # false to opt out in DEVELOP mode only (caveated, human-reviewed PRs for unverified # fixes); deliver mode is always hard-gated regardless. # require_verified_pr: true # repo_map: {checkout-api: org/checkout} # (deprecated — use services:) still honored # --- github-app auth (no secret in YAML: IDs + a key *path* or env *name*) --- # app_id: "123456" # client_id: Iv23li... # optional; GitHub's recommended JWT issuer (used over app_id) # installation_id: "789012" # private_key_path: ~/.cirdan/app.private-key.pem # OR: # private_key_env: CIRDAN_GH_APP_KEY # env var NAME holding the PEM # GitHub Enterprise Server (defaults are public GitHub): # api_base_url: https://ghe.example.com/api/v3 # web_base_url: https://ghe.example.com # --- delivery (remediation.mode=deliver) --- # auto_merge enables GitHub-NATIVE auto-merge on Cirdan PRs (branch protection / # reviewers still gate the merge; Cirdan never force-merges). With auto_merge on but # auto_deploy off, a merge-to-main may still deploy via YOUR CI — Cirdan will not # prod-verify it. # auto_merge: false # merge_method: squash # squash | merge | rebase # require_checks gates the deploy dispatch on the MERGE COMMIT's CI being green # (block-only: it can stop a deploy, never start one; only reachable in the # auto_deploy delivery loop). true = all checks; a list = only the named checks; # false restores the pre-gate behavior. github-app mode needs checks:read. # require_checks: true # checks_timeout_seconds: 1800 # checks still pending after this -> manual_required # require_tests blocks the PR unless the sandboxed repo test suite PASSED # (an 'unavailable' run - no sandbox image - blocks too, never fakes a pass). # require_tests: false # @cirdan PR comment commands (Stage 2): poll open Cirdan PRs for `@cirdan ` # (investigate | explain risk | reproduce | fix | add tests | rerun pipeline), # authorize the author (live collaborator permission), mode-check, act, and reply. # A comment can never escalate past remediation.mode; fork PRs allow read-only verbs # only. DIAGNOSE verbs (investigate/explain risk) run today; DEVELOP/DEPLOY verbs need # review_loop_enabled (Stage 2C). # comments_enabled: false # comment_min_permission: write # write | admin # comment_cooldown_seconds: 300 # per (repo, pr, verb) # Review loop (Stage 2C): @cirdan fix/add-tests/reproduce (with the feedback as the # argument) drives a bounded fix iteration that pushes to the SAME PR branch, reruns # tests, and replies. Comment-triggered runs are capped at develop mode; never deliver. # review_loop_enabled: false # review_max_iterations: 3 # review_cooldown_seconds: 300 # create_check_run: false # publish/update cirdan/verification on the PR head # # (needs checks:write — a manifest permission change) # GitHub webhook receiver (Stage 3): a latency accelerator over the pollers. A # signature-verified event WAKES the relevant poller (which re-fetches + re-authorizes # from the API); it never mutates state or trusts the payload. The daemon stays # loopback-bound — forward ONLY POST /v1/ingest/github through a reverse proxy. # webhook_enabled: false # webhook_secret_env: CIRDAN_GITHUB_WEBHOOK # env NAME (or webhook_secret_ref: stored:...) # webhook_public_url: https://cirdan.example.com # PUBLIC base a reverse proxy forwards # # /v1/ingest/github from (server-to-server; NOT loopback). # # Required for a setup-created App to auto-subscribe. # auto_deploy is the master switch: only then does Cirdan dispatch the deploy # workflow after merge and re-verify recovery in prod. A failed production # verification stops the delivery at manual_required, with the failure # evidence notified and audited. # auto_deploy: false # deploy_workflow: deploy.yml # global default; a services[].deploy_workflow # # overrides it per service. Unset -> inferred only # # if exactly one deploy-hinted workflow declares # # workflow_dispatch; else delivery is manual_required. # CONTRACT for deploy_workflow: Cirdan dispatches it with a `cirdan_incident_id` # input and correlates the run by an EXACT token in the run name. Declare the # input and echo it as a `key=value` token (not bare text, so inc-1 can't # match inc-10): # on: { workflow_dispatch: { inputs: { cirdan_incident_id: {}, target_sha: {} } } } # run-name: deploy cirdan_incident_id=${{ inputs.cirdan_incident_id }} target_sha=${{ inputs.target_sha }} # Governance profile. `enterprise` (derived automatically by deployment_mode: api) # hard-fails configs that use inherited host `gh` auth, remediation.mode=unrestricted, # host-spawned command hooks, or a CLI agent provider for automation — the governed # path is github-app + API/on-prem providers. security: profile: standard # standard | enterprise runners: local: {enabled: true} compose: enabled: true # sandbox_image: python:3.12-slim # toolchain image for the develop sandbox allow_egress: false # repair stacks are internal/no-egress unless opted in # sandbox_network: false # allow network inside sandbox_exec (dependency installs) # trusted_host_commands: [] # NON-isolated host fallback (exact argv lists; off by default) # Incident-reproduction probe for develop verification (runs INSIDE the isolated # service container — no host ports / egress needed). Precedence: a per-alert Grafana # annotation `cirdan_probe_cmd` (e.g. "curl -fsS http://localhost:8080/healthz") # overrides this default; with neither, a probe may be derived from an HTTP hint in # the alert (derive_probe_from_alert), else verification falls back to stack health. # probe_cmd: ["curl", "-fsS", "http://localhost:8080/healthz"] # probe_service: api # service to probe; else the affected service's # # registry deploy target # derive_probe_from_alert: true # set false to require an explicit probe # Repo test suite run inside sandbox_exec after the post-fix probe. Empty argv -> # framework detection (pytest / npm test / go test / make test). # test_cmd: [] # test_timeout: 600 # seconds; bounded — sandbox_exec force-removes on expiry k8s: # Reproduce k8s workloads in a throwaway namespace (manifests/Helm) for develop-mode # verification. Off by default — needs cluster write access (verified via the full # `kubectl auth can-i` matrix). Selected over compose only when the affected service's # deploy kind is k8s/helm and no compose file is present. enabled: false allow_egress: false # default-deny NetworkPolicy + imagePullPolicy Never unless opted in cpu_limit: "2" # LimitRange per-container default cpu cap memory_limit: 2Gi # LimitRange per-container default memory cap pod_limit: 10 # ResourceQuota pod cap tmp_size_limit: 64Mi # size-limited emptyDir /tmp (readOnlyRootFilesystem scratch) # probe_cmd: ["curl", "-fsS", "http://localhost:8080/healthz"] # probe_service: api # workload to exec the probe in; else the registry deploy target # helm_values: [values.repair.yaml] # extra -f values; workspace-relative only (preflighted)