From 7387df3276308cd82972750fa8da4bfd4b845b87 Mon Sep 17 00:00:00 2001 From: dtoro Date: Fri, 10 Jul 2026 14:51:52 +0200 Subject: [PATCH] fix: assent-window auto-approve goroutine used the request-scoped context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified live testing the new atomic pct_create: an assent-window auto-approved pct_create appeared to "run" (logged "auto-approved... running now") but the execution stayed stuck at 'approved' forever. Root cause: `go executeApprovedViaAPI(ctx, ...)` passed the MCP tool-call's own context — which is cancelled the instant the triggering /chat request's HTTP response completes, i.e. on every normal turn. The spawned goroutine's POST to the approval-decision endpoint died with "context canceled" before it could even start the real work, and nothing surfaced this to the operator or the agent — the execution just sat at 'approved' with no error, indistinguishable from "still running." This is exactly the context-lifetime bug class httpapi's own approval goroutine (executeApprovedAction) already avoided by using context.Background() — it had just been missed in these two call sites (apt_upgrade and pct_create auto-approve). Fixed both to use context.Background(), matching the correct pattern already in place elsewhere. Audited for other goroutines spawned with a request-scoped ctx — none found; the sshExec internal goroutines are synchronous/waited-on via select and correctly scoped to the call. Co-Authored-By: Claude Opus 4.8 --- internal/mcp/server.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 2095cb4..633cfb4 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -404,7 +404,19 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server { 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") if approved := autoApprove(ctx, pool, id); approved { - go executeApprovedViaAPI(ctx, id, targetSlug, "apt_upgrade:"+params) + // context.Background(), NOT ctx: ctx is scoped to this + // MCP tool call, which ends (and cancels) as soon as the + // chat turn's HTTP response completes — normal, expected, + // happens on every turn. A goroutine meant to outlive the + // request must not inherit its context, or the async work + // dies silently the instant the turn ends. Found live: + // every assent-window auto-approved pct_create failed + // with "context canceled" the moment the triggering + // /chat request finished — exactly the same context- + // lifetime class of bug, in the one place it had been + // missed (httpapi's own approval goroutine already used + // context.Background() correctly). + go executeApprovedViaAPI(context.Background(), id, targetSlug, "apt_upgrade:"+params) slog.Info("mcp: apt_upgrade auto-approved via assent window", "execution_id", id) return textResult(fmt.Sprintf("apt_upgrade on %s auto-approved via assent window — execution %s running.", targetSlug, id)), nil } @@ -421,7 +433,7 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server { pool.Exec(ctx, `UPDATE executions SET status='pending_approval', risk_class='config_mutation' WHERE entity_id=$1`, id) createApproval(ctx, pool, id, targetID, "pct_create", params, "config_mutation") if approved := autoApprove(ctx, pool, id); approved { - go executeApprovedViaAPI(ctx, id, targetSlug, "pct_create:"+params) + go executeApprovedViaAPI(context.Background(), id, targetSlug, "pct_create:"+params) // see context.Background() comment above (apt_upgrade case) — same bug, same fix slog.Info("mcp: pct_create auto-approved via assent window", "execution_id", id) return textResult(fmt.Sprintf("pct_create on %s auto-approved via assent window — execution %s running. The LXC is being provisioned now.", targetSlug, id)), nil }