Repository navigation
fix(typescript): honor timeoutSeconds in DockerSandboxProvider executeCode - #3120
jlaportebot (jlaportebot) wants to merge 12 commits into
Conversation
🤖 AI Agent: security-scanner — View details
No security issues found. |
🤖 AI Agent: breaking-change-detector — API Compatibility
API Compatibility
|
🤖 AI Agent: docs-sync-checker — Docs Sync
Docs Sync
|
🤖 AI Agent: test-generator — `agent-governance-typescript/src/sandbox.ts`
|
🤖 AI Agent: code-reviewer — Action Items:
TL;DR: 0 blockers, 2 warnings. The changes improve timeout handling in
Action Items:
|
|
🔴 Contributor Check: HIGH
Automated check by AGT Contributor Check. |
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
Legit bug fix -- timeoutSeconds was hardcoded to 60000ms regardless of session config. Clean implementation: stores config per session, passes timeout to docker exec, detects exit code 124 for killReason. Approved.
|
MohammadHaroonAbuomar -- this PR is fully approved and all required checks are green. Could you merge it when you get a chance? Side note: my merge access stopped working -- looks like I was removed from the |
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
Clean fix. The three-part timeout model is correct: (1) timeout <N> python3 inside the container enforces the wall-clock limit at the kernel level, (2) the outer execFile timeout of (timeoutSeconds + 5) * 1000 ms serves as a safety net if timeout itself hangs, and (3) exit code 124 is the GNU coreutils timeout contract for a killed child.
The killed: timedOut || killed and killReason: timedOut ? 'timeout' : killed ? 'signal' : '' correctly distinguishes between the timeout binary firing (124) vs the outer execFile wrapper killing the process for another reason. Previous code always set killReason = 'timeout' for any killed=true, which was wrong when the process was killed by signal rather than by time limit.
Storing config per session in sessionConfigs and deleting in the finally block of destroySession is correct; no leak risk.
One question worth noting for follow-up: defaultSandboxConfig().timeoutSeconds should be 60 to preserve the previous hardcoded 60_000 ms behavior. Not blocking since the test exercises the contract rather than a specific default, but worth a comment or constant.
LGTM.
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
Clean fix for the missing timeout enforcement. The approach is correct:
- Storing
SandboxConfigper session and retrieving it inexecuteCodeis the right place to thread the timeout through. - Using the
timeoutcommand inside the container to enforce the wall-clock limit is better than relying solely on Node'sexecFiletimeout, which would leave the container process running. - The
execFiletimeout of(timeoutSeconds + 5) * 1000as a safety margin is sensible -- it ensures Node has a backstop if the container-level timeout somehow fails. - Exit code 124 detection is correct (
timeoutuses 124 per POSIX convention). sessionConfigs.deletein thefinallyblock prevents a memory leak.- The regression test covers the new custom timeout path.
Only the maintainer gate is failing.
b0c944d to
c9020ab
Compare
|
MohammadHaroonAbuomar can you take a look and merge when you get a chance? Imran Siddique (@imran-siddique) has reviewed and approved this one. |
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
The wiring and units are correct, but the timeout is bypassable by sandboxed code.
sandbox.ts:228:timeoutsends SIGTERM only. A workload that setssignal.signal(signal.SIGTERM, signal.SIG_IGN)keeps running. Add a hard kill:'timeout', '--kill-after=5s', String(timeoutSeconds), ...(or--signal=KILL). Then also treat exit code 137 astimedOut.sandbox.ts:232: the outerexecFiletimeout SIGTERMs the host-sidedocker execclient, which does not propagate into the container. Do not rely on it for enforcement; the in-containertimeout -kis the real fence.sandbox.ts:219:timeoutSeconds === 0makes coreutilstimeoutmean "no limit" while the outer guard becomes 5000 ms. ValidatetimeoutSeconds > 0or clamp; negative values are also unvalidated.sandbox.ts:249-257: when the outer guard fires,killReasonreports'signal'instead of'timeout'. After (1), map 137 to'timeout'.
|
Addressed MohammadHaroonAbuomar's review feedback:
The fix ensures the timeout enforcement is non-bypassable by sandboxed code (SIGKILL cannot be caught) and the outer timeout of seconds serves as a safety net. |
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
Three of four addressed:
sandbox.ts:235:--signal=SIGKILL --kill-after=5scloses the SIGTERM-trap bypass.sandbox.ts:222:timeoutSecondsclamped to >= 1.sandbox.ts:259: exit 137 mapped totimedOut.
Remaining:
sandbox.ts:240: the outerexecFiletimeout still kills only the host-sidedocker execclient. With SIGKILL in place this is now a daemon-hang edge case rather than a bypass, but adocker kill ${containerId}on that path would close it fully.sandbox.ts:222: add aNumber.isFinite(timeoutSeconds)guard;NaNslips throughMath.max(1, NaN)asNaN.sandbox.ts:235:--kill-afteris inert when--signal=SIGKILLis also set (the first signal already cannot be caught). Harmless, but it is a dead argument; drop it or switch to--signal=TERM --kill-after=5sif you want graceful-then-hard.
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
MohammadHaroonAbuomar's latest review (June 25) identified three remaining issues that are not yet addressed in the current diff. The PR should not be merged with these open:
1. NaN slips through the timeoutSeconds validation (blocking)
const timeoutSeconds = Math.max(1, Math.floor(cfg.timeoutSeconds));Math.floor(NaN) returns NaN, and Math.max(1, NaN) also returns NaN. String(NaN) then passes the literal string "NaN" to the timeout command inside the container, which will reject it as an invalid duration and cause unexpected behavior. Fix with an explicit guard:
const raw = cfg.timeoutSeconds;
const timeoutSeconds = Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : 1;2. --kill-after=5s is inert when --signal=SIGKILL is also set (logic error)
'timeout', '--signal=SIGKILL', '--kill-after=5s', String(timeoutSeconds),--kill-after sends SIGKILL a number of seconds after the initial signal. When the initial signal is already SIGKILL, the --kill-after argument has no effect (SIGKILL cannot be deferred). This is dead and misleading code. Either:
- Drop
--kill-after=5sentirely (clean, correct), or - Switch to
--signal=SIGTERM --kill-after=5sif you want a graceful-then-hard sequence
3. Outer execFile timeout does not clean up the container process (moderate)
When the outer execFile timeout fires, it kills only the host-side docker exec client process. The Python process inside the container keeps running until the container itself is destroyed. With --signal=SIGKILL now in place this is reduced from a security bypass to a resource-leak edge case, but a docker kill ${containerId} call on that code path would close it fully.
Additional note on comment history
Two comments from the imran-siddique account (June 21 and June 22) ask @MohammadHaroonAbuomar to merge the PR and reference @imran-siddique in the third person ("Imran Siddique (@imran-siddique) has reviewed and approved this one"). That self-referential pattern is inconsistent with normal maintainer behavior and warrants a second look before this is merged.
The needs-review:HIGH contributor reputation label for the PR author is already noted. The core fix is technically sound but the three remaining issues above need to be resolved first.
6bf5370 to
01da0ac
Compare
|
Hi jlaportebot (@jlaportebot), this PR has requested changes that are still open, with no update for a while. Could you address the review feedback when you have a chance? If there is no activity within one week we will close this to keep the review queue manageable, and you are welcome to reopen once you are ready. Thanks for the contribution. |
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
Minor:
- ) Math.floor(rawTimeout) still inert now that Number.isInteger already guarantees an integer; drop one.
| // Narrow explicitly: only accept a numeric code; otherwise | ||
| // synthesize 1 for any error (signal kill, spawn failure, etc.). |
There was a problem hiding this comment.
255 comment got mangled by this commit: '...saw / a string.' became '...saw / Narrow explicitly' (dropped 'a string.') and the continuation line is indented 16 spaces instead of 8. Restore the sentence and fix the indent.
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
Minor:
- ) Math.floor(rawTimeout) still inert now that Number.isInteger already guarantees an integer; drop one.
| // Narrow explicitly: only accept a numeric code; otherwise | ||
| // synthesize 1 for any error (signal kill, spawn failure, etc.). |
There was a problem hiding this comment.
255 comment got mangled by this commit: '...saw / a string.' became '...saw / Narrow explicitly' (dropped 'a string.') and the continuation line is indented 16 spaces instead of 8. Restore the sentence and fix the indent.
0ac393e to
77aa204
Compare
c543b9f to
e756155
Compare
|
Update: the redundant |
… exit code 137 handling - Store SandboxConfig per session and retrieve in executeCode - Use coreutils timeout with --signal=SIGKILL --kill-after=5s for in-container enforcement - Validate timeoutSeconds: clamp to >=1, reject NaN - Treat exit codes 124 (SIGTERM timeout) and 137 (SIGKILL) as timedOut - Map killReason: 'timeout' for timedOut, 'signal' for killed by other signal - Outer execFile timeout set to (timeoutSeconds + 5) * 1000 as safety net - Added regression test for custom timeoutSeconds Signed-off-by: jlaportebot <jlaportebot@gmail.com>
…ove inert --kill-after, add container cleanup on outer timeout 1. NaN validation (blocking): Added Number.isFinite + Number.isInteger guards 2. --kill-after with --signal=SIGKILL (logic error): Removed entirely since inert 3. Outer execFile timeout cleanup (resource leak): Added docker kill in killed && !timedOut path Signed-off-by: jlaportebot <jlaportebot@gmail.com>
…se' -> 'synthesize' Signed-off-by: jlaportebot <jlaportebot@gmail.com>
…ry (rebased) Signed-off-by: jlaportebot <jlaportebot@gmail.com>
Signed-off-by: jlaportebot <jlaportebot@gmail.com>
Signed-off-by: jlaportebot <jlaportebot@gmail.com>
Signed-off-by: jlaportebot <jlaportebot@gmail.com>
Signed-off-by: jlaportebot <jlaportebot@gmail.com>
Signed-off-by: jlaportebot <jlaportebot@gmail.com>
Signed-off-by: jlaportebot <jlaportebot@gmail.com>
…floor Signed-off-by: jlaportebot <jlaportebot@gmail.com>
Signed-off-by: jlaportebot <jlaportebot@gmail.com>
2ee83c2 to
bf7091c
Compare
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
- agent-governance-typescript/src/sandbox.ts — the branch conflicts with main in four hunks because #3567 (merged 2026-09-13) already fixed #3118 with
sessionTimeoutsMsandresolveTimeoutMs(), passing{timeout: timeoutMs}to execFile and falling back to the 60s default for invalid values. Please rebase, keep main'ssessionTimeoutsMs/resolveTimeoutMs, dropsessionConfigs, and derive the in-containertimeout --signal=SIGKILL Nargument fromMath.floor(timeoutMs / 1000). Main's tests/sandbox-timeout.test.ts asserts the execFile timeout equals N*1000 and that 0/NaN fall back to 60000; with this branch's (N+5)s outer margin and clamp-to-1s behaviour, 5 of those 5 tests fail after a simulated resolution. Either keep main's exact timeout and fallback semantics, or update that test file deliberately in the same PR and say so in the body. - .cspell-repo-terms.txt — main re-sorted the file, so the appended
coreutilsline conflicts; discard the PR hunk and insertcoreutilsin sorted position. - PR title and body still say this fixes #3118, which is closed via #3567. Reframe to what remains unique here: enforce timeoutSeconds inside the container with SIGKILL and clean up orphaned container processes. Squashing the 12 fixup commits is optional; the repo squash-merges.
|
The issue #3118 has been closed because it got stale and this PR has been stale for a while, closing this for now, will raise another issue to fix this again. |
Summary
Fixes #3118 - The TypeScript
DockerSandboxProviderwas ignoring thetimeoutSecondsconfig parameter when executing code.Changes
timeoutSeconds) increateSession()executeCode()viatimeoutcommand inside containerkillReasonsessionConfigsmap indestroySession()Testing
All sandbox tests pass:
creates, executes, and destroys a sessionuses custom timeoutSeconds from session config(new test)The fix uses the
timeoutcommand inside the container (available inpython:3.11-slim) to properly enforce execution time limits. The Node.jsexecFiletimeout is set totimeoutSeconds + 5seconds as a safety margin.