GitHub
08/31/2026, 3:19 PM--alerts-enabled, but rules and channels
could only be changed by direct database edits, and those edits waited on the
5-minute snapshot refresh ticker. This PR makes the subsystem operator-manageable:
a full CRUD API in osctrl-api, a service command that turns rule/channel edits
into an instant hot-reload in osctrl-tls, and a CLI command group.
Change
Management API — new cmd/api/handlers/alerts.go (mirrors the log-sinks
handler surface):
• Rules: list / get / create / update / delete —
GET|POST /api/v1/alerts/rules, GET|PUT|DELETE /api/v1/alerts/rules/{id}
• Channels: list / get / types / create / update / delete —
`GET /api/v1/alerts/channels[...]", POST ..., etc., plus /channels/types serving the registry so the SPA can render channel forms
generically
• History: GET /api/v1/alerts/history?limit= (newest first, capped at 500)
• Secret handling identical to log sinks: channel configs redacted to ***
in reads unless an admin passes `reveal=1`; an update that submits the
*** placeholder merges the previously stored secret
• All routes admin-only, audit-logged, and registered only when
--alerts-enabled is set (nil manager → routes absent; direct handler
calls → 503). The apply endpoint reuses the restart rate limiter — an
alerts reload is the same class of privileged service-command operation
• Request/response shapes added to pkg/types
(AlertRuleCreateRequest, AlertChannelCreateRequest,
AlertChannelTypeSpec, AlertFieldSpec)
Instant hot-reload — service command plumbing:
• `pkg/servicecommands`: new reload-alerts action in the allowlist
• `cmd/api`: POST /api/v1/alerts/apply queues the command for osctrl-tls
(202 + command ID, same response shape as log-sinks apply)
• `cmd/tls`: the existing service-command watcher consumes the action —
reloads the alert rule snapshot and resets the channel sender cache
atomically (copy-on-write, in-flight matches finish against the old
ruleset); warns and no-ops when alerting is disabled. The 5-minute ticker
remains as a fallback path
CLI — new osctrl-cli alert command group:
• rules / rule-create / rule-delete, channels / channel-create /
channel-delete, apply (trigger hot-reload)
• json / csv / pretty output consistent with the rest of the CLI;
channel configs validated client-side against the registry before the
POST; db-mode attempts fail with a clear "requires --api" error
Supporting changes in pkg/alerts:
• Exported DecodeChannelIDs / EncodeChannelIDs for API and CLI use
• RedactedChannelConfig + MergeChannelSecrets (channel-registry
equivalents of the logsinks helpers)
• ErrInvalidChannelConfig sentinel, mapped to 400 by the API error router
Validation
• go build ./..., go vet, gofmt — clean
• golangci-lint on all touched packages — 0 new issues (5 pre-existing in
untouched files)
• 10 new handler tests, all passing: non-admin 403 (rules + channels),
disabled 503, full rule CRUD round-trip (create → 409 duplicate → list →
get → update → delete → 404), invalid source 400, channel CRUD with
redaction / reveal / secret-merge round-trip, malformed config 400, types
endpoint, history listing, and apply → command consumable by osctrl-tls
(asserted via the real servicecommands manager)
• Full affected suites green: cmd/api/handlers, cmd/api, cmd/cli,
cmd/alerts, cmd/tls, pkg/servicecommands
• CLI smoke: osctrl-cli alert --help renders the full group
Roadmap position
Stage 4 of 7 (alerts-api). The alerting system is now fully operable
end-to-end: rules and channels are managed via API/CLI, edits hot-reload into
osctrl-tls within one poll cycle, and delivery lands in email/webhooks with
per-channel audit history. Remaining: Stage 5 (node-inactive watcher),
Stage 6 (SPA "Alerts" section), Stage 7 (load hardening + retention sweep).
jmpsec/osctrlGitHub
08/31/2026, 3:24 PM