Summary
Scans started from the web UI run their Python subprocess with no deadline at all. The job
context is cancel-only, and the InvokeCtx / InvokeCtxCapture entry points never consult
resolveInvokeTimeout(), so OPENANT_INVOKE_TIMEOUT has no effect on this path. The CLI path is
bounded at 30 minutes by default; the server path is unbounded.
Because a running scan holds one of four server slots for its whole duration, a wedged subprocess
permanently consumes a quarter of the server's scan capacity with no automatic recovery.
This is the follow-up merged PR #237 said it was leaving for later.
The job context carries no deadline
apps/openant-cli/internal/server/server.go:655:
ctx, cancel := context.WithCancel(context.Background())
context.WithCancel gives cancellation but no timeout. Repo-verified: server.go contains exactly
two context.WithTimeout call sites, and neither covers the Python subprocess —
1520: rctx, cancel := context.WithTimeout(ctx, 5*time.Second) // DNS resolution
1545: ctx, cancel := context.WithTimeout(ctx, 15*time.Minute) // git clone
The invoke path never applies the configured timeout
apps/openant-cli/internal/python/invoke_ctx.go contains zero occurrences of
resolveInvokeTimeout (/usr/bin/grep -c resolveInvokeTimeout → 0). It takes the caller's context
and hands it straight to exec.CommandContext. The CLI path does the opposite —
apps/openant-cli/internal/python/invoke.go:91:
ctx, cancel := context.WithTimeout(context.Background(), resolveInvokeTimeout())
with defaultInvokeTimeout = 30 * time.Minute and the OPENANT_INVOKE_TIMEOUT override documented
alongside it.
Complete site enumeration (CODE-TRACED, not executed)
All three server call sites pass a deadline-free context:
server.go:1152 python.InvokeCtxCapture(job.ctx, ...) // the scan itself
server.go:1620 python.InvokeCtxCapture(ctx, ...) // report-data
server.go:1669 python.InvokeCtx(ctx, ...) // summary (makes LLM calls)
I have not executed a hung scan against a live server — that needs a wedged parser and a running
openant serve. The evidence here is the call graph, and I am labelling it as such.
Operational consequence
server.go:221 sizes the scan semaphore at four:
sem: make(chan struct{}, 4),
and :1096-1097 holds a slot for the entire run:
case s.sem <- struct{}{}:
defer func() { <-s.sem }()
So an unbounded subprocess does not merely hang its own job — it removes 1 of 4 slots until someone
cancels it by hand. The CLI cannot get into this state.
Why this is not the "cancel-only by design" case
server.go:1541-1545 is worth quoting in full, because read partially it looks like a ratification
of the missing deadline and it is the opposite:
// Bound the clone in time so a hostile remote that streams forever can't hold a
// scan slot indefinitely (the parent ctx is cancel-only). Disk size is not
// bounded here — a huge working tree is a documented limitation for a
// local, user-chosen scan target.
ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
"The parent ctx is cancel-only" is the justification for adding a local deadline, not an argument
that none is needed. The same pattern appears at :1520 for DNS. The maintainers' approach is
evidently: leave the job context cancel-only, and bound each subprocess that could run away. The
Python invoke is the one that never got that treatment.
Prior art — this is the maintainers' own stated follow-up
Merged PR #237:
that webui path does not honor OPENANT_INVOKE_TIMEOUT... unifying the timeout handling across
both is a reasonable follow-up once both are merged.
Both are merged. No issue tracks it — a search across all open and closed issues for
OPENANT_INVOKE_TIMEOUT / InvokeCtx / webui-timeout returns nothing.
Suggested fix
- Apply
resolveInvokeTimeout() in InvokeCtx / InvokeCtxCapture, or wrap the three server call
sites in context.WithTimeout(ctx, resolveInvokeTimeout()) — matching the local-bound pattern
already used at :1520 and :1545.
- Keep the job context cancel-only. This asks for parity with the CLI's operator escape hatch, not
for a job-level deadline that would truncate legitimately long scans; OPENANT_INVOKE_TIMEOUT
already exists precisely so an operator can raise it.
- On timeout, surface a distinct status so the UI can show "timed out" rather than a generic failure.
What I am not claiming
- I am not claiming a hang has occurred in a real deployment. The evidence is the call graph;
the consequence is code-traced, not executed.
- I am not asking for a deadline on the job context itself — see fix item 2.
- I have not measured how often a Python subprocess wedges in practice.
Grounding curve (added 2026-08-22)
| commit |
job ctx cancel-only |
invoke_ctx skips timeout |
3 call sites |
sem=4 |
env var exists |
verdict |
ad17f3f (PR #133) |
no server |
file absent |
— |
— |
partial (30 min fixed) |
FAILS |
b8844d4 (PR #235) |
true |
true |
true |
true |
not yet |
PARTIAL |
a601f82 (PR #237) |
true |
true |
true |
true |
true |
FULL |
b5019628 (HEAD) |
true |
true |
true |
true |
true |
FULL |
Attribution: every server-side leg entered in one squashed commit — PR #235 (b8844d4,
"feat(webui): add a local web UI for the scan pipeline"). git log -- internal/server/server.go
returns exactly that one commit, so there has been no drift since. A fix belongs against #235.
PR #237 (a601f82) added resolveInvokeTimeout and OPENANT_INVOKE_TIMEOUT to invoke.go only —
it never touched the server. Note the headline phrasing is only sayable from #237: before it there
was no env var to have no effect, and the honest statement would have been "the server path has no
deadline while the CLI is fixed at 30 minutes".
Summary
Scans started from the web UI run their Python subprocess with no deadline at all. The job
context is cancel-only, and the
InvokeCtx/InvokeCtxCaptureentry points never consultresolveInvokeTimeout(), soOPENANT_INVOKE_TIMEOUThas no effect on this path. The CLI path isbounded at 30 minutes by default; the server path is unbounded.
Because a running scan holds one of four server slots for its whole duration, a wedged subprocess
permanently consumes a quarter of the server's scan capacity with no automatic recovery.
This is the follow-up merged PR #237 said it was leaving for later.
The job context carries no deadline
apps/openant-cli/internal/server/server.go:655:context.WithCancelgives cancellation but no timeout. Repo-verified:server.gocontains exactlytwo
context.WithTimeoutcall sites, and neither covers the Python subprocess —The invoke path never applies the configured timeout
apps/openant-cli/internal/python/invoke_ctx.gocontains zero occurrences ofresolveInvokeTimeout(/usr/bin/grep -c resolveInvokeTimeout→0). It takes the caller's contextand hands it straight to
exec.CommandContext. The CLI path does the opposite —apps/openant-cli/internal/python/invoke.go:91:with
defaultInvokeTimeout = 30 * time.Minuteand theOPENANT_INVOKE_TIMEOUToverride documentedalongside it.
Complete site enumeration (CODE-TRACED, not executed)
All three server call sites pass a deadline-free context:
I have not executed a hung scan against a live server — that needs a wedged parser and a running
openant serve. The evidence here is the call graph, and I am labelling it as such.Operational consequence
server.go:221sizes the scan semaphore at four:and
:1096-1097holds a slot for the entire run:So an unbounded subprocess does not merely hang its own job — it removes 1 of 4 slots until someone
cancels it by hand. The CLI cannot get into this state.
Why this is not the "cancel-only by design" case
server.go:1541-1545is worth quoting in full, because read partially it looks like a ratificationof the missing deadline and it is the opposite:
"The parent ctx is cancel-only" is the justification for adding a local deadline, not an argument
that none is needed. The same pattern appears at
:1520for DNS. The maintainers' approach isevidently: leave the job context cancel-only, and bound each subprocess that could run away. The
Python invoke is the one that never got that treatment.
Prior art — this is the maintainers' own stated follow-up
Merged PR #237:
Both are merged. No issue tracks it — a search across all open and closed issues for
OPENANT_INVOKE_TIMEOUT/InvokeCtx/ webui-timeout returns nothing.Suggested fix
resolveInvokeTimeout()inInvokeCtx/InvokeCtxCapture, or wrap the three server callsites in
context.WithTimeout(ctx, resolveInvokeTimeout())— matching the local-bound patternalready used at
:1520and:1545.for a job-level deadline that would truncate legitimately long scans;
OPENANT_INVOKE_TIMEOUTalready exists precisely so an operator can raise it.
What I am not claiming
the consequence is code-traced, not executed.
Grounding curve (added 2026-08-22)
invoke_ctxskips timeoutad17f3f(PR #133)b8844d4(PR #235)a601f82(PR #237)b5019628(HEAD)Attribution: every server-side leg entered in one squashed commit — PR #235 (
b8844d4,"feat(webui): add a local web UI for the scan pipeline").
git log -- internal/server/server.goreturns exactly that one commit, so there has been no drift since. A fix belongs against #235.
PR #237 (
a601f82) addedresolveInvokeTimeoutandOPENANT_INVOKE_TIMEOUTtoinvoke.goonly —it never touched the server. Note the headline phrasing is only sayable from #237: before it there
was no env var to have no effect, and the honest statement would have been "the server path has no
deadline while the CLI is fixed at 30 minutes".