88// unknown execution.
99//
1010// The journey drives exactly that shape: ONE execution with TWO approval
11- // gates. The first approval is granted late in its window (~3.5 min), so the
12- // second pause's window reaches well past the sandbox's 5-minute mark. The
13- // second approval arrives ~5.75 min after execution start — inside its OWN
14- // advertised window, but past the old absolute deadline. Deliberately slow
15- // (~6 min): the elapsed time IS the subject under test. A single-pause
16- // variant cannot express this cross-target — hosts that advertise a
17- // 4-minute window would expire it legitimately before the sandbox clock
18- // even matters.
11+ // gates. The first approval is granted late (70% of the sandbox budget in),
12+ // so the second pause's window reaches well past the budget. The second
13+ // approval arrives at ~115% of the budget after execution start — inside its
14+ // OWN advertised window, but past the old absolute deadline. The subject is
15+ // that RATIO, not any absolute duration, so the delays scale off the budget
16+ // the target was booted with: selfhost boots with a seconds-long
17+ // EXECUTOR_SANDBOX_TIMEOUT_MS (setup/sandbox-timeout.ts) and proves the race
18+ // in ~25s; a target on the production 5-minute budget runs the original
19+ // ~6-minute journey (the elapsed time IS the subject — nothing is mocked). A
20+ // single-pause variant cannot express this cross-target — hosts that
21+ // advertise a 4-minute window would expire it legitimately before the
22+ // sandbox clock even matters.
1923//
2024// The gate is `policies.create`'s own `requiresApproval` annotation
2125// (hermetic, same device as policy-tool-approval.test.ts); both approvals
@@ -29,15 +33,23 @@ import { composePluginApi } from "@executor-js/api/server";
2933import { scenario } from "../src/scenario" ;
3034import { Api , Mcp , Target } from "../src/services" ;
3135import { configuredMcpPausedSessionIdleTimeoutMs } from "../setup/mcp-session-timeouts" ;
36+ import { configuredSandboxTimeoutMs } from "../setup/sandbox-timeout" ;
3237
3338const coreApi = composePluginApi ( [ ] as const ) ;
3439
35- // Grant the first approval at 3.5 min — late but inside its 4-minute window.
36- // The second pause then opens a fresh window reaching ~7.5 min.
37- const FIRST_APPROVAL_DELAY_MS = 3.5 * 60_000 ;
38- // Grant the second approval 2.25 min later: ~5.75 min after execution start,
39- // past the sandbox's 5-minute budget but inside the second window.
40- const SECOND_APPROVAL_DELAY_MS = 2.25 * 60_000 ;
40+ const SANDBOX_BUDGET_MS = configuredSandboxTimeoutMs ( ) ;
41+
42+ // Grant the first approval at 70% of the budget — late but inside its window
43+ // (was 3.5 of 5 min). The second pause then opens a fresh window reaching
44+ // past the budget.
45+ const FIRST_APPROVAL_DELAY_MS = 0.7 * SANDBOX_BUDGET_MS ;
46+ // Grant the second approval 45% of the budget later: ~115% of the budget
47+ // after execution start, past the sandbox clock but inside the second window
48+ // (was 2.25 of 5 min → ~5.75 min total).
49+ const SECOND_APPROVAL_DELAY_MS = 0.45 * SANDBOX_BUDGET_MS ;
50+ // The whole journey plus scheduling slack, for the idle-window guard and the
51+ // vitest timeout.
52+ const JOURNEY_MS = FIRST_APPROVAL_DELAY_MS + SECOND_APPROVAL_DELAY_MS ;
4153
4254/** Sandbox code that creates two policies through the approval-gated core
4355 * tool. Patterns are unique-per-run and match no real tool, so the rules are
@@ -56,19 +68,22 @@ const second = await tools.executor.coreTools.policies.create({
5668return JSON.stringify({ first: first.ok, second: second.ok });
5769` ;
5870
59- // The journey spans ~6 real minutes of paused waiting, so the host must keep
60- // the paused session alive that long. The suite's default e2e override shrinks
71+ // The journey spans the whole paused waiting time , so the host must keep the
72+ // paused session alive that long. The suite's default e2e override shrinks
6173// the paused-session idle teardown to seconds (to keep teardown tests fast),
6274// which would evict the session mid-scenario for reasons unrelated to the
63- // clock under test — require the production-like window instead.
75+ // clock under test — require a window that outlasts the journey instead.
76+ // With a shrunken sandbox budget the journey shrinks too, so even the short
77+ // e2e idle window can suffice; the guard compares the two rather than
78+ // hardcoding either.
6479const PAUSED_IDLE_WINDOW_TOO_SHORT =
65- configuredMcpPausedSessionIdleTimeoutMs ( ) < 8 * 60_000
66- ? `the target's paused-session idle teardown (${ configuredMcpPausedSessionIdleTimeoutMs ( ) } ms) evicts the session before this ~6-minute journey completes; boot the target with MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS >= 480000 to run it`
80+ configuredMcpPausedSessionIdleTimeoutMs ( ) < JOURNEY_MS + 60_000
81+ ? `the target's paused-session idle teardown (${ configuredMcpPausedSessionIdleTimeoutMs ( ) } ms) evicts the session before this ${ Math . round ( JOURNEY_MS / 1000 ) } s journey completes; boot the target with MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS >= ${ JOURNEY_MS + 60_000 } or a smaller E2E_SANDBOX_TIMEOUT_MS to run it`
6782 : undefined ;
6883
6984scenario (
7085 "MCP · chained approvals granted within their windows survive the sandbox clock" ,
71- { timeout : 480_000 , skip : PAUSED_IDLE_WINDOW_TOO_SHORT } ,
86+ { timeout : Math . max ( 120_000 , JOURNEY_MS + 120_000 ) , skip : PAUSED_IDLE_WINDOW_TOO_SHORT } ,
7287 Effect . gen ( function * ( ) {
7388 const target = yield * Target ;
7489 const apiSurface = yield * Api ;
0 commit comments