mcp: expand request_execution — restart, systemctl, pct_exec, apt_upgrade with inline SSH
This commit is contained in:
@@ -223,17 +223,19 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server {
|
||||
nStr(args["status"])), nil
|
||||
})
|
||||
|
||||
register(&mcp.Tool{Name: "request_execution", Description: "Request a gated execution (Hermes-only mutation path)",
|
||||
register(&mcp.Tool{Name: "request_execution", Description: "Request a gated execution (Hermes-only mutation path). Supported actions: restart, systemctl, pct_exec, apt_upgrade.",
|
||||
InputSchema: objSchema(
|
||||
prop{"target", "string", "Target entity slug"},
|
||||
prop{"action", "string", "Action to perform"},
|
||||
prop{"target", "string", "Target entity slug (e.g. lxc:caddy)"},
|
||||
prop{"action", "string", "Action: restart, systemctl, pct_exec, apt_upgrade"},
|
||||
prop{"params", "string", "Extra params: for systemctl use 'enable|disable|reload', for pct_exec use the shell command, for apt_upgrade use 'audit|upgrade'"},
|
||||
),
|
||||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := argsMap(req)
|
||||
targetSlug, _ := args["target"].(string)
|
||||
action, _ := args["action"].(string)
|
||||
params, _ := args["params"].(string)
|
||||
if targetSlug == "" || action == "" {
|
||||
return textResult("error: target and action are required"), nil
|
||||
return textResult("error: target and action required"), nil
|
||||
}
|
||||
|
||||
var targetID uuid.UUID
|
||||
@@ -241,34 +243,97 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server {
|
||||
return textResult(fmt.Sprintf("target not found: %s", targetSlug)), nil
|
||||
}
|
||||
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("uuid error: %v", err)), nil
|
||||
}
|
||||
id, _ := uuid.NewV7()
|
||||
correlationID := uuid.New().String()
|
||||
|
||||
execName := action + " on " + targetSlug + " (" + id.String()[:8] + ")"
|
||||
// Write execution record
|
||||
execSlug := "exec:" + targetSlug + ":" + id.String()[:8]
|
||||
pool.Exec(ctx, `INSERT INTO entities (id, slug, type, name, attributes) VALUES ($1, $2, 'execution', $3, '{}')`,
|
||||
id, execSlug, action+" on "+targetSlug)
|
||||
pool.Exec(ctx, `INSERT INTO executions (entity_id, target_entity_id, action, risk_class, status, correlation_id, agent_id) VALUES ($1, $2, $3, 'reversible_low', 'running', $4, $5)`,
|
||||
id, targetID, action+":"+params, correlationID, agentID)
|
||||
|
||||
_, err = pool.Exec(ctx, `
|
||||
INSERT INTO entities (id, slug, type, name, attributes)
|
||||
VALUES ($1, $2, 'execution', $3, '{}')`,
|
||||
id, execSlug, execName)
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("insert entity: %v", err)), nil
|
||||
// Execute reversible actions immediately
|
||||
switch action {
|
||||
case "restart":
|
||||
host, user, err := resolveHost(ctx, pool, targetSlug)
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("resolve: %v", err)), nil
|
||||
}
|
||||
svc := strings.TrimPrefix(targetSlug, "lxc:")
|
||||
out, err := sshExec(ctx, host, user, fmt.Sprintf("systemctl restart %s 2>&1; sleep 1; systemctl is-active %s", svc, svc))
|
||||
result := fmt.Sprintf("restart %s: %s", svc, out)
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("restart %s: ERROR %v", svc, err)
|
||||
}
|
||||
pool.Exec(ctx, `UPDATE executions SET status='completed', result=$2::jsonb WHERE entity_id=$1`,
|
||||
id, fmt.Sprintf(`{"output":"%s"}`, strings.ReplaceAll(out, "\n", "\\n")))
|
||||
return textResult(result), nil
|
||||
|
||||
case "systemctl":
|
||||
host, user, err := resolveHost(ctx, pool, targetSlug)
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("resolve: %v", err)), nil
|
||||
}
|
||||
svc := strings.TrimPrefix(targetSlug, "lxc:")
|
||||
cmd := fmt.Sprintf("systemctl %s %s 2>&1; sleep 1; systemctl is-active %s", params, svc, svc)
|
||||
if params == "enable" || params == "disable" {
|
||||
// config_mutation — mark as pending for operator approval
|
||||
pool.Exec(ctx, `UPDATE executions SET status='pending_approval', risk_class='config_mutation' WHERE entity_id=$1`, id)
|
||||
return textResult(fmt.Sprintf("systemctl %s on %s requires approval — execution %s queued", params, svc, id)), nil
|
||||
}
|
||||
out, err := sshExec(ctx, host, user, cmd)
|
||||
result := fmt.Sprintf("systemctl %s %s: %s", params, svc, out)
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("systemctl %s %s: ERROR %v", params, svc, err)
|
||||
}
|
||||
pool.Exec(ctx, `UPDATE executions SET status='completed', result=$2::jsonb WHERE entity_id=$1`,
|
||||
id, fmt.Sprintf(`{"output":"%s"}`, strings.ReplaceAll(out, "\n", "\\n")))
|
||||
return textResult(result), nil
|
||||
|
||||
case "pct_exec":
|
||||
var pveID string
|
||||
if err := pool.QueryRow(ctx, "SELECT attributes->>'pve_id' FROM entities WHERE slug = $1", targetSlug).Scan(&pveID); err != nil || pveID == "" {
|
||||
return textResult(fmt.Sprintf("LXC not found: %s", targetSlug)), nil
|
||||
}
|
||||
// Resolve Proxmox host
|
||||
var hostSlug string
|
||||
pool.QueryRow(ctx, "SELECT attributes->>'host' FROM entities WHERE slug = $1", targetSlug).Scan(&hostSlug)
|
||||
if hostSlug == "" {
|
||||
hostSlug = "host:hubris" // default
|
||||
}
|
||||
host, user, err := resolveHost(ctx, pool, hostSlug)
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("resolve Proxmox host: %v", err)), nil
|
||||
}
|
||||
out, err := sshExec(ctx, host, user, fmt.Sprintf("pct exec %s -- %s 2>&1", pveID, params))
|
||||
result := fmt.Sprintf("pct exec %s: %s", pveID, out)
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("pct exec %s: ERROR %v", pveID, err)
|
||||
}
|
||||
pool.Exec(ctx, `UPDATE executions SET status='completed', result=$2::jsonb WHERE entity_id=$1`,
|
||||
id, fmt.Sprintf(`{"output":"%s"}`, strings.ReplaceAll(out, "\n", "\\n")))
|
||||
return textResult(result), nil
|
||||
|
||||
case "apt_upgrade":
|
||||
if params == "audit" {
|
||||
host, user, err := resolveHost(ctx, pool, targetSlug)
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("resolve: %v", err)), nil
|
||||
}
|
||||
out, err := sshExec(ctx, host, user, "apt update -qq 2>&1 >/dev/null; apt list --upgradable 2>/dev/null | tail -n +2 | wc -l; apt list --upgradable 2>/dev/null | tail -n +2 | head -20")
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("apt audit error: %v", err)), nil
|
||||
}
|
||||
return textResult("apt audit:\n" + out), nil
|
||||
}
|
||||
// upgrade requires approval — queue
|
||||
pool.Exec(ctx, `UPDATE executions SET status='pending_approval', risk_class='config_mutation' WHERE entity_id=$1`, id)
|
||||
return textResult(fmt.Sprintf("apt_upgrade on %s requires approval — execution %s queued", targetSlug, id)), nil
|
||||
|
||||
default:
|
||||
return textResult(fmt.Sprintf("unknown action: %s. Supported: restart, systemctl, pct_exec, apt_upgrade", action)), nil
|
||||
}
|
||||
|
||||
_, err = pool.Exec(ctx, `
|
||||
INSERT INTO executions (entity_id, target_entity_id, action, risk_class,
|
||||
status, correlation_id, agent_id)
|
||||
VALUES ($1, $2, $3, 'unclassified', 'pending', $4, $5)`,
|
||||
id, targetID, action, correlationID, agentID)
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("insert execution: %v", err)), nil
|
||||
}
|
||||
|
||||
return textResult(fmt.Sprintf("execution requested: id=%s target=%s action=%s correlation=%s",
|
||||
id, targetSlug, action, correlationID)), nil
|
||||
})
|
||||
|
||||
register(&mcp.Tool{Name: "get_trend", Description: "Get metric trends for an entity",
|
||||
|
||||
Reference in New Issue
Block a user