@stigmer/runner 3.4.1 → 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 (123) 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/tool-kind.js +6 -0
  65. package/dist/shared/tool-kind.js.map +1 -1
  66. package/dist/tools/index.d.ts +27 -0
  67. package/dist/tools/index.js +27 -0
  68. package/dist/tools/index.js.map +1 -0
  69. package/dist/tools/think-tool.js.map +1 -0
  70. package/dist/tools/url-guard.d.ts +64 -0
  71. package/dist/tools/url-guard.js +211 -0
  72. package/dist/tools/url-guard.js.map +1 -0
  73. package/dist/tools/web-fetch-tool.d.ts +53 -0
  74. package/dist/tools/web-fetch-tool.js +216 -0
  75. package/dist/tools/web-fetch-tool.js.map +1 -0
  76. package/package.json +4 -2
  77. package/src/__tests__/otel-metrics.test.ts +6 -0
  78. package/src/__tests__/pool-member.test.ts +76 -0
  79. package/src/activities/__tests__/attach-session.test.ts +133 -0
  80. package/src/activities/__tests__/discover-mcp-server.test.ts +34 -11
  81. package/src/activities/attach-session.ts +112 -0
  82. package/src/activities/discover-mcp-server.ts +19 -1
  83. package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +77 -0
  84. package/src/activities/execute-cursor/__tests__/sdk-warmup.test.ts +45 -0
  85. package/src/activities/execute-cursor/connect-backfill.ts +3 -0
  86. package/src/activities/execute-cursor/fetch-interceptor.ts +39 -0
  87. package/src/activities/execute-cursor/index.ts +72 -3
  88. package/src/activities/execute-cursor/mcp-resolver.ts +21 -2
  89. package/src/activities/execute-cursor/sdk-warmup.ts +68 -0
  90. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +24 -0
  91. package/src/activities/execute-deep-agent/setup.ts +48 -7
  92. package/src/activities/execute-deep-agent/subagent-transformer.ts +27 -2
  93. package/src/client/__tests__/stigmer-client.test.ts +77 -2
  94. package/src/client/__tests__/token-claims.test.ts +15 -1
  95. package/src/client/stigmer-client.ts +51 -6
  96. package/src/client/token-claims.ts +29 -6
  97. package/src/main.ts +139 -2
  98. package/src/middleware/index.ts +0 -1
  99. package/src/otel-metrics.ts +25 -1
  100. package/src/pool-member.ts +99 -0
  101. package/src/runner-manager.ts +52 -0
  102. package/src/runner.ts +15 -0
  103. package/src/shared/__tests__/cold-start-timing.test.ts +271 -0
  104. package/src/shared/__tests__/connect-backfill.test.ts +11 -11
  105. package/src/shared/__tests__/mcp-manager.test.ts +2 -73
  106. package/src/shared/__tests__/mcp-resolver.test.ts +108 -0
  107. package/src/shared/__tests__/mcp-transport-guard.test.ts +71 -0
  108. package/src/shared/cold-start-timing.ts +197 -0
  109. package/src/shared/connect-backfill.ts +5 -1
  110. package/src/shared/mcp-manager.ts +4 -41
  111. package/src/shared/mcp-resolver.ts +29 -6
  112. package/src/shared/mcp-transport-guard.ts +81 -0
  113. package/src/shared/tool-kind.ts +6 -0
  114. package/src/tools/__tests__/url-guard.test.ts +137 -0
  115. package/src/tools/__tests__/web-fetch-tool.test.ts +193 -0
  116. package/src/tools/index.ts +28 -0
  117. package/src/tools/url-guard.ts +232 -0
  118. package/src/tools/web-fetch-tool.ts +275 -0
  119. package/dist/middleware/think-tool.js.map +0 -1
  120. /package/dist/{middleware → tools}/think-tool.d.ts +0 -0
  121. /package/dist/{middleware → tools}/think-tool.js +0 -0
  122. /package/src/{middleware → tools}/__tests__/think-tool.test.ts +0 -0
  123. /package/src/{middleware → tools}/think-tool.ts +0 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * URL guard for the native `web_fetch` tool — the SSRF boundary.
3
+ *
4
+ * Why this is load-bearing: `web_fetch` is deliberately auto-approved (see
5
+ * shared/tool-kind.ts `toolApprovalCategory`), so in unattended runs it is
6
+ * the only ungated network path an agent has. On managed cloud runners the
7
+ * fetch executes inside the runner process, which sits on infrastructure
8
+ * with network reach to internal services and the cloud metadata endpoint —
9
+ * without this guard, "fetch a URL" becomes "read my pod's credentials".
10
+ *
11
+ * Posture is mode-aware because locality differs, not trust:
12
+ *
13
+ * - "strict" (managed cloud runners): loopback, RFC 1918 private,
14
+ * link-local, and their IPv6 equivalents are all rejected.
15
+ * - "relaxed" (self-hosted / local runners): the machine belongs to the
16
+ * user, and fetching their own dev server (http://localhost:3000) is a
17
+ * legitimate ask — only the link-local range (which contains the cloud
18
+ * metadata endpoint 169.254.169.254) stays blocked, as costless
19
+ * defense-in-depth.
20
+ *
21
+ * The default posture derives from Config.mode ("cloud" → strict), NOT from
22
+ * cloudModeEnabled — that is the Cursor cloud-agent feature flag and says
23
+ * nothing about where this process runs. Config.mode tracks credential
24
+ * transport rather than physical locality (a desktop runner proxies traffic
25
+ * while executing on the user's machine), so STIGMER_WEB_FETCH_ALLOW_PRIVATE
26
+ * exists as an explicit override for embedders that know better. Managed
27
+ * cloud deployments never set it.
28
+ *
29
+ * Validation runs on the addresses DNS resolves to immediately before the
30
+ * request is dispatched, and the caller re-validates every redirect hop.
31
+ * Accepted limitation: a DNS-rebinding window remains between our lookup
32
+ * and the socket connect — Node's fetch (undici) offers no lookup pinning
33
+ * without replacing the dispatcher, and the strict posture's range blocks
34
+ * make the rebinding payoff (an internal address) unreachable anyway.
35
+ */
36
+ import { lookup } from "node:dns/promises";
37
+ import { isIP } from "node:net";
38
+ /** Thrown for every guard rejection; message is safe to surface to the model. */
39
+ export class UrlGuardError extends Error {
40
+ constructor(message) {
41
+ super(message);
42
+ this.name = "UrlGuardError";
43
+ }
44
+ }
45
+ /**
46
+ * Derive the guard posture from runner mode plus the explicit override.
47
+ *
48
+ * @param mode Config.mode — "cloud" means a managed runner on shared infrastructure.
49
+ * @param env Environment to read STIGMER_WEB_FETCH_ALLOW_PRIVATE from.
50
+ */
51
+ export function resolveGuardPosture(mode, env = process.env) {
52
+ const override = env.STIGMER_WEB_FETCH_ALLOW_PRIVATE;
53
+ if (override === "true")
54
+ return "relaxed";
55
+ if (override === "false")
56
+ return "strict";
57
+ return mode === "cloud" ? "strict" : "relaxed";
58
+ }
59
+ /**
60
+ * Validate a URL for fetching under the given posture.
61
+ *
62
+ * Checks the scheme, then resolves the hostname and checks EVERY address it
63
+ * resolves to (a hostname with one public and one private A record must be
64
+ * rejected, or the private record becomes the bypass).
65
+ *
66
+ * @returns the parsed URL on success.
67
+ * @throws UrlGuardError with a model-readable reason on rejection.
68
+ */
69
+ export async function validateFetchUrl(rawUrl, posture) {
70
+ let url;
71
+ try {
72
+ url = new URL(rawUrl);
73
+ }
74
+ catch {
75
+ throw new UrlGuardError(`Invalid URL: ${rawUrl}`);
76
+ }
77
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
78
+ throw new UrlGuardError(`Unsupported URL scheme "${url.protocol.replace(/:$/, "")}" — only http and https are allowed.`);
79
+ }
80
+ const addresses = await resolveAddresses(url.hostname);
81
+ for (const address of addresses) {
82
+ const reason = blockedReason(address, posture);
83
+ if (reason) {
84
+ throw new UrlGuardError(`Refusing to fetch ${url.hostname}: it resolves to ${address}, a ${reason} address that this runner does not fetch from.`);
85
+ }
86
+ }
87
+ return url;
88
+ }
89
+ /**
90
+ * Resolve a hostname to all of its addresses. Literal IPs pass through
91
+ * (bracketed IPv6 hosts arrive from URL.hostname still bracketed).
92
+ */
93
+ async function resolveAddresses(hostname) {
94
+ const literal = hostname.replace(/^\[|\]$/g, "");
95
+ if (isIP(literal) !== 0) {
96
+ return [literal];
97
+ }
98
+ try {
99
+ const records = await lookup(hostname, { all: true, verbatim: true });
100
+ return records.map((r) => r.address);
101
+ }
102
+ catch {
103
+ throw new UrlGuardError(`Could not resolve hostname: ${hostname}`);
104
+ }
105
+ }
106
+ /**
107
+ * Classify an IP address against the posture's blocked ranges.
108
+ *
109
+ * @returns a human-readable range name when blocked, or null when allowed.
110
+ */
111
+ export function blockedReason(address, posture) {
112
+ const family = isIP(address);
113
+ if (family === 4) {
114
+ return blockedReasonV4(address, posture);
115
+ }
116
+ if (family === 6) {
117
+ const groups = expandV6(address);
118
+ if (!groups)
119
+ return "unrecognized";
120
+ // IPv4-mapped IPv6 (::ffff:a.b.c.d, in dotted OR hex form) — classify
121
+ // the embedded IPv4 so the mapping cannot smuggle a blocked v4 address
122
+ // past the guard.
123
+ const embedded = extractMappedV4(groups);
124
+ if (embedded) {
125
+ return blockedReasonV4(embedded, posture);
126
+ }
127
+ return blockedReasonV6(groups, posture);
128
+ }
129
+ // Unparseable — fail closed; only real addresses get sockets.
130
+ return "unrecognized";
131
+ }
132
+ function blockedReasonV4(address, posture) {
133
+ const octets = address.split(".").map(Number);
134
+ if (octets.length !== 4 || octets.some((o) => Number.isNaN(o) || o < 0 || o > 255)) {
135
+ return "unrecognized";
136
+ }
137
+ const [a, b] = octets;
138
+ // Link-local (169.254.0.0/16) hosts the cloud metadata service at
139
+ // 169.254.169.254 — blocked under BOTH postures.
140
+ if (a === 169 && b === 254)
141
+ return "link-local (cloud metadata)";
142
+ if (posture === "relaxed")
143
+ return null;
144
+ if (a === 127)
145
+ return "loopback";
146
+ if (a === 0)
147
+ return "unspecified";
148
+ if (a === 10)
149
+ return "private (RFC 1918)";
150
+ if (a === 172 && b >= 16 && b <= 31)
151
+ return "private (RFC 1918)";
152
+ if (a === 192 && b === 168)
153
+ return "private (RFC 1918)";
154
+ return null;
155
+ }
156
+ function blockedReasonV6(groups, posture) {
157
+ // fe80::/10 — IPv6 link-local, the v6 sibling of the metadata range;
158
+ // blocked under BOTH postures for symmetry with v4.
159
+ if ((groups[0] & 0xffc0) === 0xfe80)
160
+ return "link-local (cloud metadata)";
161
+ if (posture === "relaxed")
162
+ return null;
163
+ const allZero = groups.every((g) => g === 0);
164
+ if (allZero)
165
+ return "unspecified";
166
+ if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1)
167
+ return "loopback";
168
+ // fc00::/7 — unique local (private) addresses.
169
+ if ((groups[0] & 0xfe00) === 0xfc00)
170
+ return "private (unique local)";
171
+ return null;
172
+ }
173
+ /**
174
+ * Expand an IPv6 address (already validated by isIP) into its 8 groups.
175
+ * Handles `::` compression, a trailing dotted-IPv4 tail, and zone suffixes.
176
+ */
177
+ function expandV6(address) {
178
+ let text = address.toLowerCase().split("%")[0];
179
+ // Convert a dotted-IPv4 tail (e.g. ::ffff:127.0.0.1) into two hex groups.
180
+ const v4Tail = /(\d+\.\d+\.\d+\.\d+)$/.exec(text);
181
+ if (v4Tail) {
182
+ const octets = v4Tail[1].split(".").map(Number);
183
+ if (octets.length !== 4 || octets.some((o) => o > 255))
184
+ return null;
185
+ const hex = `${((octets[0] << 8) | octets[1]).toString(16)}:${((octets[2] << 8) | octets[3]).toString(16)}`;
186
+ text = text.slice(0, -v4Tail[1].length) + hex;
187
+ }
188
+ const halves = text.split("::");
189
+ if (halves.length > 2)
190
+ return null;
191
+ const parseHalf = (half) => half === "" ? [] : half.split(":").map((g) => parseInt(g, 16));
192
+ const head = parseHalf(halves[0]);
193
+ const tail = halves.length === 2 ? parseHalf(halves[1]) : [];
194
+ const fill = 8 - head.length - tail.length;
195
+ if (halves.length === 2 ? fill < 0 : head.length !== 8)
196
+ return null;
197
+ const groups = [...head, ...(halves.length === 2 ? Array(fill).fill(0) : []), ...tail];
198
+ if (groups.length !== 8 || groups.some((g) => Number.isNaN(g) || g < 0 || g > 0xffff)) {
199
+ return null;
200
+ }
201
+ return groups;
202
+ }
203
+ /** Extract the IPv4 payload from IPv4-mapped groups (::ffff:0:0/96), if any. */
204
+ function extractMappedV4(groups) {
205
+ const isMapped = groups.slice(0, 5).every((g) => g === 0) && groups[5] === 0xffff;
206
+ if (!isMapped)
207
+ return null;
208
+ const [hi, lo] = [groups[6], groups[7]];
209
+ return `${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff}`;
210
+ }
211
+ //# sourceMappingURL=url-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-guard.js","sourceRoot":"","sources":["../../src/tools/url-guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Native `web_fetch` tool — built-in URL retrieval for the deep-agent harness.
3
+ *
4
+ * Closes the harness parity gap of stigmer/stigmer#214: the Cursor harness
5
+ * ships WebFetch as a CLI built-in, while native runs previously needed an
6
+ * MCP server (seedpack `fetch`) for the same capability. Both tool names
7
+ * classify to TOOL_KIND_FETCH, so rendering is identical across harnesses.
8
+ *
9
+ * Design constraints, in order:
10
+ *
11
+ * - Security: every URL — including every redirect hop — passes the URL
12
+ * guard (url-guard.ts). web_fetch is auto-approved, so the guard is the
13
+ * only boundary between "fetch a page" and the runner's network position.
14
+ * - Model ergonomics: HTML is reduced to Markdown (turndown, with obvious
15
+ * chrome stripped), and `start_index`/`max_length` give the model
16
+ * deterministic pagination over large pages — the same surface the
17
+ * seedpack fetch MCP exposed, so existing agent instructions keep working.
18
+ * - Budget discipline: the default window (20 000 chars) sits deliberately
19
+ * below the tool-truncation middleware's 30 000-char default, so the
20
+ * truncation layer never chops fetch output and the model's pagination
21
+ * arithmetic stays exact.
22
+ *
23
+ * Failures return a plain "Error: …" string instead of throwing: a fetch
24
+ * miss is information the model should route around (try the .md export,
25
+ * cite a different page), not an execution fault.
26
+ */
27
+ import { z } from "zod";
28
+ import { type GuardPosture } from "./url-guard.js";
29
+ export interface WebFetchToolOptions {
30
+ /** URL guard posture — derive via resolveGuardPosture(config.mode). */
31
+ readonly posture: GuardPosture;
32
+ }
33
+ export declare function createWebFetchTool(options: WebFetchToolOptions): import("@langchain/core/tools").DynamicStructuredTool<z.ZodObject<{
34
+ url: z.ZodString;
35
+ max_length: z.ZodOptional<z.ZodNumber>;
36
+ start_index: z.ZodOptional<z.ZodNumber>;
37
+ }, "strip", z.ZodTypeAny, {
38
+ url: string;
39
+ max_length?: number | undefined;
40
+ start_index?: number | undefined;
41
+ }, {
42
+ url: string;
43
+ max_length?: number | undefined;
44
+ start_index?: number | undefined;
45
+ }>, {
46
+ url: string;
47
+ max_length?: number | undefined;
48
+ start_index?: number | undefined;
49
+ }, {
50
+ url: string;
51
+ max_length?: number | undefined;
52
+ start_index?: number | undefined;
53
+ }, string, unknown, "web_fetch">;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Native `web_fetch` tool — built-in URL retrieval for the deep-agent harness.
3
+ *
4
+ * Closes the harness parity gap of stigmer/stigmer#214: the Cursor harness
5
+ * ships WebFetch as a CLI built-in, while native runs previously needed an
6
+ * MCP server (seedpack `fetch`) for the same capability. Both tool names
7
+ * classify to TOOL_KIND_FETCH, so rendering is identical across harnesses.
8
+ *
9
+ * Design constraints, in order:
10
+ *
11
+ * - Security: every URL — including every redirect hop — passes the URL
12
+ * guard (url-guard.ts). web_fetch is auto-approved, so the guard is the
13
+ * only boundary between "fetch a page" and the runner's network position.
14
+ * - Model ergonomics: HTML is reduced to Markdown (turndown, with obvious
15
+ * chrome stripped), and `start_index`/`max_length` give the model
16
+ * deterministic pagination over large pages — the same surface the
17
+ * seedpack fetch MCP exposed, so existing agent instructions keep working.
18
+ * - Budget discipline: the default window (20 000 chars) sits deliberately
19
+ * below the tool-truncation middleware's 30 000-char default, so the
20
+ * truncation layer never chops fetch output and the model's pagination
21
+ * arithmetic stays exact.
22
+ *
23
+ * Failures return a plain "Error: …" string instead of throwing: a fetch
24
+ * miss is information the model should route around (try the .md export,
25
+ * cite a different page), not an execution fault.
26
+ */
27
+ import { tool } from "@langchain/core/tools";
28
+ import { z } from "zod";
29
+ import TurndownService from "turndown";
30
+ import { validateFetchUrl, UrlGuardError } from "./url-guard.js";
31
+ /** Ceiling on response bytes read from the wire, before any conversion. */
32
+ const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
33
+ /** Per-request timeout; applied to each redirect hop independently. */
34
+ const REQUEST_TIMEOUT_MS = 15_000;
35
+ /** Redirect chain limit — matches the common browser/curl default region. */
36
+ const MAX_REDIRECTS = 5;
37
+ /** Default pagination window; see file header for why it is below 30 000. */
38
+ const DEFAULT_MAX_LENGTH = 20_000;
39
+ /** Hard ceiling for a caller-supplied max_length. */
40
+ const MAX_MAX_LENGTH = 100_000;
41
+ const USER_AGENT = "Stigmer/1.0 (web_fetch; +https://stigmer.ai)";
42
+ export function createWebFetchTool(options) {
43
+ return tool(async (input) => {
44
+ try {
45
+ return await runFetch(input, options.posture);
46
+ }
47
+ catch (err) {
48
+ if (err instanceof UrlGuardError) {
49
+ return `Error: ${err.message}`;
50
+ }
51
+ if (err instanceof Error && err.name === "TimeoutError") {
52
+ return `Error: Fetching ${input.url} timed out after ${REQUEST_TIMEOUT_MS / 1000}s.`;
53
+ }
54
+ const message = err instanceof Error ? err.message : String(err);
55
+ return `Error: Failed to fetch ${input.url}: ${message}`;
56
+ }
57
+ }, {
58
+ name: "web_fetch",
59
+ description: "Fetch the contents of a URL over http(s). HTML pages are converted " +
60
+ "to Markdown; plain text, Markdown, JSON, and other text formats are " +
61
+ "returned as-is. Binary content is not supported.\n\n" +
62
+ "Large pages are windowed: at most max_length characters are " +
63
+ "returned per call (default 20000). If the result ends with a " +
64
+ "truncation notice, call web_fetch again with the same url and the " +
65
+ "start_index the notice gives you to continue reading.",
66
+ schema: z.object({
67
+ url: z.string().describe("The http(s) URL to fetch."),
68
+ max_length: z
69
+ .number()
70
+ .int()
71
+ .positive()
72
+ .max(MAX_MAX_LENGTH)
73
+ .optional()
74
+ .describe(`Maximum characters to return (default ${DEFAULT_MAX_LENGTH}).`),
75
+ start_index: z
76
+ .number()
77
+ .int()
78
+ .nonnegative()
79
+ .optional()
80
+ .describe("Character offset to start from, for paginating large pages (default 0)."),
81
+ }),
82
+ });
83
+ }
84
+ async function runFetch(input, posture) {
85
+ const maxLength = input.max_length ?? DEFAULT_MAX_LENGTH;
86
+ const startIndex = input.start_index ?? 0;
87
+ const { response, finalUrl } = await fetchFollowingRedirects(input.url, posture);
88
+ if (!response.ok) {
89
+ // Drain defensively; some agents (undici) keep the connection reserved otherwise.
90
+ await response.body?.cancel().catch(() => undefined);
91
+ return `Error: ${finalUrl} responded with HTTP ${response.status} ${response.statusText}.`;
92
+ }
93
+ const contentType = response.headers.get("content-type") ?? "";
94
+ if (!isTextual(contentType)) {
95
+ await response.body?.cancel().catch(() => undefined);
96
+ return (`Error: ${finalUrl} returned non-text content (${contentType || "unknown type"}). ` +
97
+ "web_fetch only supports text formats.");
98
+ }
99
+ const { text, bytesTruncated } = await readBodyCapped(response, MAX_RESPONSE_BYTES);
100
+ const content = isHtml(contentType) ? htmlToMarkdown(text) : text;
101
+ return paginate(content, {
102
+ url: finalUrl,
103
+ startIndex,
104
+ maxLength,
105
+ bytesTruncated,
106
+ });
107
+ }
108
+ /**
109
+ * Fetch with `redirect: "manual"`, re-validating each hop against the URL
110
+ * guard — automatic following would let a public host 302 the runner into
111
+ * a private address, bypassing the pre-flight check entirely.
112
+ */
113
+ async function fetchFollowingRedirects(rawUrl, posture) {
114
+ let url = await validateFetchUrl(rawUrl, posture);
115
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
116
+ const response = await fetch(url, {
117
+ redirect: "manual",
118
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
119
+ headers: {
120
+ "user-agent": USER_AGENT,
121
+ accept: "text/html, text/markdown;q=0.9, text/plain;q=0.8, application/json;q=0.7, */*;q=0.5",
122
+ },
123
+ });
124
+ if (response.status < 300 || response.status >= 400) {
125
+ return { response, finalUrl: url.toString() };
126
+ }
127
+ const location = response.headers.get("location");
128
+ await response.body?.cancel().catch(() => undefined);
129
+ if (!location) {
130
+ throw new Error(`redirect (HTTP ${response.status}) without a Location header`);
131
+ }
132
+ // Resolve relative redirects against the current hop, then re-guard.
133
+ url = await validateFetchUrl(new URL(location, url).toString(), posture);
134
+ }
135
+ throw new Error(`too many redirects (more than ${MAX_REDIRECTS})`);
136
+ }
137
+ /** Stream the body, stopping at the byte cap instead of buffering unbounded. */
138
+ async function readBodyCapped(response, maxBytes) {
139
+ if (!response.body) {
140
+ return { text: "", bytesTruncated: false };
141
+ }
142
+ const reader = response.body.getReader();
143
+ const chunks = [];
144
+ let total = 0;
145
+ let bytesTruncated = false;
146
+ for (;;) {
147
+ const { done, value } = await reader.read();
148
+ if (done)
149
+ break;
150
+ if (value) {
151
+ total += value.byteLength;
152
+ if (total > maxBytes) {
153
+ chunks.push(value.subarray(0, value.byteLength - (total - maxBytes)));
154
+ bytesTruncated = true;
155
+ await reader.cancel().catch(() => undefined);
156
+ break;
157
+ }
158
+ chunks.push(value);
159
+ }
160
+ }
161
+ const merged = new Uint8Array(chunks.reduce((n, c) => n + c.byteLength, 0));
162
+ let offset = 0;
163
+ for (const chunk of chunks) {
164
+ merged.set(chunk, offset);
165
+ offset += chunk.byteLength;
166
+ }
167
+ return { text: new TextDecoder("utf-8").decode(merged), bytesTruncated };
168
+ }
169
+ function isHtml(contentType) {
170
+ return /text\/html|application\/xhtml\+xml/i.test(contentType);
171
+ }
172
+ function isTextual(contentType) {
173
+ if (contentType === "")
174
+ return true; // no header — assume text, the cap bounds the damage
175
+ return /^(text\/|application\/(json|xml|xhtml\+xml|rss\+xml|atom\+xml|javascript|x-yaml|yaml|toml))/i.test(contentType);
176
+ }
177
+ /**
178
+ * Reduce HTML to Markdown. Page chrome that never carries answerable
179
+ * content (scripts, styles, navigation, embedded media shells) is removed
180
+ * before conversion so the pagination window is spent on substance.
181
+ */
182
+ function htmlToMarkdown(html) {
183
+ const turndown = new TurndownService({
184
+ headingStyle: "atx",
185
+ codeBlockStyle: "fenced",
186
+ bulletListMarker: "-",
187
+ });
188
+ turndown.remove([
189
+ "script", "style", "noscript", "iframe", "object", "embed",
190
+ "nav", "aside", "footer", "form", "button", "select", "canvas",
191
+ ]);
192
+ const markdown = turndown.turndown(html);
193
+ // Collapse the blank-line runs left behind by removed blocks.
194
+ return markdown.replace(/\n{3,}/g, "\n\n").trim();
195
+ }
196
+ /** Apply the start_index/max_length window and explain any truncation. */
197
+ function paginate(content, opts) {
198
+ if (opts.startIndex >= content.length && content.length > 0) {
199
+ return `Error: start_index ${opts.startIndex} is beyond the end of the content (${content.length} characters).`;
200
+ }
201
+ if (content.length === 0) {
202
+ return `[${opts.url} returned an empty body]`;
203
+ }
204
+ const window = content.slice(opts.startIndex, opts.startIndex + opts.maxLength);
205
+ const end = opts.startIndex + window.length;
206
+ const notices = [];
207
+ if (end < content.length) {
208
+ notices.push(`[Content truncated at ${end} of ${content.length} characters. ` +
209
+ `Call web_fetch again with start_index=${end} to continue.]`);
210
+ }
211
+ if (opts.bytesTruncated) {
212
+ notices.push(`[The response exceeded the ${MAX_RESPONSE_BYTES / (1024 * 1024)} MB fetch limit and was cut off.]`);
213
+ }
214
+ return notices.length > 0 ? `${window}\n\n${notices.join("\n")}` : window;
215
+ }
216
+ //# sourceMappingURL=web-fetch-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-fetch-tool.js","sourceRoot":"","sources":["../../src/tools/web-fetch-tool.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/runner",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "description": "Embeddable Temporal worker for the Stigmer AI agent platform — handles agent execution, workflow orchestration, and MCP server management",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -85,7 +85,7 @@
85
85
  "@opentelemetry/sdk-metrics": "^2.0.0",
86
86
  "@opentelemetry/sdk-trace-base": "^2.0.0",
87
87
  "@opentelemetry/sdk-trace-node": "^2.0.0",
88
- "@stigmer/protos": "3.4.1",
88
+ "@stigmer/protos": "3.5.0",
89
89
  "@temporalio/activity": "^1.11.0",
90
90
  "@temporalio/client": "^1.11.0",
91
91
  "@temporalio/common": "^1.11.0",
@@ -98,6 +98,7 @@
98
98
  "js-yaml": "^4.1.1",
99
99
  "proper-lockfile": "^4.1.2",
100
100
  "semver": "^7.8.0",
101
+ "turndown": "^7.2.4",
101
102
  "zod": "^3.25.0",
102
103
  "zod-to-json-schema": "^3.24.0"
103
104
  },
@@ -106,6 +107,7 @@
106
107
  "@types/js-yaml": "^4.0.9",
107
108
  "@types/proper-lockfile": "^4.1.4",
108
109
  "@types/semver": "^7.7.1",
110
+ "@types/turndown": "^5.0.6",
109
111
  "esbuild": "^0.28.0",
110
112
  "tsx": "^4.21.0",
111
113
  "typescript": "^5.7.0",
@@ -13,6 +13,9 @@ describe("OTel Metrics Registry", () => {
13
13
  expect(instruments.activityDuration).toBeDefined();
14
14
  expect(instruments.workflowTaskDuration).toBeDefined();
15
15
  expect(instruments.workflowTaskCount).toBeDefined();
16
+ expect(instruments.runnerBootDuration).toBeDefined();
17
+ expect(instruments.executionSetupDuration).toBeDefined();
18
+ expect(instruments.poolAttachDuration).toBeDefined();
16
19
  });
17
20
 
18
21
  it("returns the same singleton on repeated calls", async () => {
@@ -29,6 +32,9 @@ describe("OTel Metrics Registry", () => {
29
32
  expect(() => instruments.activityDuration.record(150)).not.toThrow();
30
33
  expect(() => instruments.workflowTaskDuration.record(42)).not.toThrow();
31
34
  expect(() => instruments.workflowTaskCount.add(1, { "task.kind": "set" })).not.toThrow();
35
+ expect(() => instruments.runnerBootDuration.record(6200, { mode: "cloud" })).not.toThrow();
36
+ expect(() => instruments.executionSetupDuration.record(3000, { harness: "cursor" })).not.toThrow();
37
+ expect(() => instruments.poolAttachDuration.record(400)).not.toThrow();
32
38
  });
33
39
 
34
40
  it("reset allows re-creation", async () => {
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import {
3
+ decidePoolBoot,
4
+ registerPoolMemberContext,
5
+ getPoolMemberContext,
6
+ clearPoolMemberContext,
7
+ type PoolAttachTarget,
8
+ } from "../pool-member.js";
9
+
10
+ /** Build an unsigned JWT-shaped token with the given payload. */
11
+ function fakeJwt(payload: Record<string, unknown>): string {
12
+ const b64 = (obj: Record<string, unknown>) =>
13
+ Buffer.from(JSON.stringify(obj)).toString("base64url");
14
+ return `${b64({ alg: "RS256", typ: "JWT" })}.${b64(payload)}.signature`;
15
+ }
16
+
17
+ const noopManager: PoolAttachTarget = {
18
+ addSession: async () => {},
19
+ updateToken: () => {},
20
+ };
21
+
22
+ describe("decidePoolBoot", () => {
23
+ it("polls the control queue with a fresh pool_sandbox credential", () => {
24
+ const intent = decidePoolBoot(fakeJwt({ token_type: "pool_sandbox", pool_member_id: "pm_1" }));
25
+ expect(intent).toEqual({ kind: "pool-control" });
26
+ });
27
+
28
+ it("resumes the claimed session when the Secret already holds a session token (post-claim restart)", () => {
29
+ const intent = decidePoolBoot(fakeJwt({ token_type: "sandbox", session_id: "ses_9" }));
30
+ expect(intent).toEqual({ kind: "claimed-session", sessionId: "ses_9" });
31
+ });
32
+
33
+ it("rejects a session token without a session_id claim", () => {
34
+ const intent = decidePoolBoot(fakeJwt({ token_type: "sandbox" }));
35
+ expect(intent.kind).toBe("invalid");
36
+ });
37
+
38
+ it("rejects every other credential class rather than guessing", () => {
39
+ // Booting a pool member with the wrong token class is a provisioning bug;
40
+ // serving anything on a guess would hand the pod a scope it must not have.
41
+ expect(decidePoolBoot(fakeJwt({ token_type: "embedded_runner" })).kind).toBe("invalid");
42
+ expect(decidePoolBoot(fakeJwt({ token_type: "workflow_sandbox" })).kind).toBe("invalid");
43
+ expect(decidePoolBoot(fakeJwt({ sub: "user-1" })).kind).toBe("invalid");
44
+ expect(decidePoolBoot(null).kind).toBe("invalid");
45
+ expect(decidePoolBoot("not-a-jwt").kind).toBe("invalid");
46
+ });
47
+
48
+ it("names the offending token_type in the rejection reason", () => {
49
+ const intent = decidePoolBoot(fakeJwt({ token_type: "embedded_runner" }));
50
+ expect(intent.kind === "invalid" && intent.reason).toContain("embedded_runner");
51
+ });
52
+ });
53
+
54
+ describe("pool member context registry", () => {
55
+ afterEach(() => {
56
+ clearPoolMemberContext();
57
+ });
58
+
59
+ it("is undefined outside a pool member (the inert default)", () => {
60
+ expect(getPoolMemberContext()).toBeUndefined();
61
+ });
62
+
63
+ it("returns the registered context after pool boot", () => {
64
+ registerPoolMemberContext({ memberId: "pm_1", poolToken: "tok", manager: noopManager });
65
+
66
+ const ctx = getPoolMemberContext();
67
+ expect(ctx?.memberId).toBe("pm_1");
68
+ expect(ctx?.poolToken).toBe("tok");
69
+ });
70
+
71
+ it("clears for test isolation", () => {
72
+ registerPoolMemberContext({ memberId: "pm_1", poolToken: "tok", manager: noopManager });
73
+ clearPoolMemberContext();
74
+ expect(getPoolMemberContext()).toBeUndefined();
75
+ });
76
+ });