@stigmer/runner 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/attach-session.d.ts +34 -0
  3. package/dist/activities/attach-session.js +95 -0
  4. package/dist/activities/attach-session.js.map +1 -0
  5. package/dist/activities/discover-mcp-server.d.ts +9 -0
  6. package/dist/activities/discover-mcp-server.js +6 -1
  7. package/dist/activities/discover-mcp-server.js.map +1 -1
  8. package/dist/activities/execute-cursor/connect-backfill.d.ts +2 -1
  9. package/dist/activities/execute-cursor/connect-backfill.js +2 -2
  10. package/dist/activities/execute-cursor/connect-backfill.js.map +1 -1
  11. package/dist/activities/execute-cursor/fetch-interceptor.js +31 -0
  12. package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
  13. package/dist/activities/execute-cursor/index.js +70 -3
  14. package/dist/activities/execute-cursor/index.js.map +1 -1
  15. package/dist/activities/execute-cursor/mcp-resolver.d.ts +7 -1
  16. package/dist/activities/execute-cursor/mcp-resolver.js +15 -2
  17. package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
  18. package/dist/activities/execute-cursor/sdk-warmup.d.ts +44 -0
  19. package/dist/activities/execute-cursor/sdk-warmup.js +61 -0
  20. package/dist/activities/execute-cursor/sdk-warmup.js.map +1 -0
  21. package/dist/activities/execute-deep-agent/setup.js +48 -5
  22. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  23. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +9 -1
  24. package/dist/activities/execute-deep-agent/subagent-transformer.js +19 -5
  25. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  26. package/dist/client/stigmer-client.d.ts +13 -1
  27. package/dist/client/stigmer-client.js +40 -5
  28. package/dist/client/stigmer-client.js.map +1 -1
  29. package/dist/client/token-claims.d.ts +9 -6
  30. package/dist/client/token-claims.js +22 -6
  31. package/dist/client/token-claims.js.map +1 -1
  32. package/dist/main.d.ts +8 -1
  33. package/dist/main.js +122 -2
  34. package/dist/main.js.map +1 -1
  35. package/dist/middleware/index.d.ts +0 -1
  36. package/dist/middleware/index.js +0 -1
  37. package/dist/middleware/index.js.map +1 -1
  38. package/dist/otel-metrics.d.ts +10 -1
  39. package/dist/otel-metrics.js +22 -1
  40. package/dist/otel-metrics.js.map +1 -1
  41. package/dist/pool-member.d.ts +61 -0
  42. package/dist/pool-member.js +51 -0
  43. package/dist/pool-member.js.map +1 -0
  44. package/dist/runner-manager.d.ts +7 -0
  45. package/dist/runner-manager.js +39 -1
  46. package/dist/runner-manager.js.map +1 -1
  47. package/dist/runner.js +15 -1
  48. package/dist/runner.js.map +1 -1
  49. package/dist/shared/cold-start-timing.d.ts +89 -0
  50. package/dist/shared/cold-start-timing.js +166 -0
  51. package/dist/shared/cold-start-timing.js.map +1 -0
  52. package/dist/shared/connect-backfill.d.ts +2 -1
  53. package/dist/shared/connect-backfill.js +4 -2
  54. package/dist/shared/connect-backfill.js.map +1 -1
  55. package/dist/shared/mcp-manager.d.ts +5 -15
  56. package/dist/shared/mcp-manager.js +5 -32
  57. package/dist/shared/mcp-manager.js.map +1 -1
  58. package/dist/shared/mcp-resolver.d.ts +15 -5
  59. package/dist/shared/mcp-resolver.js +23 -6
  60. package/dist/shared/mcp-resolver.js.map +1 -1
  61. package/dist/shared/mcp-transport-guard.d.ts +53 -0
  62. package/dist/shared/mcp-transport-guard.js +70 -0
  63. package/dist/shared/mcp-transport-guard.js.map +1 -0
  64. package/dist/shared/model-registry.d.ts +4 -6
  65. package/dist/shared/model-registry.js +4 -6
  66. package/dist/shared/model-registry.js.map +1 -1
  67. package/dist/shared/tool-kind.js +6 -0
  68. package/dist/shared/tool-kind.js.map +1 -1
  69. package/dist/tools/index.d.ts +27 -0
  70. package/dist/tools/index.js +27 -0
  71. package/dist/tools/index.js.map +1 -0
  72. package/dist/tools/think-tool.js.map +1 -0
  73. package/dist/tools/url-guard.d.ts +64 -0
  74. package/dist/tools/url-guard.js +211 -0
  75. package/dist/tools/url-guard.js.map +1 -0
  76. package/dist/tools/web-fetch-tool.d.ts +53 -0
  77. package/dist/tools/web-fetch-tool.js +216 -0
  78. package/dist/tools/web-fetch-tool.js.map +1 -0
  79. package/package.json +4 -2
  80. package/src/__tests__/otel-metrics.test.ts +6 -0
  81. package/src/__tests__/pool-member.test.ts +76 -0
  82. package/src/activities/__tests__/attach-session.test.ts +133 -0
  83. package/src/activities/__tests__/discover-mcp-server.test.ts +34 -11
  84. package/src/activities/attach-session.ts +112 -0
  85. package/src/activities/discover-mcp-server.ts +19 -1
  86. package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +77 -0
  87. package/src/activities/execute-cursor/__tests__/sdk-warmup.test.ts +45 -0
  88. package/src/activities/execute-cursor/connect-backfill.ts +3 -0
  89. package/src/activities/execute-cursor/fetch-interceptor.ts +39 -0
  90. package/src/activities/execute-cursor/index.ts +72 -3
  91. package/src/activities/execute-cursor/mcp-resolver.ts +21 -2
  92. package/src/activities/execute-cursor/sdk-warmup.ts +68 -0
  93. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +24 -0
  94. package/src/activities/execute-deep-agent/setup.ts +48 -7
  95. package/src/activities/execute-deep-agent/subagent-transformer.ts +27 -2
  96. package/src/client/__tests__/stigmer-client.test.ts +77 -2
  97. package/src/client/__tests__/token-claims.test.ts +15 -1
  98. package/src/client/stigmer-client.ts +51 -6
  99. package/src/client/token-claims.ts +29 -6
  100. package/src/main.ts +139 -2
  101. package/src/middleware/index.ts +0 -1
  102. package/src/otel-metrics.ts +25 -1
  103. package/src/pool-member.ts +99 -0
  104. package/src/runner-manager.ts +52 -0
  105. package/src/runner.ts +15 -0
  106. package/src/shared/__tests__/cold-start-timing.test.ts +271 -0
  107. package/src/shared/__tests__/connect-backfill.test.ts +11 -11
  108. package/src/shared/__tests__/mcp-manager.test.ts +2 -73
  109. package/src/shared/__tests__/mcp-resolver.test.ts +108 -0
  110. package/src/shared/__tests__/mcp-transport-guard.test.ts +71 -0
  111. package/src/shared/__tests__/model-registry.test.ts +6 -4
  112. package/src/shared/cold-start-timing.ts +197 -0
  113. package/src/shared/connect-backfill.ts +5 -1
  114. package/src/shared/mcp-manager.ts +4 -41
  115. package/src/shared/mcp-resolver.ts +29 -6
  116. package/src/shared/mcp-transport-guard.ts +81 -0
  117. package/src/shared/model-registry.ts +4 -6
  118. package/src/shared/tool-kind.ts +6 -0
  119. package/src/tools/__tests__/url-guard.test.ts +137 -0
  120. package/src/tools/__tests__/web-fetch-tool.test.ts +193 -0
  121. package/src/tools/index.ts +28 -0
  122. package/src/tools/url-guard.ts +232 -0
  123. package/src/tools/web-fetch-tool.ts +275 -0
  124. package/dist/middleware/think-tool.js.map +0 -1
  125. /package/dist/{middleware → tools}/think-tool.d.ts +0 -0
  126. /package/dist/{middleware → tools}/think-tool.js +0 -0
  127. /package/src/{middleware → tools}/__tests__/think-tool.test.ts +0 -0
  128. /package/src/{middleware → tools}/think-tool.ts +0 -0
@@ -45,6 +45,7 @@ import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_p
45
45
  import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
46
46
  import { MessageAccumulator, cancelInProgressSubAgentProtos, collapseRedundantToolCallTwins } from "./message-translator.js";
47
47
  import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "../../shared/status.js";
48
+ import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
48
49
  import { readContextBridge } from "../../shared/context-bridge.js";
49
50
  import { readSenderIdentity } from "../../shared/sender-identity.js";
50
51
  import { readSessionContext } from "../../shared/session-context.js";
@@ -57,6 +58,7 @@ import { TodoTracker } from "./todo-tracker.js";
57
58
  import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/streaming-scheduler.js";
58
59
  import { createCursorEventRecorder } from "./cursor-event-recorder.js";
59
60
  import { resolveMcpServers, toCursorMcpConfig, validateMcpServerEnv } from "./mcp-resolver.js";
61
+ import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
60
62
  import {
61
63
  injectDatastoreAttachment,
62
64
  synthesizeDatastoreAttachment,
@@ -189,6 +191,12 @@ async function executeCursorInner(
189
191
  startedAt: utcTimestamp(),
190
192
  });
191
193
 
194
+ // Cold-start timeline of this turn's setup (warm-agent-surfaces Phase 0):
195
+ // one mark after each phase, emitted as a single structured log line once
196
+ // the Cursor agent is resolved (early returns skip it — partial setups are
197
+ // not comparable cold-start samples).
198
+ const setupTiming = new TimingRecorder();
199
+
192
200
  // Artifact storage for offloading oversized tool outputs (screenshots, giant
193
201
  // dumps) out of the persisted status, and for publishing the plan artifact.
194
202
  // Resolved once here so it is available to EVERY persist below. Best-effort via
@@ -198,6 +206,7 @@ async function executeCursorInner(
198
206
  // the aggregate size cap) and flips capture mode off (deny-gate fallback).
199
207
  const artifactStorage: ArtifactStorage | undefined =
200
208
  await resolveUsableArtifactStorage(loadArtifactStorageConfig(config), { executionId });
209
+ setupTiming.mark("resolve_artifact_storage");
201
210
  const statusOffload = artifactStorage
202
211
  ? { artifactStorage, executionId }
203
212
  : undefined;
@@ -289,17 +298,20 @@ async function executeCursorInner(
289
298
  const execution = await client.getExecution(executionId);
290
299
  const spec = execution.spec!;
291
300
  sessionId = spec.sessionId;
301
+ setupTiming.mark("fetch_execution");
292
302
 
293
303
  // Phase 2: Load session and resolve full agent blueprint
294
304
  await reportSetupProgress(client, executionId, "Resolving agent blueprint");
295
305
  session = await client.getSession(sessionId);
296
306
  const blueprint = await resolveBlueprint(client, session, config.workspaceRootDir);
307
+ setupTiming.mark("resolve_blueprint");
297
308
 
298
309
  // Phase 2b: Resolve execution environment (MCP server credentials)
299
310
  heartbeatPhase = "resolving_environment";
300
311
  await reportSetupProgress(client, executionId, "Resolving environment");
301
312
  const { envVars, secretKeys } = await resolveExecutionEnv(client, executionId);
302
313
  heartbeat();
314
+ setupTiming.mark("resolve_environment");
303
315
 
304
316
  // Phase 2c: Provision the workspace (clone git repos / mount local paths)
305
317
  // so the LOCAL Cursor agent operates on the actual repo. Cursor previously
@@ -314,6 +326,7 @@ async function executeCursorInner(
314
326
  );
315
327
  blueprint.workspaceDirs = workspaceProvision.workspaceDirs;
316
328
  heartbeat();
329
+ setupTiming.mark("provision_workspace");
317
330
 
318
331
  // Git write-back: pushes the session's APPROVED tree to the session
319
332
  // branch (stigmer/<session-id>) and keeps one PR open — the same
@@ -404,6 +417,7 @@ async function executeCursorInner(
404
417
  }
405
418
  }
406
419
  heartbeat();
420
+ setupTiming.mark("acquire_workspace_lock");
407
421
 
408
422
  // Set OTel baggage so downstream calls carry execution context.
409
423
  try {
@@ -577,17 +591,20 @@ async function executeCursorInner(
577
591
 
578
592
  // Phase 4: Resolve MCP servers with approval policies
579
593
  await reportSetupProgress(client, executionId, "Resolving MCP servers");
594
+ const transportPosture = resolveMcpTransportPosture(config.mode);
580
595
  let mcpResolution = await resolveMcpServers(
581
- client, blueprint.mergedMcpServerUsages, envVars,
596
+ client, blueprint.mergedMcpServerUsages, envVars, transportPosture,
582
597
  );
598
+ setupTiming.mark("resolve_mcp_servers");
583
599
 
584
600
  // Phase 4a: Connect backfill for undiscovered MCP servers
585
601
  heartbeatPhase = "resolving_mcp_servers";
586
602
  const sessionOrg = session.metadata?.org ?? "";
587
603
  mcpResolution = await backfillMcpServersIfNeeded(
588
604
  client, mcpResolution, blueprint.mergedMcpServerUsages, envVars, sessionOrg,
589
- heartbeat, secretKeys,
605
+ transportPosture, heartbeat, secretKeys,
590
606
  );
607
+ setupTiming.mark("backfill_mcp");
591
608
 
592
609
  // Phase 4a2: Synthesize the datastore records attachment (T05).
593
610
  // Deliberately AFTER resolve + backfill: the attachment has no
@@ -657,6 +674,7 @@ async function executeCursorInner(
657
674
  primaryWorkspaceDir,
658
675
  });
659
676
  heartbeat();
677
+ setupTiming.mark("resolve_skills");
660
678
 
661
679
  // Phase 5b: Resolve attachments (fail-hard — explicit user inputs; see
662
680
  // attachment-resolver.ts). Downloads by storage key through the same
@@ -668,6 +686,7 @@ async function executeCursorInner(
668
686
  storage: artifactStorage,
669
687
  });
670
688
  const attachmentPaths = attachmentResults.map((a) => a.relativePath);
689
+ setupTiming.mark("resolve_attachments");
671
690
 
672
691
  // Phase 5b3: Exact-apply approved whole-file writes (HITL "what you approve
673
692
  // is what gets applied"). The Cursor deny-only harness reinvokes the model,
@@ -797,6 +816,7 @@ async function executeCursorInner(
797
816
  stopDenialWatcher = watchDenialLedger(hitlDir, () => {
798
817
  turnState.denialLedgerDirty = true;
799
818
  });
819
+ setupTiming.mark("install_hitl_gate");
800
820
 
801
821
  // Mid-run live capture (DD-32 / DD-33): choose the progress substrate for this
802
822
  // turn's workspace shape ONCE (git / non-git CAS / hybrid). It owns its own
@@ -815,6 +835,7 @@ async function executeCursorInner(
815
835
 
816
836
  // Phase 5d: Ensure model pricing registry is populated before validation
817
837
  await ensurePricingLoaded();
838
+ setupTiming.mark("load_pricing");
818
839
 
819
840
  // Phase 6: Validate model selection
820
841
  const requestedModel = spec.executionConfig?.modelName || "default";
@@ -887,6 +908,12 @@ async function executeCursorInner(
887
908
  // the activity returns rather than throws, and maximumAttempts is 1).
888
909
  heartbeatPhase = "resolving_agent";
889
910
  const resolveTimeoutSeconds = Math.round(config.agentResolveTimeoutMs / 1000);
911
+ // Close the span since load_pricing here so the resolve_agent segment
912
+ // below measures the SDK Agent.create/resume call alone, not the
913
+ // progress-report gRPC + options assembly above (issue #209: resolve_agent
914
+ // is the largest user-visible setup segment; this split keeps its
915
+ // historical meaning — the SDK call was already 98%+ of it).
916
+ setupTiming.mark("prepare_agent");
890
917
  let resolution: AgentResolution = await resolveAgentWithTransportRecovery({
891
918
  harnessStateId: threadId,
892
919
  createOptions,
@@ -909,6 +936,17 @@ async function executeCursorInner(
909
936
  `agentId=${resolution.agentId}, resumed=${resolution.resumed}` +
910
937
  (resolution.resumeFailureDetail ? `, failureDetail=${resolution.resumeFailureDetail}` : ""),
911
938
  );
939
+ setupTiming.mark("resolve_agent");
940
+ emitTimingLog("execution_setup", {
941
+ execution_id: executionId,
942
+ session_id: sessionId,
943
+ harness: "cursor",
944
+ agent_resumed: resolution.resumed,
945
+ cursor_mode: agentMode,
946
+ mcp_server_count: blueprint.mergedMcpServerUsages.length,
947
+ skill_count: blueprint.mergedSkillRefs.length,
948
+ workspace_entry_count: session.spec?.workspaceEntries?.length ?? 0,
949
+ }, setupTiming);
912
950
 
913
951
  errorContext = { model: validatedModel, mode: agentMode, agentId: resolution.agentId };
914
952
 
@@ -1068,12 +1106,43 @@ async function executeCursorInner(
1068
1106
  // (e.g. older SDK), the warning is harmless — ignore.
1069
1107
  }
1070
1108
 
1109
+ // Issue #209 forensics: the SDK acquires the local executor — the piece
1110
+ // that actually spawns stdio MCP servers — inside send(), AFTER the
1111
+ // execution_setup timeline above has already been emitted. This one-shot
1112
+ // timeline makes that previously invisible window measurable:
1113
+ // `send_returned` covers the send() call itself, `first_delta` the wait
1114
+ // until the SDK's first delta. Primary send only — the recovery retries
1115
+ // below rebuild the agent and would skew the user-perceived turn start
1116
+ // this measures. No delta (immediate pause/failure) → no line.
1117
+ const turnStartTiming = new TimingRecorder();
1118
+ let turnFirstEventEmitted = false;
1119
+ const primaryOnDelta = makeCursorTurnOnDelta(onDeltaDeps);
1120
+
1071
1121
  // The stall watchdog is armed inside consumeCursorTurnStream (it needs the
1072
1122
  // run to cancel), stored on turnState.stallWatchdog so this shared onDelta can
1073
1123
  // reset it and the activity's finally can stop it as a backstop.
1074
1124
  const run = await resolution.agent.send(effectivePrompt, {
1075
- onDelta: makeCursorTurnOnDelta(onDeltaDeps),
1125
+ onDelta: (event) => {
1126
+ if (!turnFirstEventEmitted) {
1127
+ turnFirstEventEmitted = true;
1128
+ turnStartTiming.mark("first_delta");
1129
+ emitTimingLog("turn_first_event", {
1130
+ execution_id: executionId,
1131
+ session_id: sessionId,
1132
+ harness: "cursor",
1133
+ agent_resumed: resolution.resumed,
1134
+ mcp_server_count: blueprint.mergedMcpServerUsages.length,
1135
+ }, turnStartTiming);
1136
+ }
1137
+ primaryOnDelta(event);
1138
+ },
1076
1139
  });
1140
+ // Normally send() resolves before any delta arrives, making send_returned
1141
+ // the first segment; if a delta beat it, the line is already emitted and
1142
+ // adding a mark now would be meaningless.
1143
+ if (!turnFirstEventEmitted) {
1144
+ turnStartTiming.mark("send_returned");
1145
+ }
1077
1146
 
1078
1147
  // Everything at an index >= this was produced by THIS turn's stream — the
1079
1148
  // positional turn boundary the approved-command provenance (DD-28) scopes
@@ -18,6 +18,11 @@ import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1
18
18
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
19
19
  import type { ToolApprovalPolicy } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
20
20
  import type { StigmerClient } from "../../client/stigmer-client.js";
21
+ import {
22
+ assertTransportAllowed,
23
+ McpTransportError,
24
+ type McpTransportPosture,
25
+ } from "../../shared/mcp-transport-guard.js";
21
26
  import {
22
27
  resolveHeaders,
23
28
  resolvePlaceholders,
@@ -85,11 +90,17 @@ export interface McpResolutionResult {
85
90
  * 2. Extract connection config (stdio or http) from McpServerSpec
86
91
  * 3. Extract approval policies from McpServerStatus + McpServerSpec
87
92
  * 4. Transform into Cursor SDK's mcpServers config
93
+ *
94
+ * @param transportPosture Whether stdio servers may run here (derive via
95
+ * resolveMcpTransportPosture(config.mode)). A stdio server under a
96
+ * forbidding posture throws {@link McpTransportError} and fails the
97
+ * whole resolution — never degraded to a skipped server.
88
98
  */
89
99
  export async function resolveMcpServers(
90
100
  client: StigmerClient,
91
101
  usages: McpServerUsage[],
92
- envVars: Record<string, string> = {},
102
+ envVars: Record<string, string>,
103
+ transportPosture: McpTransportPosture,
93
104
  ): Promise<McpResolutionResult> {
94
105
  const resolved: ResolvedMcpServer[] = [];
95
106
 
@@ -105,8 +116,16 @@ export async function resolveMcpServers(
105
116
  ref.slug,
106
117
  );
107
118
  const server = mcpServerToResolved(mcpServer, ref.slug, serverEnv);
108
- if (server) resolved.push(server);
119
+ if (server) {
120
+ assertTransportAllowed(server.slug, server.connectionType, transportPosture);
121
+ resolved.push(server);
122
+ }
109
123
  } catch (err) {
124
+ if (err instanceof McpTransportError) {
125
+ // Policy rejection, not a resolution hiccup: swallowing it here
126
+ // would mean the agent silently loses tools. Fail the execution.
127
+ throw err;
128
+ }
110
129
  if (err instanceof PlaceholderResolutionError) {
111
130
  console.error(
112
131
  `MCP server ${ref.org}/${ref.slug}: ${err.message}`,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Cursor SDK state-store warm-up for pool members.
3
+ *
4
+ * The first `Agent.create` in a pod pays the SDK's platform construction:
5
+ * four SQLite-backed stores (run/checkpoint/event stores + notifier) whose
6
+ * first use loads the sqlite native binding and compiles the schema
7
+ * migrations. The 2026-07-30 instrumented baseline (issue #209) measured
8
+ * this at ~1.2s of the ~2.4s `resolve_agent` segment, with a 624ms floor —
9
+ * roughly half the cost is per-process warm-up, not per-session work.
10
+ *
11
+ * Pool members idle between boot and claim, so constructing one throwaway
12
+ * platform there moves that per-process cost off the user-visible path.
13
+ * This respects the pool's pre-warm boundary (warm-agent-surfaces DD-C:
14
+ * image caches only, nothing per-agent): the throwaway store is
15
+ * org-agnostic, credential-free, and keyed to a synthetic workspace ref.
16
+ *
17
+ * Deliberately NOT run on session-mode boots: those pods receive their
18
+ * execution the moment the worker polls, so a concurrent warm-up would
19
+ * compete with the real `Agent.create` for the same CPU instead of running
20
+ * in idle time.
21
+ */
22
+
23
+ import { mkdtempSync } from "node:fs";
24
+ import { tmpdir } from "node:os";
25
+ import { join } from "node:path";
26
+
27
+ export interface SdkWarmupResult {
28
+ readonly warmed: boolean;
29
+ readonly durationMs: number;
30
+ readonly error?: string;
31
+ }
32
+
33
+ /**
34
+ * Construct (and abandon) one SDK agent platform on a throwaway temp-dir
35
+ * state root, forcing the sqlite native binding + store schema work to
36
+ * happen now instead of inside the first user-facing `Agent.create`.
37
+ *
38
+ * Total by construction — warm-up is an optimization and must never affect
39
+ * the member's ability to serve. The throwaway state root is a few KB on
40
+ * the pod's ephemeral disk and is intentionally NOT deleted: the SDK memoizes
41
+ * the platform (with open SQLite handles) for the process lifetime, and
42
+ * unlinking files under live handles buys nothing on a pod that vanishes
43
+ * with its disk anyway.
44
+ *
45
+ * The `@cursor/sdk` import is dynamic so this module never adds the SDK
46
+ * bundle to any boot path that doesn't already carry it — by the time the
47
+ * warm-up runs (after the worker starts polling), the activities have
48
+ * already loaded the SDK and the import resolves from the module cache.
49
+ */
50
+ export async function warmCursorSdkStateStores(): Promise<SdkWarmupResult> {
51
+ const startMs = performance.now();
52
+ try {
53
+ const stateRoot = mkdtempSync(join(tmpdir(), "cursor-sdk-warm-"));
54
+ const { createAgentPlatform } = await import("@cursor/sdk");
55
+ await createAgentPlatform({ workspaceRef: "stigmer-warm:boot", stateRoot });
56
+ return { warmed: true, durationMs: elapsed(startMs) };
57
+ } catch (err) {
58
+ return {
59
+ warmed: false,
60
+ durationMs: elapsed(startMs),
61
+ error: err instanceof Error ? err.message : String(err),
62
+ };
63
+ }
64
+ }
65
+
66
+ function elapsed(startMs: number): number {
67
+ return Math.round((performance.now() - startMs) * 10) / 10;
68
+ }
@@ -121,6 +121,14 @@ describe("createBuiltinSubagents", () => {
121
121
  }
122
122
  });
123
123
 
124
+ it("gives web_fetch to general-purpose only, when a guard posture is supplied", () => {
125
+ const result = createBuiltinSubagents(true, [], "strict");
126
+ for (const sa of result) {
127
+ const hasWebFetch = sa.tools.some((t) => t.name === "web_fetch");
128
+ expect(hasWebFetch).toBe(sa.name === "general-purpose");
129
+ }
130
+ });
131
+
124
132
  it("built-in subagents have no model override", () => {
125
133
  const result = createBuiltinSubagents(true);
126
134
  for (const sa of result) {
@@ -140,6 +148,7 @@ describe("transformSingleSubagent", () => {
140
148
  parentMcpUsages: [] as McpServerUsage[],
141
149
  parentHasNativeThinking: true,
142
150
  parentModelName: "claude-sonnet-4-6",
151
+ webFetchPosture: "strict" as const,
143
152
  };
144
153
 
145
154
  beforeEach(() => {
@@ -202,6 +211,17 @@ describe("transformSingleSubagent", () => {
202
211
  expect(hasThinkTool).toBe(false);
203
212
  });
204
213
 
214
+ it("always injects web_fetch, regardless of thinking support", async () => {
215
+ for (const parentHasNativeThinking of [true, false]) {
216
+ const result = await transformSingleSubagent(mockSubAgentProto(), {
217
+ ...baseOpts,
218
+ parentHasNativeThinking,
219
+ });
220
+ const hasWebFetch = result!.tools.some((t) => t.name === "web_fetch");
221
+ expect(hasWebFetch).toBe(true);
222
+ }
223
+ });
224
+
205
225
  it("injects think tool when parent lacks native thinking", async () => {
206
226
  const proto = mockSubAgentProto();
207
227
  const result = await transformSingleSubagent(proto, {
@@ -433,6 +453,7 @@ describe("transformAndCompileSubagents", () => {
433
453
  approvalGate: null,
434
454
  parentModelName: "claude-sonnet-4-6",
435
455
  parentHasNativeThinking: true,
456
+ webFetchPosture: "strict" as const,
436
457
  costCap: undefined,
437
458
  };
438
459
 
@@ -639,6 +660,7 @@ describe("subagent-transformer edge cases", () => {
639
660
  parentMcpUsages: [],
640
661
  parentHasNativeThinking: true,
641
662
  parentModelName: "claude-sonnet-4-6",
663
+ webFetchPosture: "strict",
642
664
  });
643
665
 
644
666
  expect(result).not.toBeNull();
@@ -664,6 +686,7 @@ describe("subagent-transformer edge cases", () => {
664
686
  approvalGate: null,
665
687
  parentModelName: "claude-sonnet-4-6",
666
688
  parentHasNativeThinking: true,
689
+ webFetchPosture: "strict",
667
690
  costCap: undefined,
668
691
  } as Parameters<typeof transformAndCompileSubagents>[0]);
669
692
 
@@ -687,6 +710,7 @@ describe("subagent-transformer edge cases", () => {
687
710
  approvalGate: null,
688
711
  parentModelName: "claude-sonnet-4-6",
689
712
  parentHasNativeThinking: true,
713
+ webFetchPosture: "strict",
690
714
  costCap: undefined,
691
715
  } as Parameters<typeof transformAndCompileSubagents>[0]);
692
716
 
@@ -22,12 +22,14 @@ import type { DynamicStructuredTool } from "@langchain/core/tools";
22
22
 
23
23
  import type { Config } from "../../config.js";
24
24
  import type { StigmerClient } from "../../client/stigmer-client.js";
25
+ import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
25
26
  import { createCheckpointer } from "../../shared/checkpointer/factory.js";
26
27
  import { readContextBridge } from "../../shared/context-bridge.js";
27
28
  import { readSenderIdentity } from "../../shared/sender-identity.js";
28
29
  import { readSessionContext } from "../../shared/session-context.js";
29
30
  import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
30
31
  import { resolveMcpServers } from "../../shared/mcp-resolver.js";
32
+ import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
31
33
  import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
32
34
  import {
33
35
  formatDatastoresSection,
@@ -49,8 +51,9 @@ import { buildWorkspaceFileTree } from "../../shared/workspace/file-tree.js";
49
51
  import { reportSetupProgress } from "../../shared/status.js";
50
52
  import { resolveEnvironment, type EnvironmentResult } from "./environment.js";
51
53
  import { buildEnhancedSystemPrompt } from "./prompt-builder.js";
52
- import { buildMiddlewareStack, createThinkTool } from "../../middleware/index.js";
54
+ import { buildMiddlewareStack } from "../../middleware/index.js";
53
55
  import type { GracefulStopMiddleware } from "../../middleware/index.js";
56
+ import { createThinkTool, createWebFetchTool, resolveGuardPosture } from "../../tools/index.js";
54
57
  import type { ApprovalGateConfig } from "../../middleware/approval-gate.js";
55
58
  import { deriveExecutionFingerprintKey } from "../../shared/approval-fingerprint.js";
56
59
  import { getRunnerHitlMasterSecret } from "../../shared/fingerprint-secret.js";
@@ -194,12 +197,16 @@ export interface SetupDependencies {
194
197
  export async function performSetup(deps: SetupDependencies): Promise<SetupResult> {
195
198
  const { config, client, executionId, threadId } = deps;
196
199
  let mcpConnection: McpConnectionResult | null = null;
200
+ // Cold-start timeline of this setup (warm-agent-surfaces Phase 0): one mark
201
+ // after each phase, emitted as a single structured log line before return.
202
+ const timing = new TimingRecorder();
197
203
 
198
204
  try {
199
205
  // Step 1: Hydrate execution
200
206
  await reportSetupProgress(client, executionId, "Fetching execution…");
201
207
  const execution = await client.getExecution(executionId);
202
208
  console.log(`[setup] Execution fetched: agent_id=${execution.spec?.agentId}`);
209
+ timing.mark("fetch_execution");
203
210
 
204
211
  // Step 2: Resolve chain — execution → session → agentInstance → agent
205
212
  await reportSetupProgress(client, executionId, "Resolving agent…");
@@ -220,6 +227,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
220
227
  `[setup] Chain resolved: session=${sessionId}, ` +
221
228
  `agent=${agent.metadata!.name}`,
222
229
  );
230
+ timing.mark("resolve_chain");
223
231
 
224
232
  // Step 3: Resolve model
225
233
  const modelName = execution.spec!.executionConfig?.modelName
@@ -237,10 +245,12 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
237
245
  ? await ensureCheckpointDbPath(sessionId)
238
246
  : undefined,
239
247
  });
248
+ timing.mark("create_checkpointer");
240
249
 
241
250
  // Step 5: Resolve environment
242
251
  await reportSetupProgress(client, executionId, "Resolving environment…");
243
252
  const envResult: EnvironmentResult = await resolveEnvironment(client, executionId);
253
+ timing.mark("resolve_environment");
244
254
 
245
255
  // Step 6: Resolve a usable artifact store (shared with the Cursor harness so
246
256
  // both degrade identically). Returns `undefined` — never throws — when there
@@ -250,6 +260,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
250
260
  // disables offload, instead of flowing writes then crashing at the boundary.
251
261
  const artifactStorage: ArtifactStorage | undefined =
252
262
  await resolveUsableArtifactStorage(loadArtifactStorageConfig(config), { executionId });
263
+ timing.mark("resolve_artifact_storage");
253
264
 
254
265
  // Step 7: Provision workspace
255
266
  await reportSetupProgress(client, executionId, "Initializing workspace…");
@@ -259,6 +270,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
259
270
  envResult.mergedEnvVars,
260
271
  sessionId,
261
272
  );
273
+ timing.mark("provision_workspace");
262
274
 
263
275
  // Apply-then-review is the file-review model whenever there is a capture
264
276
  // SUBSTRATE (DD-21 D2, Slice 2b): file edits flow during the turn and are
@@ -325,9 +337,11 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
325
337
  let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
326
338
  if (mcpServerUsages.length > 0 || datastoreUsages.length > 0) {
327
339
  await reportSetupProgress(client, executionId, "Connecting tools…");
340
+ const transportPosture = resolveMcpTransportPosture(config.mode);
328
341
  resolvedMcpServers = await resolveMcpServers(
329
- client, mcpServerUsages, envResult.mergedEnvVars,
342
+ client, mcpServerUsages, envResult.mergedEnvVars, transportPosture,
330
343
  );
344
+ timing.mark("resolve_mcp_servers");
331
345
 
332
346
  const sessionOrg = session.metadata?.org ?? "";
333
347
  let backfilledServers = await backfillMcpServersIfNeeded(
@@ -336,6 +350,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
336
350
  mcpServerUsages,
337
351
  envResult.mergedEnvVars,
338
352
  sessionOrg,
353
+ transportPosture,
339
354
  undefined,
340
355
  envResult.secretKeys,
341
356
  );
@@ -359,11 +374,12 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
359
374
  }
360
375
  }
361
376
  resolvedMcpServers = { resolvedServers: backfilledServers };
377
+ timing.mark("backfill_mcp");
362
378
 
363
- mcpConnection = await connectMcpServers(
364
- resolvedMcpServers.resolvedServers,
365
- { isCloudMode: config.cloudModeEnabled },
366
- );
379
+ // connect_mcp covers the whole fan-out: stdio servers spawn npx/uvx
380
+ // subprocesses here, so on-demand package installs land in this span.
381
+ mcpConnection = await connectMcpServers(resolvedMcpServers.resolvedServers);
382
+ timing.mark("connect_mcp");
367
383
  }
368
384
 
369
385
  // Step 7b: Resolve and write skills
@@ -407,6 +423,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
407
423
  `[setup] Skills loaded: ${skills.length} total, prompt section ${skillsPromptSection.length} chars`,
408
424
  );
409
425
  }
426
+ timing.mark("resolve_skills");
410
427
  }
411
428
 
412
429
  // Step 7c: Inject attachments
@@ -417,6 +434,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
417
434
  storage: artifactStorage,
418
435
  isLocalMode: config.mode === "local",
419
436
  });
437
+ timing.mark("inject_attachments");
420
438
 
421
439
  // Step 8: Build enhanced system prompt
422
440
  const systemPrompt = buildEnhancedSystemPrompt({
@@ -449,6 +467,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
449
467
  headerScope: { executionId },
450
468
  timeoutMs: requestTimeoutMs,
451
469
  });
470
+ timing.mark("build_model");
452
471
 
453
472
  // Step 10: Build middleware stack
454
473
  await ensurePricingLoaded();
@@ -563,11 +582,22 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
563
582
  otelSpans: { toolServerMap },
564
583
  approvalGate: approvalGateConfig,
565
584
  });
585
+ timing.mark("build_middleware");
566
586
 
567
- // Step 11: Build tools list (MCP tools + think tool)
587
+ // Step 11: Build tools list (MCP tools + native built-in tools).
588
+ //
589
+ // web_fetch is always-on for every native run — the same posture as the
590
+ // Cursor harness, whose WebFetch is a CLI built-in that no configuration
591
+ // removes (issue #214 parity). It is auto-approved by design (see
592
+ // toolApprovalCategory in shared/tool-kind.ts), so its URL guard posture
593
+ // — strict on managed cloud runners, relaxed on user-owned machines — is
594
+ // the entire safety boundary. Derived from config.mode, NOT the
595
+ // cloudModeEnabled Cursor feature flag.
596
+ const webFetchPosture = resolveGuardPosture(config.mode);
568
597
  const tools = [
569
598
  ...(mcpConnection?.tools as DynamicStructuredTool[] ?? []),
570
599
  createThinkTool(),
600
+ createWebFetchTool({ posture: webFetchPosture }),
571
601
  ];
572
602
 
573
603
  // Step 11b: Transform and compile subagents
@@ -599,6 +629,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
599
629
  casObserver,
600
630
  parentModelName: modelName,
601
631
  parentHasNativeThinking: _modelHasNativeThinking(modelName),
632
+ webFetchPosture,
602
633
  costCap: costCapMiddleware ?? undefined,
603
634
  modelFactory: async (m: string) =>
604
635
  (await buildChatModel({
@@ -611,6 +642,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
611
642
  // backends too (undefined in plan mode; see buildShellEnv above).
612
643
  shellEnv,
613
644
  });
645
+ timing.mark("compile_subagents");
614
646
  }
615
647
 
616
648
  // Step 12: Create the agent graph with middleware
@@ -682,6 +714,15 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
682
714
  `tools=${tools.length}, middleware=${middleware.length}, ` +
683
715
  `thread_id=${threadId}, streamVersion=${streamVersion}`,
684
716
  );
717
+ timing.mark("create_agent_graph");
718
+ emitTimingLog("execution_setup", {
719
+ execution_id: executionId,
720
+ session_id: sessionId,
721
+ harness: "native",
722
+ mcp_server_count: mcpServerUsages.length,
723
+ skill_count: skillRefs.length,
724
+ workspace_entry_count: session.spec!.workspaceEntries?.length ?? 0,
725
+ }, timing);
685
726
 
686
727
  return {
687
728
  agentGraph,
@@ -33,7 +33,7 @@ import type { ApprovalGateConfig } from "../../middleware/approval-gate.js";
33
33
  import { createCasCaptureBackend } from "./cas-capture-backend.js";
34
34
  import type { CasCaptureObserver } from "./cas-capture-observer.js";
35
35
  import type { CostCapMiddleware, StigmerMiddleware } from "../../middleware/index.js";
36
- import { createThinkTool } from "../../middleware/index.js";
36
+ import { createThinkTool, createWebFetchTool, type GuardPosture } from "../../tools/index.js";
37
37
  import { buildSubAgentMiddleware } from "./subagent-wiring.js";
38
38
  import { SubAgentGate } from "../../shared/subagent-gate.js";
39
39
  import { isModelRegistered } from "../../shared/model-registry.js";
@@ -158,6 +158,12 @@ export interface SubagentTransformOptions {
158
158
  readonly casObserver?: CasCaptureObserver;
159
159
  readonly parentModelName: string;
160
160
  readonly parentHasNativeThinking: boolean;
161
+ /**
162
+ * URL-guard posture for the native `web_fetch` tool, inherited from the
163
+ * parent (resolveGuardPosture(config.mode) in setup.ts) so a sub-agent
164
+ * fetch is bounded exactly like a parent fetch.
165
+ */
166
+ readonly webFetchPosture: GuardPosture;
161
167
  readonly costCap?: CostCapMiddleware;
162
168
  /**
163
169
  * Builds a configured chat-model instance for a given model name. When
@@ -203,6 +209,7 @@ export interface SubagentTransformOptions {
203
209
  export function createBuiltinSubagents(
204
210
  hasWorkspace: boolean,
205
211
  parentMcpTools: readonly StructuredTool[] = [],
212
+ webFetchPosture?: GuardPosture,
206
213
  ): TransformedSubagent[] {
207
214
  if (!hasWorkspace) {
208
215
  return [];
@@ -216,7 +223,15 @@ export function createBuiltinSubagents(
216
223
  name: "general-purpose",
217
224
  description: DEFAULT_GENERAL_PURPOSE_DESCRIPTION,
218
225
  systemPrompt: DEFAULT_SUBAGENT_PROMPT + RESPONSE_RULES,
219
- tools: [...parentMcpTools],
226
+ // Parent-parity tool set: MCP tools plus the native web_fetch the
227
+ // parent always carries. explore/shell stay web-less on purpose —
228
+ // their prompts scope them to the workspace, not the internet.
229
+ tools: [
230
+ ...parentMcpTools,
231
+ ...(webFetchPosture
232
+ ? [createWebFetchTool({ posture: webFetchPosture }) as unknown as StructuredTool]
233
+ : []),
234
+ ],
220
235
  });
221
236
  continue;
222
237
  }
@@ -257,6 +272,7 @@ export async function transformSingleSubagent(
257
272
  readonly parentMcpUsages: readonly McpServerUsage[];
258
273
  readonly parentHasNativeThinking: boolean;
259
274
  readonly parentModelName: string;
275
+ readonly webFetchPosture: GuardPosture;
260
276
  },
261
277
  ): Promise<TransformedSubagent | null> {
262
278
  const name = subAgent.name;
@@ -290,6 +306,12 @@ export async function transformSingleSubagent(
290
306
  tools.push(createThinkTool() as unknown as StructuredTool);
291
307
  }
292
308
 
309
+ // web_fetch is unconditional — unlike think, it is a capability, not a
310
+ // reasoning aid, and the parent always has it (setup.ts Step 11). A
311
+ // sub-agent silently lacking web access the parent has would recreate the
312
+ // harness-parity gap of issue #214 one level down.
313
+ tools.push(createWebFetchTool({ posture: opts.webFetchPosture }) as unknown as StructuredTool);
314
+
293
315
  // Append response rules
294
316
  systemPrompt += RESPONSE_RULES;
295
317
 
@@ -597,6 +619,7 @@ export async function transformAndCompileSubagents(
597
619
  casObserver,
598
620
  parentModelName,
599
621
  parentHasNativeThinking,
622
+ webFetchPosture,
600
623
  costCap,
601
624
  modelFactory,
602
625
  shellEnv,
@@ -615,6 +638,7 @@ export async function transformAndCompileSubagents(
615
638
  const builtins = createBuiltinSubagents(
616
639
  !!workspaceBackend.rootDir,
617
640
  parentMcpTools,
641
+ webFetchPosture,
618
642
  );
619
643
 
620
644
  // Step 1b: Batch fetch all skills referenced by subagents
@@ -665,6 +689,7 @@ export async function transformAndCompileSubagents(
665
689
  parentMcpUsages,
666
690
  parentHasNativeThinking,
667
691
  parentModelName,
692
+ webFetchPosture,
668
693
  });
669
694
 
670
695
  if (result) {