fix: build execution result JSON via json.Marshal (was stuck at 'approved')
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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user