Skip to content

Commit 53d47e7

Browse files
authored
feat: add bifrost.alerting to Helm chart with declarative channels and CEL-based rules (#5079)
## Summary Adds `bifrost.alerting` as a new top-level Helm values key, enabling declarative configuration of alert channels and rules that are seeded into the Bifrost config store at startup. This allows operators to define alerting behavior entirely through Helm without manual UI configuration. ## Changes - Added `bifrost.alerting` to `values.yaml` with commented examples covering all supported channel types (Slack, Microsoft Teams, PagerDuty, generic webhook) and CEL-expression-based rules. - Added the full JSON schema for `bifrost.alerting` in `values.schema.json`, covering `history_retention_days`, `webhook_network` (`allow_http`, `allow_private_network`), `channels[]`, and `rules[]` with required field validation and enum constraints. - Wired `bifrost.alerting` into the Helm template (`_helpers.tpl`) so it renders directly as the top-level `alerting` key in the generated config. - Documented the addition in the `helm-v2.1.28` changelog and `README.md`. Alert rules use CEL expressions evaluated against governance usage variables (e.g., `budget_usage_percent`, `budget_spent`, `request_usage`) and support scoping to `virtual_key`, `team`, or `customer`. Rules can optionally target a specific budget via `target_type`/`target_id` and support per-rule `cooldown_seconds` and `notify_once_per_reset_cycle` controls. ## Type of change - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [ ] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [x] Docs ## How to test Deploy the Helm chart with an `alerting` block in your `values.yaml` and verify the rendered config contains the expected `alerting` top-level key: ```sh helm template bifrost ./helm-charts/bifrost -f your-values.yaml | grep -A 50 '"alerting"' ``` Example minimal `values.yaml` snippet to test: ```yaml bifrost: alerting: history_retention_days: 365 channels: - id: "slack-ops" name: "Ops Slack" type: slack enabled: true config: webhook_url: "env.SLACK_WEBHOOK_URL" rules: - id: "budget-80pct" name: "Budget 80% Alert" enabled: true scope_type: virtual_key scope_id: "vk-1" cel_expression: "budget_usage_percent >= 80" channel_ids: ["slack-ops"] ``` Validate schema enforcement by providing an invalid `type` value or omitting a required field and confirming `helm lint` rejects it. ## Screenshots/Recordings N/A ## Breaking changes - [ ] Yes - [x] No ## Related issues N/A ## Security considerations - `webhook_network.allow_http` and `allow_private_network` default to `false`, preventing accidental exposure of internal services or plaintext webhook traffic. Operators must explicitly opt in to relax these restrictions. - Channel configs support `env.*` references for secrets (e.g., `routing_key`, `webhook_url`), avoiding hardcoded credentials in `values.yaml`. ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [ ] I added/updated tests where appropriate - [x] I updated documentation where needed - [ ] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
1 parent abed892 commit 53d47e7

8 files changed

Lines changed: 355 additions & 21 deletions

File tree

docs/changelogs/helm-v2.1.27.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: "v2.1.27"
3+
description: "Helm v2.1.27 changelog - 2026-07-09"
4+
---
5+
6+
<Update label="Bifrost Helm" description="v2.1.27">
7+
8+
## Changelog
9+
10+
- `bifrost.schemaUrl` — override the generated `config.json` `$schema` location for isolated or air-gapped deployments. Accepts an HTTP(S) URL, `file://` URL, or filesystem path. When set, the value is also exported as `BIFROST_SCHEMA_URL` in the pod; when empty (default), the env var is not injected and the public schema URL is used. Renders into `$schema`.
11+
12+
</Update>

docs/changelogs/helm-v2.1.28.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ description: "Helm v2.1.28 changelog - 2026-07-10"
1212
- `calendar_aligned` on `bifrost.accessProfiles[*]` (top-level) — snaps all budget and rate-limit reset windows to calendar boundaries for the profile. Passes through into `access_profiles[*].calendar_aligned`.
1313
- `calendar_aligned` on `bifrost.accessProfiles[*].budgets[*]` and `bifrost.accessProfiles[*].provider_configs[*].budgets[*]` — schema previously blocked this field; now matches parity with `governance.budgets[*].calendar_aligned`.
1414
- `calendar_aligned` on `bifrost.governance.virtualKeys[*]` — was accepted by schema but not rendered into config. Now correctly emits `virtual_keys[*].calendar_aligned` in the generated config.
15-
- `allowOnAllVirtualKeys` on `bifrost.mcp.clientConfigs[*]` — grants this MCP server access to all virtual keys without per-key assignment. Already wired in the template; now documented in `values.yaml`. Renders into `mcp.client_configs[*].allow_on_all_virtual_keys`.
15+
- `bifrost.alerting` for declarative alert channels and rules. Supports `history_retention_days`, `webhook_network` (`allow_http`, `allow_private_network`), `channels[]` (slack, microsoft_teams, pagerduty, webhook), and `rules[]` (CEL-expression-based, governance-scope-aware). Renders into `alerting`.
1616

1717
</Update>

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,7 @@
949949
"icon": "box",
950950
"pages": [
951951
"changelogs/helm-v2.1.28",
952+
"changelogs/helm-v2.1.27",
952953
"changelogs/helm-v2.1.26",
953954
"changelogs/helm-v2.1.25",
954955
"changelogs/helm-v2.1.24",

helm-charts/bifrost/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ Official Helm charts for deploying [Bifrost](https://github.com/maximhq/bifrost)
1515
- Added `calendar_aligned` to `bifrost.accessProfiles[*]` (top-level on each profile). Snaps all budget and rate-limit reset windows to calendar boundaries for the profile. Passes through directly into `access_profiles[*].calendar_aligned`.
1616
- Added `calendar_aligned` to `bifrost.accessProfiles[*].budgets[*]` and `bifrost.accessProfiles[*].provider_configs[*].budgets[*]`. Schema previously blocked this field via `additionalProperties: false`; now parity with `governance.budgets[*].calendar_aligned`.
1717
- Added `calendar_aligned` rendering for `bifrost.governance.virtualKeys[*].calendar_aligned`. Was in schema but not rendered into config. Now emits `virtual_keys[*].calendar_aligned` in the generated config.
18-
- Added `allowOnAllVirtualKeys` to the `bifrost.mcp.clientConfigs[*]` example documentation. Field was already wired in the template; now visible in `values.yaml`. Renders into `mcp.client_configs[*].allow_on_all_virtual_keys`.
1918
- Documented `calendar_aligned` in the `bifrost.governance.budgets[*]` example. Was already schema-supported; now shown in the `values.yaml` commented example.
19+
- Added `bifrost.alerting` for declarative alert channels and rules. Supports `history_retention_days`, `webhook_network` (`allow_http`, `allow_private_network`), `channels[]` (slack, microsoft_teams, pagerduty, webhook), and `rules[]` (CEL-expression-based, governance-scope-aware). Renders into `alerting`.
2020

2121
### 2.1.27
2222

helm-charts/bifrost/templates/_helpers.tpl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,20 @@ false
743743
{{- if .Values.bifrost.accessProfiles }}
744744
{{- $_ := set $config "access_profiles" .Values.bifrost.accessProfiles }}
745745
{{- end }}
746+
{{- /* Alerting */ -}}
747+
{{- if .Values.bifrost.alerting }}
748+
{{- if .Values.bifrost.alerting.rules }}
749+
{{- range .Values.bifrost.alerting.rules }}
750+
{{- if and (hasKey . "target_type") (not (hasKey . "target_id")) }}
751+
{{- fail (printf "alerting rule '%s': target_type is set but target_id is missing" .id) }}
752+
{{- end }}
753+
{{- if and (hasKey . "target_id") (not (hasKey . "target_type")) }}
754+
{{- fail (printf "alerting rule '%s': target_id is set but target_type is missing" .id) }}
755+
{{- end }}
756+
{{- end }}
757+
{{- end }}
758+
{{- $_ := set $config "alerting" .Values.bifrost.alerting }}
759+
{{- end }}
746760
{{- /* Config Store */ -}}
747761
{{- if .Values.storage.configStore.enabled }}
748762
{{- $configStoreType := .Values.storage.configStore.type | default .Values.storage.mode }}

helm-charts/bifrost/values.schema.json

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3251,6 +3251,262 @@
32513251
"additionalProperties": false
32523252
}
32533253
},
3254+
"alerting": {
3255+
"type": "object",
3256+
"description": "Declarative alert channels, rules, and global alerting settings synced into the config store at startup.",
3257+
"properties": {
3258+
"history_retention_days": {
3259+
"type": "integer",
3260+
"minimum": 0,
3261+
"default": 365,
3262+
"description": "Days to retain alert history (0 = no pruning)"
3263+
},
3264+
"webhook_network": {
3265+
"type": "object",
3266+
"description": "Outbound URL validation controls for webhook-based alert channels",
3267+
"properties": {
3268+
"allow_http": {
3269+
"type": "boolean",
3270+
"default": false,
3271+
"description": "Allow http (not just https) webhook URLs"
3272+
},
3273+
"allow_private_network": {
3274+
"type": "boolean",
3275+
"default": false,
3276+
"description": "Allow webhooks targeting private or loopback addresses"
3277+
}
3278+
},
3279+
"additionalProperties": false
3280+
},
3281+
"channels": {
3282+
"type": "array",
3283+
"description": "Declarative alert channels (Slack, Microsoft Teams, PagerDuty, generic webhook)",
3284+
"items": {
3285+
"type": "object",
3286+
"properties": {
3287+
"id": { "type": "string", "description": "Stable channel ID" },
3288+
"name": { "type": "string", "description": "Operator-facing channel name" },
3289+
"description": { "type": "string" },
3290+
"type": {
3291+
"type": "string",
3292+
"enum": ["slack", "microsoft_teams", "pagerduty", "webhook"],
3293+
"description": "Channel delivery type"
3294+
},
3295+
"enabled": { "type": "boolean" },
3296+
"cooldown_seconds": {
3297+
"type": "integer",
3298+
"minimum": 0,
3299+
"description": "Minimum seconds between sends for this channel"
3300+
},
3301+
"config": {
3302+
"type": "object",
3303+
"description": "Channel-specific delivery configuration. Slack and Microsoft Teams use webhook_url or url. PagerDuty uses routing_key or integration_key. Generic webhook uses url or webhook_url, plus optional string headers.",
3304+
"additionalProperties": true
3305+
}
3306+
},
3307+
"required": ["id", "name", "type", "enabled", "config"],
3308+
"allOf": [
3309+
{
3310+
"if": {
3311+
"properties": { "type": { "const": "slack" } },
3312+
"required": ["type"]
3313+
},
3314+
"then": {
3315+
"properties": {
3316+
"config": {
3317+
"type": "object",
3318+
"properties": {
3319+
"webhook_url": {
3320+
"type": "string",
3321+
"minLength": 1,
3322+
"anyOf": [{ "format": "uri" }, { "pattern": "^env\\.[A-Za-z_][A-Za-z0-9_]*$" }],
3323+
"description": "Slack incoming webhook URL. Supports env.VAR_NAME references."
3324+
},
3325+
"url": {
3326+
"type": "string",
3327+
"minLength": 1,
3328+
"anyOf": [{ "format": "uri" }, { "pattern": "^env\\.[A-Za-z_][A-Za-z0-9_]*$" }],
3329+
"description": "Alias for webhook_url. Supports env.VAR_NAME references."
3330+
}
3331+
},
3332+
"oneOf": [{ "required": ["webhook_url"] }, { "required": ["url"] }],
3333+
"additionalProperties": true
3334+
}
3335+
},
3336+
"required": ["config"]
3337+
}
3338+
},
3339+
{
3340+
"if": {
3341+
"properties": { "type": { "const": "microsoft_teams" } },
3342+
"required": ["type"]
3343+
},
3344+
"then": {
3345+
"properties": {
3346+
"config": {
3347+
"type": "object",
3348+
"properties": {
3349+
"webhook_url": {
3350+
"type": "string",
3351+
"minLength": 1,
3352+
"anyOf": [{ "format": "uri" }, { "pattern": "^env\\.[A-Za-z_][A-Za-z0-9_]*$" }],
3353+
"description": "Microsoft Teams incoming webhook or workflow URL. Supports env.VAR_NAME references."
3354+
},
3355+
"url": {
3356+
"type": "string",
3357+
"minLength": 1,
3358+
"anyOf": [{ "format": "uri" }, { "pattern": "^env\\.[A-Za-z_][A-Za-z0-9_]*$" }],
3359+
"description": "Alias for webhook_url. Supports env.VAR_NAME references."
3360+
}
3361+
},
3362+
"oneOf": [{ "required": ["webhook_url"] }, { "required": ["url"] }],
3363+
"additionalProperties": true
3364+
}
3365+
},
3366+
"required": ["config"]
3367+
}
3368+
},
3369+
{
3370+
"if": {
3371+
"properties": { "type": { "const": "pagerduty" } },
3372+
"required": ["type"]
3373+
},
3374+
"then": {
3375+
"properties": {
3376+
"config": {
3377+
"type": "object",
3378+
"properties": {
3379+
"routing_key": {
3380+
"type": "string",
3381+
"minLength": 1,
3382+
"description": "PagerDuty Events API v2 integration key. Supports env.VAR_NAME references."
3383+
},
3384+
"integration_key": {
3385+
"type": "string",
3386+
"minLength": 1,
3387+
"description": "Alias for routing_key. Supports env.VAR_NAME references."
3388+
}
3389+
},
3390+
"oneOf": [{ "required": ["routing_key"] }, { "required": ["integration_key"] }],
3391+
"additionalProperties": true
3392+
}
3393+
},
3394+
"required": ["config"]
3395+
}
3396+
},
3397+
{
3398+
"if": {
3399+
"properties": { "type": { "const": "webhook" } },
3400+
"required": ["type"]
3401+
},
3402+
"then": {
3403+
"properties": {
3404+
"config": {
3405+
"type": "object",
3406+
"properties": {
3407+
"url": {
3408+
"type": "string",
3409+
"minLength": 1,
3410+
"anyOf": [{ "format": "uri" }, { "pattern": "^env\\.[A-Za-z_][A-Za-z0-9_]*$" }],
3411+
"description": "Generic webhook URL. Supports env.VAR_NAME references."
3412+
},
3413+
"webhook_url": {
3414+
"type": "string",
3415+
"minLength": 1,
3416+
"anyOf": [{ "format": "uri" }, { "pattern": "^env\\.[A-Za-z_][A-Za-z0-9_]*$" }],
3417+
"description": "Alias for url. Supports env.VAR_NAME references."
3418+
},
3419+
"headers": {
3420+
"type": "object",
3421+
"description": "Optional headers to send with the webhook request. Values support env.VAR_NAME references. Sensitive hop-by-hop headers are ignored at send time.",
3422+
"additionalProperties": { "type": "string" }
3423+
}
3424+
},
3425+
"oneOf": [{ "required": ["url"] }, { "required": ["webhook_url"] }],
3426+
"additionalProperties": true
3427+
}
3428+
},
3429+
"required": ["config"]
3430+
}
3431+
}
3432+
],
3433+
"additionalProperties": false
3434+
}
3435+
},
3436+
"rules": {
3437+
"type": "array",
3438+
"description": "Declarative alert rules evaluated against governance usage metrics",
3439+
"items": {
3440+
"type": "object",
3441+
"properties": {
3442+
"id": { "type": "string", "description": "Stable rule ID" },
3443+
"name": { "type": "string", "description": "Operator-facing rule name" },
3444+
"description": { "type": "string" },
3445+
"enabled": { "type": "boolean" },
3446+
"scope_type": {
3447+
"type": "string",
3448+
"enum": ["virtual_key", "team", "customer"],
3449+
"description": "Governance owner scope to evaluate"
3450+
},
3451+
"scope_id": { "type": "string", "description": "ID of the virtual key, team, or customer" },
3452+
"cel_expression": {
3453+
"type": "string",
3454+
"description": "CEL expression evaluated against usage variables (budget_usage_percent, budget_spent, request_usage, token_usage, etc.)"
3455+
},
3456+
"query": {
3457+
"type": "object",
3458+
"description": "Optional UI query-builder representation of cel_expression",
3459+
"additionalProperties": true
3460+
},
3461+
"cooldown_seconds": {
3462+
"type": "integer",
3463+
"minimum": 0,
3464+
"default": 60,
3465+
"description": "Minimum seconds between notifications for this rule"
3466+
},
3467+
"notify_once_per_reset_cycle": {
3468+
"type": "boolean",
3469+
"default": false,
3470+
"description": "When true, notify at most once per budget/rate-limit reset cycle"
3471+
},
3472+
"channel_ids": {
3473+
"type": "array",
3474+
"description": "IDs of channels to notify when this rule matches",
3475+
"items": { "type": "string", "minLength": 1 },
3476+
"minItems": 1
3477+
},
3478+
"target_type": {
3479+
"type": "string",
3480+
"enum": ["budget"],
3481+
"description": "Explicit target type; pair with target_id to evaluate one specific budget"
3482+
},
3483+
"target_id": { "type": "string", "description": "ID of the explicit target" }
3484+
},
3485+
"required": ["id", "name", "enabled", "scope_type", "scope_id", "cel_expression", "channel_ids"],
3486+
"allOf": [
3487+
{
3488+
"if": {
3489+
"required": ["target_type"]
3490+
},
3491+
"then": {
3492+
"required": ["target_id"]
3493+
}
3494+
},
3495+
{
3496+
"if": {
3497+
"required": ["target_id"]
3498+
},
3499+
"then": {
3500+
"required": ["target_type"]
3501+
}
3502+
}
3503+
],
3504+
"additionalProperties": false
3505+
}
3506+
}
3507+
},
3508+
"additionalProperties": false
3509+
},
32543510
"auditLogs": {
32553511
"type": "object",
32563512
"properties": {

helm-charts/bifrost/values.yaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,55 @@ bifrost:
10631063
# tool_name: "create_pull_request"
10641064
# action: "include"
10651065

1066+
# Alerting: declarative alert channels and rules seeded from Helm.
1067+
# Rendered directly as top-level `alerting` in config.json.
1068+
# alerting:
1069+
# history_retention_days: 365 # Days to retain alert history (0 = no pruning)
1070+
# webhook_network:
1071+
# allow_http: false # Allow http (not just https) webhook URLs
1072+
# allow_private_network: false # Allow webhooks targeting private/loopback addresses
1073+
# channels:
1074+
# - id: "slack-ops"
1075+
# name: "Ops Slack"
1076+
# type: slack # Options: slack, microsoft_teams, pagerduty, webhook
1077+
# enabled: true
1078+
# cooldown_seconds: 300
1079+
# config:
1080+
# webhook_url: "env.SLACK_WEBHOOK_URL"
1081+
# - id: "pagerduty-critical"
1082+
# name: "PagerDuty Critical"
1083+
# type: pagerduty
1084+
# enabled: true
1085+
# config:
1086+
# routing_key: "env.PAGERDUTY_ROUTING_KEY"
1087+
# - id: "teams-alerts"
1088+
# name: "Teams Alerts"
1089+
# type: microsoft_teams
1090+
# enabled: true
1091+
# config:
1092+
# webhook_url: "env.TEAMS_WEBHOOK_URL"
1093+
# - id: "generic-webhook"
1094+
# name: "Generic Webhook"
1095+
# type: webhook
1096+
# enabled: true
1097+
# config:
1098+
# url: "https://my-internal-hook.corp/alerts"
1099+
# headers:
1100+
# X-Alert-Token: "env.WEBHOOK_AUTH_HEADER"
1101+
# rules:
1102+
# - id: "budget-80pct"
1103+
# name: "Budget 80% Alert"
1104+
# enabled: true
1105+
# scope_type: virtual_key # Options: virtual_key, team, customer
1106+
# scope_id: "vk-1"
1107+
# cel_expression: "budget_usage_percent >= 80"
1108+
# cooldown_seconds: 3600
1109+
# notify_once_per_reset_cycle: false
1110+
# channel_ids: ["slack-ops"]
1111+
# # Optionally target a specific budget:
1112+
# # target_type: budget
1113+
# # target_id: "budget-1"
1114+
10661115
# Audit logs configuration for CADF-compliant activity logging
10671116
auditLogs:
10681117
disabled: false

0 commit comments

Comments
 (0)