feat(monitors): validate metric monitors and confine selectors to token scope

This commit is contained in:
2026-09-17 08:56:15 +00:00
parent f01375470e
commit 4068349afe
5 changed files with 152 additions and 11 deletions
+37 -4
View File
@@ -118,6 +118,25 @@ func buildMonitor(args map[string]any) (models.Monitor, error) {
if b, ok := rawTarget["insecure"].(bool); ok {
target.Insecure = b
}
target.Metric = stringArg(rawTarget, "metric")
target.Mount = stringArg(rawTarget, "mount")
if n, ok := rawTarget["threshold"].(float64); ok {
target.Threshold = n
}
if n, ok := rawTarget["period_sec"].(float64); ok {
target.PeriodSec = int(n)
}
if n, ok := rawTarget["grace_sec"].(float64); ok {
target.GraceSec = int(n)
}
if sel, ok := rawTarget["selector"].(map[string]any); ok {
target.Selector = map[string]string{}
for k, v := range sel {
if s, ok := v.(string); ok {
target.Selector[k] = s
}
}
}
switch monitorType {
case models.MonitorHTTP, models.MonitorTLS:
@@ -131,6 +150,9 @@ func buildMonitor(args map[string]any) (models.Monitor, error) {
if monitorType == models.MonitorTCP && target.Port == 0 {
return models.Monitor{}, fmt.Errorf("target.port is required for a tcp monitor")
}
case models.MonitorHeartbeat, models.MonitorMetric:
// Full validation (validateHeartbeat / validateMetric) runs inside
// services.CreateMonitor; nothing further is required here.
default:
return models.Monitor{}, fmt.Errorf("unknown monitor type %q", monitorType)
}
@@ -150,6 +172,10 @@ func buildMonitor(args map[string]any) (models.Monitor, error) {
if n, ok := args["interval_sec"].(float64); ok && int(n) > 0 {
interval = int(n)
}
forSec := 0
if n, ok := args["for_sec"].(float64); ok && int(n) > 0 {
forSec = int(n)
}
return models.Monitor{
Name: name,
@@ -157,6 +183,7 @@ func buildMonitor(args map[string]any) (models.Monitor, error) {
Type: monitorType,
Target: target,
IntervalSec: interval,
ForSec: forSec,
// Never armed on creation. A monitor that started enabled would begin
// alerting real people the moment a model invented it, and creating
// must stay a separate decision from acting.
@@ -257,10 +284,11 @@ func init() {
Name: "create_monitor",
Args: []ToolArg{
{Name: "name", Type: ArgString, Description: "Name for the monitor.", Required: true},
{Name: "type", Type: ArgString, Description: "Check type: http, tcp, icmp or tls.", Required: true},
{Name: "target", Type: ArgObject, Description: "What to check. http/tls take url; tcp/icmp take host, and tcp also port. Optional: method, keyword, expected_status, tls_warn_days, insecure.", Required: true},
{Name: "type", Type: ArgString, Description: "Check type: http, tcp, icmp, tls, heartbeat or metric.", Required: true},
{Name: "target", Type: ArgObject, Description: "What to check. http/tls take url; tcp/icmp take host, and tcp also port. Optional: method, keyword, expected_status, tls_warn_days, insecure. heartbeat takes period_sec and optional grace_sec; metric takes metric (disk_pct, disk_free_gb, mem_pct, load_per_core, unit_failed, container_unhealthy, reboot_pending_days, agent_offline_min), threshold, optional mount and selector (tag map).", Required: true},
{Name: "group", Type: ArgString, Description: "Optional group name to file the monitor under."},
{Name: "interval_sec", Type: ArgInteger, Description: "Seconds between checks; defaults to 60."},
{Name: "for_sec", Type: ArgInteger, Description: "For a metric monitor, how long the condition must hold before a server counts as down."},
},
Write: true,
Scope: "monitors:write",
@@ -277,12 +305,17 @@ func init() {
return nil, fmt.Errorf("could not create the monitor: %w", err)
}
LogCreated(c, "monitor", created.MonitorID, created.Name)
return map[string]any{
out := map[string]any{
"monitor_id": created.MonitorID,
"name": created.Name,
"enabled": false,
"note": "Created disabled. Enable it in Vantage to start checking.",
}, nil
}
if created.HeartbeatToken != "" {
out["heartbeat_token"] = created.HeartbeatToken
out["heartbeat_token_note"] = "Shown once."
}
return out, nil
},
})
}