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
|
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(
|
InputSchema: objSchema(
|
||||||
prop{"target", "string", "Target entity slug"},
|
prop{"target", "string", "Target entity slug (e.g. lxc:caddy)"},
|
||||||
prop{"action", "string", "Action to perform"},
|
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) {
|
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
args := argsMap(req)
|
args := argsMap(req)
|
||||||
targetSlug, _ := args["target"].(string)
|
targetSlug, _ := args["target"].(string)
|
||||||
action, _ := args["action"].(string)
|
action, _ := args["action"].(string)
|
||||||
|
params, _ := args["params"].(string)
|
||||||
if targetSlug == "" || action == "" {
|
if targetSlug == "" || action == "" {
|
||||||
return textResult("error: target and action are required"), nil
|
return textResult("error: target and action required"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetID uuid.UUID
|
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
|
return textResult(fmt.Sprintf("target not found: %s", targetSlug)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := uuid.NewV7()
|
id, _ := uuid.NewV7()
|
||||||
if err != nil {
|
|
||||||
return textResult(fmt.Sprintf("uuid error: %v", err)), nil
|
|
||||||
}
|
|
||||||
correlationID := uuid.New().String()
|
correlationID := uuid.New().String()
|
||||||
|
|
||||||
execName := action + " on " + targetSlug + " (" + id.String()[:8] + ")"
|
// Write execution record
|
||||||
execSlug := "exec:" + targetSlug + ":" + id.String()[:8]
|
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, `
|
// Execute reversible actions immediately
|
||||||
INSERT INTO entities (id, slug, type, name, attributes)
|
switch action {
|
||||||
VALUES ($1, $2, 'execution', $3, '{}')`,
|
case "restart":
|
||||||
id, execSlug, execName)
|
host, user, err := resolveHost(ctx, pool, targetSlug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return textResult(fmt.Sprintf("insert entity: %v", 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",
|
register(&mcp.Tool{Name: "get_trend", Description: "Get metric trends for an entity",
|
||||||
|
|||||||
Reference in New Issue
Block a user