complete MCP tool surface — Matrix approval webhook loop + token verification

Plan #6 (MCP Tool Completion / bin/homelab Migration) done.

- Approval records created for gated request_execution actions
- Notifier sends Matrix messages with HMAC approval tokens
- Stores matrix_event_id, polls /relations/{id}/m.annotation for /
- Reaction detection triggers DecideApproval API call
- Token verification added to DecideApproval endpoint
- Migration 013: matrix_event_id + alert_sent_at on approvals
- AGENTS.md: 21-tool surface documented, stale homelab CLI refs removed
- Plan index updated, audit cross-reference refreshed
This commit is contained in:
2026-07-08 11:02:06 +02:00
parent 5b22f2367b
commit 7c6cffb5f5
12 changed files with 907 additions and 235 deletions

View File

@@ -271,17 +271,17 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server {
return textResult(result), nil
case "systemctl":
svc := strings.TrimPrefix(targetSlug, "lxc:")
if params == "enable" || params == "disable" {
pool.Exec(ctx, `UPDATE executions SET status='pending_approval', risk_class='config_mutation' WHERE entity_id=$1`, id)
createApproval(ctx, pool, id, targetID, "systemctl", svc+":"+params, "config_mutation")
return textResult(fmt.Sprintf("systemctl %s on %s requires approval — execution %s queued", params, svc, id)), nil
}
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 {
@@ -329,6 +329,7 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server {
}
// upgrade requires approval — queue
pool.Exec(ctx, `UPDATE executions SET status='pending_approval', risk_class='config_mutation' WHERE entity_id=$1`, id)
createApproval(ctx, pool, id, targetID, "apt_upgrade", params, "config_mutation")
return textResult(fmt.Sprintf("apt_upgrade on %s requires approval — execution %s queued", targetSlug, id)), nil
default:
@@ -941,4 +942,5 @@ func createApproval(ctx context.Context, pool *db.Pool, execID, targetID uuid.UU
VALUES ($1, $2, $3, $4, 'execution', $5::jsonb, 'pending',
now() + interval '1 hour', now())`,
approvalID, targetID, action, riskClass, payload)
pool.Exec(ctx, `UPDATE executions SET approval_id = $2 WHERE entity_id = $1`, execID, approvalID)
}