fix: assent-window auto-approve goroutine used the request-scoped context
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 14:51:52 +02:00
parent 2e922f6421
commit 7387df3276

View File

@@ -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
}