Repository navigation
Add the custom-alert web API mirroring /api/views (#3285) - #3333
Conversation
Component 7's backend: the HTTP surface for the custom-alert-rule editor
(its JS deferred). MapCustomAlerts shadows MapCustomViews exactly, reusing
the SAME store/evaluator/templates the MCP tools use rather than a second
implementation:
GET /api/alerts list rules (CustomAlertRuleStore.ListAsync)
GET /api/alerts/{id} get one, full definition
POST /api/alerts create (TryParse-validated before persist)
PUT /api/alerts/{id} update (optimistic concurrency, same as views)
DELETE /api/alerts/{id} delete via ResolveAndDeleteRuleAsync (#3305:
force-resolve open incidents before the cascade)
GET /api/alert-templates CustomAlertTemplates.All
POST /api/alerts/validate dry-run TryParse -> {valid, error}
POST /api/alerts/test evaluate-now, wraps test_custom_alert_rule
CustomAlertRuleResult maps onto the same HTTP codes /api/views maps
CustomViewResult to (200/201/204, 400, 404, 409). The writes plus the two
evaluate POSTs require application/json and are born gated by the host's
method-based write gate (a read-only seat is refused everything unsafe except
POST /api/compose/run), so a viewer seat gets a 403 without this layer naming
the gate. /test runs the shared per-server scalar seam on the host's VIEWER
pool, exactly as /api/compose/run runs the composer, never the owner pool.
The full-rule and summary wire builders are now shared once in
DarlingWebEndpoints (BuildFullRuleNode / BuildRuleSummariesNode) and called by
the MCP alert tools, removing the private duplicates they carried, so the web
and MCP rule shapes cannot drift (matching how /api/views shares its builders).
Also corrects a stale DarlingMcpCustomAlertTools class-doc paragraph that still
claimed evaluate-now was deliberately absent after #3299 shipped it.
Tests: extend the write-gate matrix with /api/alerts rows (CRUD + validate +
test born gated; reads open); pin the shared rule wire shape (enabled present,
definition embedded as an object). Fixes a pre-existing CS8620 nullability
warning in CustomAlertTemplatesTests. No MCP tool added, so the tool-inventory
pins are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WejwpgWF5Xfm3fAmoEbFz4
| /* Absent 'enabled' defaults true: create's default, and a full-replace PUT of a rule that names no | ||
| enabled state is an enabled rule (the real full-document editor always sends the field). */ | ||
| var enabled = true; | ||
| if (obj["enabled"] is JsonValue enabledValue && enabledValue.TryGetValue<bool>(out var e)) | ||
| { | ||
| enabled = e; | ||
| } |
There was a problem hiding this comment.
enabled on PUT /api/alerts/{id} diverges from the MCP surface's semantics for the same field, and fails open rather than rejecting bad input:
update_custom_alert_rule(MCP) is an explicit partial update — an omittedenabledpreserves the row's current value (enabled ?? row.EnabledinDarlingMcpCustomAlertTools.UpdateCustomAlertRule).- This web
PUTis a full replace, and an omitted (or type-mismatched, e.g."enabled": "false"sent as a string)enabledsilently resolves totrue— i.e. a PUT that only touchesname/description/definitionon a paused rule will silently re-enable it.
The PR description frames this API as "no new logic, just a web wrapper" reusing the same store/evaluator as the MCP tools, but this is genuinely new behavior for the one field both surfaces share. The comment above justifies it by assuming "the real full-document editor always sends the field" — true once that (currently deferred) JS editor ships, but in the meantime this is a public, gated-but-reachable HTTP endpoint that any script/integration can PUT against today, with no test coverage exercising the omitted/malformed case.
Given this field gates whether an alert actually fires, consider either:
- Making the web PUT a true full-replace only for the fields that were views-only before (name/description/definition), but treat
enabledlikeversion— require it explicitly and 400 when it's missing, rather than silently defaulting totrue; or - Matching the MCP tool's partial-update semantics (
enabled ?? current.Enabled) so a paused rule can't be silently resumed by an edit that never intended to touch that field.
|
Reviewed the custom-alert web API ( Overall: the route-for-route mirror of One correctness concern (posted inline on Everything else — request/definition validation ordering (validate before persist), 400/404/409 status mapping, delete routing through The pre-existing |
Part of #3285.
The custom-alert web API — the backend half of Component 7's web editor. Erik reviews on mobile (no visual pass), so the API ships now and the JS editor UI stays deferred.
MapCustomAlertsmirrorsMapCustomViewsroute-for-route, reusing the same store / evaluator / templates the MCP tools use ("no new logic, just a web wrapper"):/api/alertsCustomAlertRuleStore.ListAsync(summaries, no definition)/api/alerts/{id:long}CustomAlertRuleStore.GetAsync(full, 404 when missing)/api/alertsCustomAlertRuleDefinition.TryParsebefore persist; 201 / 400 / 409/api/alerts/{id:long}versionhandling as/api/views; 200 / 400 / 404 / 409/api/alerts/{id:long}CustomAlertEvaluator.ResolveAndDeleteRuleAsync(not rawDeleteAsync) so an open incident force-resolves before the FK cascade (#3305); 204 / 404/api/alert-templatesCustomAlertTemplates.All/api/alerts/validateTryParse→{valid, error}/api/alerts/testtest_custom_alert_ruleCustomAlertRuleResultmaps onto the same HTTP codes/api/viewsmapsCustomViewResultto.How the writes are gated
The three mutations and the two evaluate POSTs (
/validate,/test) requireapplication/json(the CSRF gate) and are born gated by the host's method-based write gate:DarlingWebSeat.IsRequestAllowedrefuses a read-only seat everything unsafe exceptPOST /api/compose/run, so a viewer seat gets a 403 on every one of them without this layer naming the gate./testruns the shared per-server scalar seam (EvaluateScalarNowAsync+ClassifyTestValue) on the host's viewer pool, exactly as/api/compose/runruns the composer — never the owner pool.Shared, drift-free wire shape
The full-rule and summary builders now live once in
DarlingWebEndpoints(BuildFullRuleNode/BuildRuleSummariesNode) and are called by the MCP alert tools, removing the private duplicates those tools carried — the same way/api/viewsshares its builders. Also corrects a staleDarlingMcpCustomAlertToolsclass-doc paragraph that still claimed evaluate-now was "deliberately absent" after #3299 shipped it.Flag: pre-existing #3305 grant gap (NOT changed here)
ResolveAndDeleteRuleAsyncwrites the force-resolve recovery row viaINSERT INTO config_alert_log, but neither theviewernor themcprole has INSERT onconfig_alert_log(onlyadmindoes). So on both the web (viewer-pool) DELETE and the already-shipped MCP (mcp-pool) delete, that INSERT fails permission-denied. It is failure-isolated (WriteTeardownResolutionAsyncswallows and logs), so the delete still succeeds (204) and the rule stops firing — but the "Resolved" history row #3305 intends is silently skipped on both least-privilege paths. It is masked in #3305's live tests because they connect as owner. This is a security-surface decision (may the least-privilege roles write alert history?), so I did not change the grant here; it wants its own issue.Tests
DarlingWebOidcTests.IsRequestAllowed_Matrixwith/api/alertsrows: CRUD writes +/validate+/testall born gated for a viewer; reads (list/get/templates) open; edit seat allowed.DarlingWebEndpointsTests(enabledpresent; definition embedded as a JSON object, not an escaped string).CustomAlertTemplatesTests.Build & full-suite results
FleetIdentifierScrubTests× 2 "scanned 0 files — the sweep lost the tree",NpgsqlRootCertificateValidationTests× 1 TLS), unrelated to this change.AuroraOnlySqlIsGatedTestsworktree source-scan baseline; no Lite files were touched, and the cross-app MCP inventory pin passed.🤖 Generated with Claude Code
https://claude.ai/code/session_01WejwpgWF5Xfm3fAmoEbFz4