fix: build execution result JSON via json.Marshal (was stuck at 'approved')
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

The final "UPDATE executions SET result=$::jsonb" built its payload with
fmt.Sprintf and only escaped newlines. apt/pct output contains quotes,
backslashes and control chars, so the payload was invalid JSON, the jsonb
cast failed, and the (unchecked) UPDATE was silently discarded — the
execution stayed 'approved' with a NULL result even though the LXC was fully
provisioned (verified live: vmid auto-assigned, container running, service
installed, post_install ran).

- executeApprovedAction: marshal result via json.Marshal; log UPDATE errors
- add jsonErr() helper; route all pct_create failure-path results through it
- mcp/server.go: add jsonOut() for restart/systemctl/pct_exec inline results
- regression test for JSON validity on quote/backslash/control-char output

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 00:58:43 +02:00
parent ac86302f52
commit a1f666f68a
3 changed files with 58 additions and 13 deletions

View File

@@ -332,7 +332,7 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server {
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")))
id, jsonOut(out))
return textResult(result), nil
case "systemctl":
@@ -353,7 +353,7 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server {
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")))
id, jsonOut(out))
return textResult(result), nil
case "pct_exec":
@@ -377,7 +377,7 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server {
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")))
id, jsonOut(out))
return textResult(result), nil
case "apt_upgrade":
@@ -872,6 +872,15 @@ func textResult(s string) *mcp.CallToolResult {
}
}
// jsonOut builds a valid {"output": "..."} JSON payload for an execution's
// result column. Command output contains quotes/backslashes/control chars, so
// it must be JSON-marshaled — a hand-built string fails the ::jsonb cast and
// silently drops the status update, leaving the execution stuck.
func jsonOut(out string) []byte {
b, _ := json.Marshal(map[string]any{"output": out})
return b
}
func queryEntity(ctx context.Context, pool *db.Pool, idOrSlug string) *mcp.CallToolResult {
var id uuid.UUID
if u, err := uuid.Parse(idOrSlug); err == nil {