@wrongstack/tools 0.313.0 → 0.313.1

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 (134) hide show
  1. package/dist/audit.js +10 -4055
  2. package/dist/bash.js +9 -2308
  3. package/dist/batch-tool-use.js +2 -187
  4. package/dist/browser/index.js +25 -1339
  5. package/dist/builtin.js +69 -33949
  6. package/dist/chunk-2CTB3UK5.js +197 -0
  7. package/dist/chunk-2ME7ZWDD.js +1272 -0
  8. package/dist/chunk-2TFQDEPU.js +113 -0
  9. package/dist/chunk-2ZCJNKFE.js +315 -0
  10. package/dist/chunk-3PVYQFRQ.js +472 -0
  11. package/dist/chunk-3QMQBO5Z.js +89 -0
  12. package/dist/chunk-42AKKX7O.js +385 -0
  13. package/dist/chunk-42SKEETB.js +113 -0
  14. package/dist/chunk-4BHUSMNL.js +104 -0
  15. package/dist/chunk-4I4LWFOO.js +431 -0
  16. package/dist/chunk-553IQUMO.js +478 -0
  17. package/dist/chunk-55KF77OK.js +1944 -0
  18. package/dist/chunk-6HLZK37X.js +359 -0
  19. package/dist/chunk-6RNIFLG4.js +305 -0
  20. package/dist/chunk-A6BRWIXN.js +317 -0
  21. package/dist/chunk-AHHDWNXC.js +200 -0
  22. package/dist/chunk-B2CIOKWP.js +899 -0
  23. package/dist/chunk-BTSFTGIL.js +7 -0
  24. package/dist/chunk-D33S4RQG.js +222 -0
  25. package/dist/chunk-DIJKHSUO.js +134 -0
  26. package/dist/chunk-DPL47QGS.js +3515 -0
  27. package/dist/chunk-EMRGN4KM.js +170 -0
  28. package/dist/chunk-FGOABE4Z.js +955 -0
  29. package/dist/chunk-FW65TP4G.js +520 -0
  30. package/dist/chunk-GOIZNNOA.js +221 -0
  31. package/dist/chunk-H5MJUAA6.js +295 -0
  32. package/dist/chunk-H5OHH6F4.js +223 -0
  33. package/dist/chunk-HC5RM4WY.js +199 -0
  34. package/dist/chunk-HEQFMLZ7.js +190 -0
  35. package/dist/chunk-HFXSNKCV.js +196 -0
  36. package/dist/chunk-HXSBQ2SE.js +1723 -0
  37. package/dist/chunk-ILESNGNM.js +163 -0
  38. package/dist/chunk-INF5CGAO.js +137 -0
  39. package/dist/chunk-IWDFZ6XR.js +337 -0
  40. package/dist/chunk-IXMNTNW5.js +238 -0
  41. package/dist/chunk-J4SRJKBW.js +428 -0
  42. package/dist/chunk-J5SKD6LN.js +235 -0
  43. package/dist/chunk-JRJJX3L5.js +424 -0
  44. package/dist/chunk-KXNDHMUB.js +366 -0
  45. package/dist/chunk-LIJQMYYX.js +59 -0
  46. package/dist/chunk-MAN26PZ5.js +260 -0
  47. package/dist/chunk-MKRQMLZA.js +474 -0
  48. package/dist/chunk-NBDZMMG2.js +169 -0
  49. package/dist/chunk-NHA6BEIH.js +101 -0
  50. package/dist/chunk-OA6YJIWL.js +4412 -0
  51. package/dist/chunk-OZC255IC.js +2830 -0
  52. package/dist/chunk-PUR6WU2B.js +154 -0
  53. package/dist/chunk-Q5JPPCZC.js +12 -0
  54. package/dist/chunk-QBV725YG.js +128 -0
  55. package/dist/chunk-QHVW4AAM.js +892 -0
  56. package/dist/chunk-QNJ7AQ4B.js +1000 -0
  57. package/dist/chunk-RGISBZBD.js +231 -0
  58. package/dist/chunk-RJZQWEMS.js +84 -0
  59. package/dist/chunk-RP2ZA47V.js +15 -0
  60. package/dist/chunk-RXISBFR5.js +315 -0
  61. package/dist/chunk-S5CCLZL3.js +99 -0
  62. package/dist/chunk-TVTCSBJF.js +122 -0
  63. package/dist/chunk-U4D5S3VQ.js +21 -0
  64. package/dist/chunk-U4VAATKP.js +193 -0
  65. package/dist/chunk-UKU2ZX4C.js +203 -0
  66. package/dist/chunk-UNSKG6DC.js +159 -0
  67. package/dist/chunk-URFKEIHM.js +1091 -0
  68. package/dist/chunk-UTV7YAW7.js +178 -0
  69. package/dist/chunk-V43EQ6VR.js +68 -0
  70. package/dist/chunk-VB3MJGCV.js +610 -0
  71. package/dist/chunk-WHIIIREL.js +223 -0
  72. package/dist/chunk-XMIFYKUT.js +1371 -0
  73. package/dist/chunk-YF2RSV34.js +54 -0
  74. package/dist/chunk-Z6DAKQ3U.js +1 -0
  75. package/dist/chunk-ZIGAZFWE.js +522 -0
  76. package/dist/circuit-breaker.js +3 -191
  77. package/dist/codebase-index/index.js +84 -13688
  78. package/dist/codebase-index/parser-worker-script.js +8 -3995
  79. package/dist/codebase-index/project-server.js +60 -8548
  80. package/dist/codebase-index/worker.js +20 -8416
  81. package/dist/design.js +2 -299
  82. package/dist/diff.js +4 -256
  83. package/dist/document.js +4 -222
  84. package/dist/e2e.js +5 -476
  85. package/dist/edit.js +14 -10200
  86. package/dist/exec.js +16 -3131
  87. package/dist/fetch.js +8 -398
  88. package/dist/format.js +10 -3750
  89. package/dist/generic-parser-VUXDIAOZ.js +13 -0
  90. package/dist/git.js +4 -380
  91. package/dist/glob.js +6 -280
  92. package/dist/go-parser-6CA77WZ4.js +469 -0
  93. package/dist/grep.js +7 -604
  94. package/dist/index.js +318 -35387
  95. package/dist/install.js +10 -4091
  96. package/dist/json-parser-7YMDJSBJ.js +213 -0
  97. package/dist/json.js +4 -665
  98. package/dist/kanban.js +3 -2096
  99. package/dist/languages/index.js +23 -4664
  100. package/dist/legacy-bridge-7UKZT46G.js +18 -0
  101. package/dist/lint.js +10 -3706
  102. package/dist/logs.js +4 -287
  103. package/dist/memory.js +6 -215
  104. package/dist/mode.js +3 -99
  105. package/dist/outdated.js +4 -5170
  106. package/dist/pack.js +65 -33897
  107. package/dist/parser-dispatch-VPOFDO2U.js +14 -0
  108. package/dist/patch.js +13 -9935
  109. package/dist/plan.js +5 -3286
  110. package/dist/process-registry.js +8 -651
  111. package/dist/ps-slash.js +12 -1439
  112. package/dist/py-parser-GPM5VYRG.js +16 -0
  113. package/dist/read.js +13 -9816
  114. package/dist/replace.js +15 -10014
  115. package/dist/rs-parser-WVQ4KYCX.js +81 -0
  116. package/dist/scaffold.js +4 -253
  117. package/dist/search.js +5 -585
  118. package/dist/session-kanban.js +30 -972
  119. package/dist/skill.js +2 -215
  120. package/dist/task.js +5 -3410
  121. package/dist/test.js +10 -3773
  122. package/dist/todo.js +4 -2891
  123. package/dist/tool-help.js +3 -123
  124. package/dist/tool-search.js +3 -79
  125. package/dist/tool-tier.js +68 -33984
  126. package/dist/tool-use.js +2 -93
  127. package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
  128. package/dist/tree.js +4 -288
  129. package/dist/ts-parser-YH4ZMWMH.js +232 -0
  130. package/dist/typecheck.js +10 -3753
  131. package/dist/win32.js +6 -60
  132. package/dist/write.js +14 -9882
  133. package/dist/yaml-parser-GFAOKV53.js +157 -0
  134. package/package.json +5 -5
@@ -1,1347 +1,33 @@
1
- // src/browser/artifacts.ts
2
- import { createHash } from "node:crypto";
3
- import { createReadStream } from "node:fs";
4
- import * as fs from "node:fs/promises";
5
- import * as path from "node:path";
6
- import { atomicWrite, ulid } from "@wrongstack/core/utils";
7
- var ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
8
- var sweptRoots = /* @__PURE__ */ new Set();
9
- var MAX_SWEPT_ROOTS = 128;
10
- function sweepOldArtifacts(root) {
11
- if (sweptRoots.has(root)) return;
12
- while (sweptRoots.size >= MAX_SWEPT_ROOTS) {
13
- const oldest = sweptRoots.values().next().value;
14
- if (oldest === void 0) break;
15
- sweptRoots.delete(oldest);
16
- }
17
- sweptRoots.add(root);
18
- void (async () => {
19
- const cutoff = Date.now() - ARTIFACT_RETENTION_MS;
20
- let sessionDirs;
21
- try {
22
- const entries = await fs.readdir(root, { withFileTypes: true });
23
- sessionDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
24
- } catch {
25
- return;
26
- }
27
- for (const sessionDir of sessionDirs) {
28
- const dir = path.join(root, sessionDir);
29
- let names;
30
- try {
31
- names = await fs.readdir(dir);
32
- } catch {
33
- continue;
34
- }
35
- let removed = 0;
36
- for (const name of names) {
37
- const target = path.join(dir, name);
38
- try {
39
- const stat3 = await fs.stat(target);
40
- if (stat3.isFile() && stat3.mtimeMs < cutoff) {
41
- await fs.rm(target, { force: true });
42
- removed++;
43
- }
44
- } catch {
45
- }
46
- }
47
- if (removed === names.length && names.length > 0) {
48
- await fs.rmdir(dir).catch(() => void 0);
49
- }
50
- }
51
- })();
52
- }
53
- var BrowserArtifactStore = class {
54
- constructor(root) {
55
- this.root = root;
56
- sweepOldArtifacts(root);
57
- }
58
- root;
59
- async write(sessionId, kind, extension, mimeType, content) {
60
- const id = ulid();
61
- const dir = path.join(this.root, safeSegment(sessionId));
62
- const target = path.join(dir, `${id}.${extension.replace(/^\./, "")}`);
63
- await atomicWrite(target, content, { mode: 384 });
64
- return this.record(id, sessionId, kind, target, mimeType);
65
- }
66
- async record(id, sessionId, kind, target, mimeType) {
67
- await fs.chmod(target, 384).catch(() => void 0);
68
- const [stat3, sha256] = await Promise.all([fs.stat(target), hashFile(target)]);
69
- const artifact = {
70
- id,
71
- kind,
72
- sensitivity: "sensitive",
73
- path: target,
74
- mimeType,
75
- sizeBytes: stat3.size,
76
- sha256,
77
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
78
- };
79
- const metadataPath = path.join(path.dirname(target), `${id}.metadata.json`);
80
- try {
81
- await atomicWrite(
82
- metadataPath,
83
- `${JSON.stringify({
84
- id: artifact.id,
85
- sessionId: safeSegment(sessionId),
86
- kind: artifact.kind,
87
- sensitivity: artifact.sensitivity,
88
- mimeType: artifact.mimeType,
89
- sizeBytes: artifact.sizeBytes,
90
- sha256: artifact.sha256,
91
- createdAt: artifact.createdAt
92
- })}
93
- `,
94
- { mode: 384 }
95
- );
96
- } catch (error) {
97
- await fs.rm(target, { force: true }).catch(() => void 0);
98
- throw error;
99
- }
100
- return artifact;
101
- }
102
- pathFor(sessionId, extension) {
103
- const id = ulid();
104
- return {
105
- id,
106
- path: path.join(this.root, safeSegment(sessionId), `${id}.${extension.replace(/^\./, "")}`)
107
- };
108
- }
109
- async describe(id, kind, target, mimeType) {
110
- return this.record(id, path.basename(path.dirname(target)), kind, target, mimeType);
111
- }
112
- };
113
- async function hashFile(target) {
114
- const hash = createHash("sha256");
115
- await new Promise((resolve3, reject) => {
116
- const stream = createReadStream(target);
117
- stream.on("data", (chunk) => hash.update(chunk));
118
- stream.once("error", reject);
119
- stream.once("end", resolve3);
120
- });
121
- return hash.digest("hex");
122
- }
123
- function safeSegment(value) {
124
- return value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
125
- }
126
-
127
- // src/browser/manager.ts
128
- import * as fs2 from "node:fs/promises";
129
- import * as path2 from "node:path";
130
- import { ulid as ulid2 } from "@wrongstack/core/utils";
131
-
132
- // src/browser/network-guard-proxy.ts
133
- import * as http from "node:http";
134
- import * as net2 from "node:net";
135
-
136
- // src/browser/security.ts
137
- import * as dns from "node:dns/promises";
138
- import * as net from "node:net";
139
- import { assertNotPrivateHost, isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
140
- var SAFE_SUBRESOURCE_PROTOCOLS = /* @__PURE__ */ new Set(["about:", "blob:", "data:"]);
141
- async function assertBrowserUrlAllowed(rawUrl, opts) {
142
- const url = parseBrowserUrl(rawUrl, opts.navigation ?? true);
143
- if (!opts.navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
144
- if (!opts.allowPrivateHosts && !isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins)) {
145
- await assertNotPrivateHost(url.hostname);
146
- }
147
- return url;
148
- }
149
- async function resolvePinnedBrowserTarget(rawUrl, opts = {}) {
150
- const url = parseBrowserUrl(rawUrl, true);
151
- const allowPrivate = opts.allowPrivateHosts === true || isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins);
152
- const hostname = unbracket(url.hostname);
153
- const literalFamily = net.isIP(hostname);
154
- if (literalFamily === 4 || literalFamily === 6) {
155
- if (!allowPrivate && isPrivateAddress(hostname, literalFamily)) {
156
- throw new Error(`browser: blocked private/loopback address "${hostname}"`);
157
- }
158
- return { url, address: hostname, family: literalFamily };
159
- }
160
- if ((hostname === "localhost" || hostname.endsWith(".localhost")) && !allowPrivate) {
161
- throw new Error("browser: blocked localhost target");
162
- }
163
- const lookup2 = opts.lookup ?? ((host) => dns.lookup(host, { all: true }));
164
- let records;
165
- try {
166
- records = await lookup2(hostname);
167
- } catch (error) {
168
- throw new Error(
169
- `browser: DNS resolution failed for "${hostname}": ${error instanceof Error ? error.message : String(error)}`
170
- );
171
- }
172
- if (records.length === 0) throw new Error(`browser: DNS returned no addresses for "${hostname}"`);
173
- for (const record of records) {
174
- if (record.family !== 4 && record.family !== 6) {
175
- throw new Error(`browser: DNS returned an unsupported address family for "${hostname}"`);
176
- }
177
- if (!allowPrivate && isPrivateAddress(record.address, record.family)) {
178
- throw new Error(`browser: resolved to private address ${record.address}`);
179
- }
180
- }
181
- const selected = records[0];
182
- return { url, address: selected.address, family: selected.family };
183
- }
184
- function parsePrivateOriginAllowlist(raw) {
185
- if (!raw?.trim()) return [];
186
- const origins = /* @__PURE__ */ new Set();
187
- for (const entry of raw.split(",")) {
188
- const candidate = entry.trim().replace(/^["']+|["']+$/gu, "");
189
- if (!candidate) continue;
190
- const url = parseBrowserUrl(candidate, true);
191
- if (url.pathname !== "/" || url.search || url.hash) {
192
- throw new Error(`browser: private origin allowlist entry must be an origin: "${candidate}"`);
193
- }
194
- origins.add(url.origin);
195
- }
196
- return [...origins];
197
- }
198
- function parseBrowserUrl(rawUrl, navigation) {
199
- let url;
200
- try {
201
- url = new URL(rawUrl);
202
- } catch {
203
- throw new Error("browser: url must be an absolute URL");
204
- }
205
- if (!navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
206
- if (url.protocol !== "http:" && url.protocol !== "https:") {
207
- throw new Error(`browser: unsupported protocol "${url.protocol}"`);
208
- }
209
- if (url.username || url.password) {
210
- throw new Error("browser: credentials in URLs are not allowed");
211
- }
212
- return url;
213
- }
214
- function isAllowedPrivateOrigin(url, origins) {
215
- return origins?.includes(url.origin) ?? false;
216
- }
217
- function isPrivateAddress(address, family) {
218
- return family === 4 ? isPrivateIPv4(address) : isPrivateIPv6(address);
219
- }
220
- function unbracket(hostname) {
221
- return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
222
- }
223
- function safeBrowserUrl(rawUrl) {
224
- try {
225
- const url = new URL(rawUrl);
226
- url.username = "";
227
- url.password = "";
228
- url.search = "";
229
- url.hash = "";
230
- return url.toString();
231
- } catch {
232
- return "about:blank";
233
- }
234
- }
235
- function redactBrowserText(text) {
236
- return text.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]").replace(
237
- /\b(api[-_ ]?key|access[-_ ]?token|auth[-_ ]?token|token|password|secret)\b\s*[:=]\s*[^\s,;]+/gi,
238
- "$1=[REDACTED]"
239
- );
240
- }
241
-
242
- // src/browser/network-guard-proxy.ts
243
- var BrowserNetworkGuardProxy = class {
244
- constructor(options = {}) {
245
- this.options = options;
246
- this.server = http.createServer((request2, response) => {
247
- void this.forwardHttp(request2, response);
248
- });
249
- this.server.on("connect", (request2, socket, head) => {
250
- void this.forwardConnect(request2, socket, head);
251
- });
252
- this.server.on("upgrade", (request2, socket, head) => {
253
- void this.forwardUpgrade(request2, socket, head);
254
- });
255
- this.server.on("connection", (socket) => {
256
- this.sockets.add(socket);
257
- socket.once("close", () => this.sockets.delete(socket));
258
- socket.on("error", () => {
259
- socket.destroy();
260
- });
261
- });
262
- this.server.on("clientError", (_error, socket) => {
263
- socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
264
- });
265
- }
266
- options;
267
- server;
268
- sockets = /* @__PURE__ */ new Set();
269
- startPromise;
270
- url;
271
- async start() {
272
- if (this.url) return this.url;
273
- if (this.startPromise) return this.startPromise;
274
- this.startPromise = new Promise((resolve3, reject) => {
275
- const onError = (error) => {
276
- this.server.off("listening", onListening);
277
- reject(error);
278
- };
279
- const onListening = () => {
280
- this.server.off("error", onError);
281
- const address = this.server.address();
282
- if (!address || typeof address === "string") {
283
- reject(new Error("browser: network guard proxy failed to bind"));
284
- return;
285
- }
286
- this.url = `http://127.0.0.1:${address.port}`;
287
- resolve3(this.url);
288
- };
289
- this.server.once("error", onError);
290
- this.server.once("listening", onListening);
291
- this.server.listen(0, "127.0.0.1");
292
- }).finally(() => {
293
- this.startPromise = void 0;
294
- });
295
- return this.startPromise;
296
- }
297
- async close() {
298
- this.url = void 0;
299
- for (const socket of this.sockets) socket.destroy();
300
- this.sockets.clear();
301
- if (!this.server.listening) return;
302
- await new Promise((resolve3) => this.server.close(() => resolve3()));
303
- }
304
- async forwardHttp(request2, response) {
305
- try {
306
- const target = await this.resolve(request2.url ?? "");
307
- if (target.url.protocol !== "http:") throw new Error("browser: HTTPS must use CONNECT");
308
- const headers = sanitizeHeaders(request2.headers);
309
- headers.host = target.url.host;
310
- const upstream = http.request({
311
- host: target.address,
312
- family: target.family,
313
- port: Number(target.url.port || 80),
314
- method: request2.method,
315
- path: `${target.url.pathname}${target.url.search}`,
316
- headers,
317
- agent: false
318
- });
319
- upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => {
320
- upstream.destroy(new Error("browser: upstream connection timed out"));
321
- });
322
- upstream.on("response", (upstreamResponse) => {
323
- response.writeHead(
324
- upstreamResponse.statusCode ?? 502,
325
- sanitizeHeaders(upstreamResponse.headers)
326
- );
327
- upstreamResponse.pipe(response);
328
- });
329
- upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
330
- request2.on("aborted", () => upstream.destroy());
331
- request2.pipe(upstream);
332
- } catch (error) {
333
- writeProxyError(response, 403, policyBlockMessage(error));
334
- }
335
- }
336
- async forwardConnect(request2, client, head) {
337
- try {
338
- const authority = request2.url ?? "";
339
- const target = await this.resolve(`https://${authority}`);
340
- const upstream = net2.connect({
341
- host: target.address,
342
- family: target.family,
343
- port: Number(target.url.port || 443)
344
- });
345
- upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => upstream.destroy());
346
- const onConnectError = () => {
347
- client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
348
- };
349
- upstream.once("error", onConnectError);
350
- upstream.once("connect", () => {
351
- upstream.off("error", onConnectError);
352
- client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
353
- if (head.length > 0) upstream.write(head);
354
- pipeDuplexPair(upstream, client);
355
- });
356
- client.once("close", () => upstream.destroy());
357
- } catch (error) {
358
- client.end(rawForbiddenResponse(policyBlockMessage(error)));
359
- }
360
- }
361
- async forwardUpgrade(request2, client, head) {
362
- let upstream;
363
- try {
364
- const rawUrl = request2.url ?? "";
365
- const websocketUrl = new URL(rawUrl);
366
- if (websocketUrl.protocol !== "ws:") {
367
- throw new Error("browser: only plain ws upgrades are handled by the HTTP proxy");
368
- }
369
- websocketUrl.protocol = "http:";
370
- const target = await this.resolve(websocketUrl.toString());
371
- const headers = sanitizeHeaders(request2.headers, true);
372
- headers.host = target.url.host;
373
- upstream = http.request({
374
- host: target.address,
375
- family: target.family,
376
- port: Number(target.url.port || 80),
377
- method: request2.method,
378
- path: `${target.url.pathname}${target.url.search}`,
379
- headers,
380
- agent: false
381
- });
382
- upstream.once("upgrade", (response, upstreamSocket, upstreamHead) => {
383
- writeRawResponseHead(client, response);
384
- if (head.length > 0) upstreamSocket.write(head);
385
- if (upstreamHead.length > 0) client.write(upstreamHead);
386
- pipeDuplexPair(upstreamSocket, client);
387
- client.once("close", () => upstreamSocket.destroy());
388
- });
389
- upstream.once("response", (response) => {
390
- writeRawResponseHead(client, response);
391
- response.once("error", () => client.destroy());
392
- response.pipe(client);
393
- });
394
- upstream.once("error", () => {
395
- client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
396
- });
397
- client.once("close", () => upstream?.destroy());
398
- upstream.end();
399
- } catch (error) {
400
- upstream?.destroy();
401
- client.end(rawForbiddenResponse(policyBlockMessage(error)));
402
- }
403
- }
404
- resolve(rawUrl) {
405
- return resolvePinnedBrowserTarget(rawUrl, {
406
- allowPrivateHosts: this.options.allowPrivateHosts,
407
- allowedPrivateOrigins: this.options.allowedPrivateOrigins,
408
- lookup: this.options.lookup
409
- });
410
- }
411
- };
412
- function pipeDuplexPair(left, right) {
413
- left.on("error", () => right.destroy());
414
- right.on("error", () => left.destroy());
415
- left.pipe(right);
416
- right.pipe(left);
417
- }
418
- function sanitizeHeaders(headers, preserveUpgrade = false) {
419
- const sanitized = { ...headers };
420
- delete sanitized["proxy-authorization"];
421
- delete sanitized["proxy-connection"];
422
- if (!preserveUpgrade) delete sanitized.connection;
423
- return sanitized;
424
- }
425
- function writeRawResponseHead(client, response) {
426
- const status = response.statusCode ?? 502;
427
- const statusText = response.statusMessage ?? "Bad Gateway";
428
- const lines = [`HTTP/1.1 ${status} ${statusText}`];
429
- for (let index = 0; index < response.rawHeaders.length; index += 2) {
430
- lines.push(`${response.rawHeaders[index]}: ${response.rawHeaders[index + 1]}`);
431
- }
432
- client.write(`${lines.join("\r\n")}\r
433
- \r
434
- `);
435
- }
436
- function writeProxyError(response, status, message) {
437
- if (response.headersSent || response.destroyed) return;
438
- response.writeHead(status, { "content-type": "text/plain", connection: "close" });
439
- response.end(message);
440
- }
441
- function policyBlockMessage(error) {
442
- const reason = error instanceof Error ? error.message : String(error);
443
- return reason ? `Blocked by browser network policy: ${reason}` : "Blocked by browser network policy";
444
- }
445
- function rawForbiddenResponse(message) {
446
- return `HTTP/1.1 403 Forbidden\r
447
- Content-Type: text/plain\r
448
- Content-Length: ${Buffer.byteLength(message)}\r
449
- Connection: close\r
450
- \r
451
- ` + message;
452
- }
453
-
454
- // src/browser/manager.ts
455
- var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
456
- var DEFAULT_MAX_SNAPSHOT_CHARS = 64 * 1024;
457
- var DEFAULT_MAX_ENTRIES = 100;
458
- var BrowserSessionManager = class {
459
- constructor(options, launcher = defaultLauncher) {
460
- this.launcher = launcher;
461
- this.artifacts = new BrowserArtifactStore(options.artifactRoot);
462
- this.allowPrivateHosts = options.allowPrivateHosts ?? false;
463
- this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
464
- this.networkProxy = new BrowserNetworkGuardProxy({
465
- allowPrivateHosts: this.allowPrivateHosts,
466
- allowedPrivateOrigins: this.allowedPrivateOrigins
467
- });
468
- this.headless = options.headless ?? true;
469
- this.operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
470
- this.maxSnapshotChars = options.maxSnapshotChars ?? DEFAULT_MAX_SNAPSHOT_CHARS;
471
- this.maxConsoleEntries = options.maxConsoleEntries ?? DEFAULT_MAX_ENTRIES;
472
- this.maxNetworkEntries = options.maxNetworkEntries ?? DEFAULT_MAX_ENTRIES;
473
- }
474
- launcher;
475
- sessions = /* @__PURE__ */ new Map();
476
- artifacts;
477
- allowPrivateHosts;
478
- allowedPrivateOrigins;
479
- networkProxy;
480
- headless;
481
- operationTimeoutMs;
482
- maxSnapshotChars;
483
- maxConsoleEntries;
484
- maxNetworkEntries;
485
- browser;
486
- launching;
487
- async open(ownerId, input, signal) {
488
- signal.throwIfAborted();
489
- if (input.url) {
490
- await assertBrowserUrlAllowed(input.url, {
491
- allowPrivateHosts: this.allowPrivateHosts,
492
- allowedPrivateOrigins: this.allowedPrivateOrigins,
493
- navigation: true
494
- });
495
- }
496
- const browser = await this.ensureBrowser(signal);
497
- const proxyServer = await this.networkProxy.start();
498
- const context = await abortable(
499
- signal,
500
- async () => {
501
- const created = await browser.newContext({
502
- acceptDownloads: false,
503
- ignoreHTTPSErrors: false,
504
- serviceWorkers: "block",
505
- proxy: { server: proxyServer },
506
- viewport: input.viewport ?? { width: 1440, height: 900 }
507
- });
508
- if (signal.aborted) {
509
- await created.close().catch(() => void 0);
510
- signal.throwIfAborted();
511
- }
512
- return created;
513
- },
514
- () => void 0
515
- );
516
- const session = {
517
- id: ulid2(),
518
- ownerId,
519
- context,
520
- page: await context.newPage(),
521
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
522
- lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
523
- tracing: input.trace ?? true,
524
- console: [],
525
- network: []
526
- };
527
- this.sessions.set(session.id, session);
528
- this.attachEvidenceCollectors(session);
529
- await context.route("**/*", async (route) => {
530
- try {
531
- await assertBrowserUrlAllowed(route.request().url(), {
532
- allowPrivateHosts: this.allowPrivateHosts,
533
- allowedPrivateOrigins: this.allowedPrivateOrigins,
534
- navigation: false
535
- });
536
- await route.continue();
537
- } catch {
538
- this.pushNetwork(session, {
539
- method: route.request().method(),
540
- url: safeBrowserUrl(route.request().url()),
541
- failed: true,
542
- at: (/* @__PURE__ */ new Date()).toISOString()
543
- });
544
- await route.abort("blockedbyclient");
545
- }
546
- });
547
- try {
548
- if (session.tracing) {
549
- await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
550
- }
551
- if (input.url) await this.navigate(session.id, ownerId, input.url, signal);
552
- return await this.summary(session);
553
- } catch (err) {
554
- await context.close().catch(() => void 0);
555
- this.sessions.delete(session.id);
556
- await this.closeBrowserIfIdle();
557
- throw err;
558
- }
559
- }
560
- async list(ownerId) {
561
- const owned = [...this.sessions.values()].filter((session) => session.ownerId === ownerId);
562
- return Promise.all(owned.map((session) => this.summary(session)));
563
- }
564
- async navigate(id, ownerId, rawUrl, signal) {
565
- await assertBrowserUrlAllowed(rawUrl, {
566
- allowPrivateHosts: this.allowPrivateHosts,
567
- allowedPrivateOrigins: this.allowedPrivateOrigins,
568
- navigation: true
569
- });
570
- const session = this.requireOwned(id, ownerId);
571
- await this.runPageOperation(
572
- session,
573
- signal,
574
- () => session.page.goto(rawUrl, {
575
- waitUntil: "domcontentloaded",
576
- timeout: this.operationTimeoutMs
577
- })
578
- );
579
- return this.summary(session);
580
- }
581
- async snapshot(id, ownerId, signal) {
582
- const session = this.requireOwned(id, ownerId);
583
- const raw = await this.runPageOperation(session, signal, async () => {
584
- try {
585
- return await session.page.locator("body").ariaSnapshot({ timeout: this.operationTimeoutMs });
586
- } catch {
587
- return await session.page.locator("body").innerText({ timeout: this.operationTimeoutMs });
588
- }
589
- });
590
- const aria = redactBrowserText(raw);
591
- const truncated = aria.length > this.maxSnapshotChars;
592
- return {
593
- session: await this.summary(session),
594
- aria: truncated ? `${aria.slice(0, this.maxSnapshotChars)}
595
- \u2026[snapshot truncated]` : aria,
596
- console: session.console.map((entry) => ({ ...entry })),
597
- network: session.network.map((entry) => ({ ...entry })),
598
- truncated
599
- };
600
- }
601
- async screenshot(id, ownerId, input, signal) {
602
- const session = this.requireOwned(id, ownerId);
603
- const bytes = await this.runPageOperation(
604
- session,
605
- signal,
606
- () => input.selector ? session.page.locator(input.selector).screenshot({ type: "png", timeout: this.operationTimeoutMs }) : session.page.screenshot({ type: "png", fullPage: input.fullPage ?? false })
607
- );
608
- signal.throwIfAborted();
609
- return this.artifacts.write(session.id, "screenshot", "png", "image/png", bytes);
610
- }
611
- async click(id, ownerId, selector, signal) {
612
- const session = this.requireOwned(id, ownerId);
613
- await this.runPageOperation(
614
- session,
615
- signal,
616
- () => session.page.locator(selector).click({ timeout: this.operationTimeoutMs })
617
- );
618
- }
619
- async type(id, ownerId, selector, text, signal) {
620
- const session = this.requireOwned(id, ownerId);
621
- await this.runPageOperation(
622
- session,
623
- signal,
624
- () => session.page.locator(selector).fill(text, { timeout: this.operationTimeoutMs })
625
- );
626
- }
627
- async select(id, ownerId, selector, value, signal) {
628
- const session = this.requireOwned(id, ownerId);
629
- await this.runPageOperation(
630
- session,
631
- signal,
632
- () => session.page.locator(selector).selectOption(value, { timeout: this.operationTimeoutMs })
633
- );
634
- }
635
- async press(id, ownerId, key, signal) {
636
- const session = this.requireOwned(id, ownerId);
637
- await this.runPageOperation(session, signal, () => session.page.keyboard.press(key));
638
- }
639
- async hover(id, ownerId, selector, signal) {
640
- const session = this.requireOwned(id, ownerId);
641
- await this.runPageOperation(
642
- session,
643
- signal,
644
- () => session.page.locator(selector).hover({ timeout: this.operationTimeoutMs })
645
- );
646
- }
647
- async wait(id, ownerId, input, signal) {
648
- const session = this.requireOwned(id, ownerId);
649
- const timeout = Math.min(
650
- Math.max(input.timeoutMs ?? this.operationTimeoutMs, 0),
651
- this.operationTimeoutMs
652
- );
653
- await this.runPageOperation(
654
- session,
655
- signal,
656
- () => input.selector ? session.page.locator(input.selector).waitFor({ state: "visible", timeout }) : session.page.waitForTimeout(timeout)
657
- );
658
- }
659
- async drag(id, ownerId, from, to, signal) {
660
- const session = this.requireOwned(id, ownerId);
661
- await this.runPageOperation(
662
- session,
663
- signal,
664
- () => session.page.locator(from).dragTo(session.page.locator(to), {
665
- timeout: this.operationTimeoutMs
666
- })
667
- );
668
- }
669
- async upload(id, ownerId, selector, files, projectRoot, signal) {
670
- const session = this.requireOwned(id, ownerId);
671
- const root = path2.resolve(projectRoot);
672
- const realRoot = await fs2.realpath(root);
673
- const resolved = [];
674
- for (const file of files) {
675
- const absolute = path2.resolve(root, file);
676
- const relative2 = path2.relative(root, absolute);
677
- if (relative2.startsWith("..") || path2.isAbsolute(relative2)) {
678
- throw new Error("browser: upload files must stay inside the project root");
679
- }
680
- let realFile;
681
- try {
682
- realFile = await fs2.realpath(absolute);
683
- } catch (error) {
684
- if (error?.code === "ENOENT") {
685
- throw new Error(`browser: upload file not found: ${file}`);
686
- }
687
- throw error;
688
- }
689
- const realRelative = path2.relative(realRoot, realFile);
690
- if (realRelative.startsWith("..") || path2.isAbsolute(realRelative)) {
691
- throw new Error("browser: upload files must not escape the project root through a symlink");
692
- }
693
- const stat3 = await fs2.stat(realFile);
694
- if (!stat3.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
695
- resolved.push(realFile);
696
- }
697
- await this.runPageOperation(
698
- session,
699
- signal,
700
- () => session.page.locator(selector).setInputFiles(resolved, { timeout: this.operationTimeoutMs })
701
- );
702
- }
703
- async evaluate(id, ownerId, expression, signal) {
704
- const session = this.requireOwned(id, ownerId);
705
- const result = await this.runPageOperation(
706
- session,
707
- signal,
708
- () => session.page.evaluate(expression)
709
- );
710
- const serialized = JSON.stringify(result);
711
- if (serialized && serialized.length > this.maxSnapshotChars) {
712
- throw new Error(`browser: evaluation result exceeds ${this.maxSnapshotChars} characters`);
713
- }
714
- return result;
715
- }
716
- async close(id, ownerId) {
717
- const session = this.requireOwned(id, ownerId);
718
- let trace;
719
- try {
720
- if (session.tracing) {
721
- const target = this.artifacts.pathFor(session.id, "zip");
722
- await fs2.mkdir(path2.dirname(target.path), { recursive: true });
723
- await session.context.tracing.stop({ path: target.path });
724
- trace = await this.artifacts.describe(target.id, "trace", target.path, "application/zip");
725
- }
726
- } finally {
727
- await session.context.close().catch(() => void 0);
728
- this.sessions.delete(id);
729
- await this.closeBrowserIfIdle();
730
- }
731
- return trace ? { trace } : {};
732
- }
733
- async closeOwner(ownerId) {
734
- const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
735
- for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
736
- }
737
- /** True when the manager owns no live browser contexts. */
738
- isIdle() {
739
- return this.sessions.size === 0;
740
- }
741
- async dispose() {
742
- for (const session of [...this.sessions.values()]) {
743
- await session.context.close().catch(() => void 0);
744
- }
745
- this.sessions.clear();
746
- const browser = this.browser;
747
- this.browser = void 0;
748
- this.launching = void 0;
749
- await browser?.close().catch(() => void 0);
750
- await this.networkProxy.close();
751
- }
752
- async ensureBrowser(signal) {
753
- if (this.browser?.isConnected()) return this.browser;
754
- if (!this.launching) {
755
- this.launching = this.launcher(this.headless).then((browser) => {
756
- this.browser = browser;
757
- browser.on("disconnected", () => {
758
- if (this.browser === browser) this.browser = void 0;
759
- for (const [id, session] of this.sessions) {
760
- if (session.context.browser() === browser) this.sessions.delete(id);
761
- }
762
- });
763
- return browser;
764
- });
765
- }
766
- try {
767
- return await abortable(
768
- signal,
769
- () => this.launching,
770
- async () => {
771
- const browser = await this.launching?.catch(() => void 0);
772
- await browser?.close().catch(() => void 0);
773
- }
774
- );
775
- } finally {
776
- this.launching = void 0;
777
- }
778
- }
779
- requireOwned(id, ownerId) {
780
- const session = this.sessions.get(id);
781
- if (!session) throw new Error(`browser: unknown session "${id}"`);
782
- if (session.ownerId !== ownerId) throw new Error("browser: session belongs to another agent");
783
- session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
784
- return session;
785
- }
786
- async summary(session) {
787
- return {
788
- id: session.id,
789
- ownerId: session.ownerId,
790
- url: safeBrowserUrl(session.page.url()),
791
- title: redactBrowserText(await session.page.title().catch(() => "")),
792
- createdAt: session.createdAt,
793
- lastUsedAt: session.lastUsedAt,
794
- tracing: session.tracing
795
- };
796
- }
797
- attachEvidenceCollectors(session) {
798
- session.page.on("console", (message) => {
799
- pushBounded(
800
- session.console,
801
- {
802
- level: message.type(),
803
- text: redactBrowserText(message.text()).slice(0, 4096),
804
- at: (/* @__PURE__ */ new Date()).toISOString()
805
- },
806
- this.maxConsoleEntries
807
- );
808
- });
809
- session.page.on("response", (response) => {
810
- this.pushNetwork(session, {
811
- method: response.request().method(),
812
- url: safeBrowserUrl(response.url()),
813
- status: response.status(),
814
- at: (/* @__PURE__ */ new Date()).toISOString()
815
- });
816
- });
817
- session.page.on("requestfailed", (request2) => {
818
- this.pushNetwork(session, {
819
- method: request2.method(),
820
- url: safeBrowserUrl(request2.url()),
821
- failed: true,
822
- at: (/* @__PURE__ */ new Date()).toISOString()
823
- });
824
- });
825
- }
826
- pushNetwork(session, entry) {
827
- pushBounded(session.network, entry, this.maxNetworkEntries);
828
- }
829
- async runPageOperation(session, signal, operation) {
830
- signal.throwIfAborted();
831
- session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
832
- return abortable(signal, operation, async () => {
833
- await session.context.close().catch(() => void 0);
834
- this.sessions.delete(session.id);
835
- await this.closeBrowserIfIdle();
836
- });
837
- }
838
- async closeBrowserIfIdle() {
839
- if (this.sessions.size > 0 || !this.browser) return;
840
- const browser = this.browser;
841
- this.browser = void 0;
842
- await browser.close().catch(() => void 0);
843
- }
844
- };
845
- async function defaultLauncher(headless) {
846
- try {
847
- const { chromium } = await import("@playwright/test");
848
- return await chromium.launch({ headless });
849
- } catch (err) {
850
- const detail = err instanceof Error ? err.message : String(err);
851
- throw new Error(
852
- `browser: Playwright Chromium is unavailable (${detail}). Run "pnpm exec playwright install chromium".`
853
- );
854
- }
855
- }
856
- async function browserInstallationDiagnostics() {
857
- try {
858
- const { chromium } = await import("@playwright/test");
859
- const executablePath = chromium.executablePath();
860
- await fs2.access(executablePath);
861
- return { available: true, executablePath, message: "Playwright Chromium is available." };
862
- } catch {
863
- return {
864
- available: false,
865
- message: 'Playwright Chromium is unavailable. Run "pnpm exec playwright install chromium".'
866
- };
867
- }
868
- }
869
- function pushBounded(target, value, limit) {
870
- target.push(value);
871
- if (target.length > limit) target.splice(0, target.length - limit);
872
- }
873
- async function abortable(signal, operation, onAbort) {
874
- signal.throwIfAborted();
875
- return new Promise((resolve3, reject) => {
876
- let settled = false;
877
- let aborting = false;
878
- const finish = (fn) => {
879
- if (settled) return;
880
- settled = true;
881
- signal.removeEventListener("abort", abort);
882
- fn();
883
- };
884
- const abort = () => {
885
- aborting = true;
886
- void Promise.resolve(onAbort()).finally(() => {
887
- finish(() => reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted")));
888
- });
889
- };
890
- signal.addEventListener("abort", abort, { once: true });
891
- operation().then(
892
- (value) => {
893
- if (!aborting) finish(() => resolve3(value));
894
- },
895
- (err) => {
896
- if (!aborting) finish(() => reject(err));
897
- }
898
- );
899
- });
900
- }
901
-
902
- // src/browser/tools.ts
903
- import * as path3 from "node:path";
904
- import { ToolCapabilities } from "@wrongstack/core/security";
905
- import { resolveWstackPaths } from "@wrongstack/core/utils";
906
- var managers = /* @__PURE__ */ new Map();
907
- var cleanupSignalByContext = /* @__PURE__ */ new WeakMap();
908
- function managerFor(ctx) {
909
- const root = path3.resolve(ctx.projectRoot);
910
- let manager = managers.get(root);
911
- if (!manager) {
912
- const wpaths = resolveWstackPaths({ projectRoot: root });
913
- manager = new BrowserSessionManager({
914
- artifactRoot: path3.join(wpaths.projectDir, "browser-artifacts"),
915
- allowedPrivateOrigins: parsePrivateOriginAllowlist(
916
- process.env["WRONGSTACK_BROWSER_PRIVATE_ORIGINS"]
917
- )
918
- });
919
- managers.set(root, manager);
920
- }
921
- const runSignal = ctx.signal;
922
- if (cleanupSignalByContext.get(ctx) !== runSignal) {
923
- cleanupSignalByContext.set(ctx, runSignal);
924
- const managerForRun = manager;
925
- const rootForRun = root;
926
- const ownerForRun = owner(ctx);
927
- ctx.registerAbortHook?.(async () => {
928
- if (cleanupSignalByContext.get(ctx) === runSignal) {
929
- cleanupSignalByContext.delete(ctx);
930
- }
931
- await managerForRun.closeOwner(ownerForRun);
932
- if (managerForRun.isIdle() && managers.get(rootForRun) === managerForRun) {
933
- managers.delete(rootForRun);
934
- await managerForRun.dispose();
935
- }
936
- });
937
- }
938
- return manager;
939
- }
940
- function owner(ctx) {
941
- return ctx.agentId || "leader";
942
- }
943
- function json(value) {
944
- return JSON.stringify(value, null, 2);
945
- }
946
- var sessionIdSchema = {
947
- type: "string",
948
- minLength: 1,
949
- description: "Browser session id returned by browser_open."
950
- };
951
- var browserOpenTool = {
952
- name: "browser_open",
953
- description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL. Private/localhost origins are blocked by default; enable specific origins via the WRONGSTACK_BROWSER_PRIVATE_ORIGINS env allowlist.",
954
- usageHint: "browser_open({ url?, width?, height?, trace? })",
955
- permission: "confirm",
956
- mutating: true,
957
- riskTier: "standard",
958
- capabilities: [ToolCapabilities.NET_OUTBOUND],
959
- subjectKey: "url",
960
- inputSchema: {
961
- type: "object",
962
- properties: {
963
- url: { type: "string", description: "Optional absolute http(s) URL." },
964
- width: { type: "integer", minimum: 320, maximum: 3840 },
965
- height: { type: "integer", minimum: 240, maximum: 2160 },
966
- trace: { type: "boolean", description: "Capture a Playwright trace; defaults to true." }
967
- }
968
- },
969
- async execute(input, ctx, opts) {
970
- const viewport = input.width || input.height ? { width: input.width ?? 1440, height: input.height ?? 900 } : void 0;
971
- return json(
972
- await managerFor(ctx).open(
973
- owner(ctx),
974
- { url: input.url, viewport, trace: input.trace },
975
- opts.signal
976
- )
977
- );
978
- }
979
- };
980
- var browserListTool = {
981
- name: "browser_list",
982
- description: "List browser sessions owned by the current agent without exposing other agents sessions.",
983
- permission: "auto",
984
- mutating: false,
985
- riskTier: "safe",
986
- capabilities: [ToolCapabilities.NET_OUTBOUND],
987
- inputSchema: { type: "object", properties: {} },
988
- async execute(_input, ctx) {
989
- return json(await managerFor(ctx).list(owner(ctx)));
990
- }
991
- };
992
- var browserStatusTool = {
993
- name: "browser_status",
994
- description: "Check whether first-party Playwright Chromium is installed and ready to launch.",
995
- permission: "auto",
996
- mutating: false,
997
- riskTier: "safe",
998
- capabilities: [ToolCapabilities.TOOL_META],
999
- inputSchema: { type: "object", properties: {} },
1000
- async execute() {
1001
- return json(await browserInstallationDiagnostics());
1002
- }
1003
- };
1004
- var browserNavigateTool = {
1005
- name: "browser_navigate",
1006
- description: "Navigate an owned browser session to an approved http(s) URL. Private/localhost origins are blocked by default; enable specific origins via the WRONGSTACK_BROWSER_PRIVATE_ORIGINS env allowlist.",
1007
- usageHint: "browser_navigate({ sessionId, url })",
1008
- permission: "confirm",
1009
- mutating: true,
1010
- riskTier: "standard",
1011
- capabilities: [ToolCapabilities.NET_OUTBOUND],
1012
- subjectKey: "url",
1013
- inputSchema: {
1014
- type: "object",
1015
- properties: { sessionId: sessionIdSchema, url: { type: "string", minLength: 1 } },
1016
- required: ["sessionId", "url"]
1017
- },
1018
- async execute(input, ctx, opts) {
1019
- return json(
1020
- await managerFor(ctx).navigate(input.sessionId, owner(ctx), input.url, opts.signal)
1021
- );
1022
- }
1023
- };
1024
- var browserSnapshotTool = {
1025
- name: "browser_snapshot",
1026
- description: "Return bounded accessibility state plus redacted console and network summaries.",
1027
- usageHint: "browser_snapshot({ sessionId })",
1028
- permission: "auto",
1029
- mutating: false,
1030
- riskTier: "safe",
1031
- capabilities: [ToolCapabilities.NET_OUTBOUND],
1032
- inputSchema: {
1033
- type: "object",
1034
- properties: { sessionId: sessionIdSchema },
1035
- required: ["sessionId"]
1036
- },
1037
- async execute(input, ctx, opts) {
1038
- return json(await managerFor(ctx).snapshot(input.sessionId, owner(ctx), opts.signal));
1039
- }
1040
- };
1041
- var browserScreenshotTool = {
1042
- name: "browser_screenshot",
1043
- description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
1044
- usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
1045
- permission: "confirm",
1046
- // WS-046: the element/expression acted on, not the sessionId — a trust
1047
- // rule must distinguish clicking one control from another.
1048
- subjectKey: "selector",
1049
- mutating: true,
1050
- riskTier: "safe",
1051
- capabilities: [ToolCapabilities.NET_OUTBOUND],
1052
- inputSchema: {
1053
- type: "object",
1054
- properties: {
1055
- sessionId: sessionIdSchema,
1056
- fullPage: { type: "boolean" },
1057
- selector: { type: "string", minLength: 1 }
1058
- },
1059
- required: ["sessionId"]
1060
- },
1061
- async execute(input, ctx, opts) {
1062
- return json(
1063
- await managerFor(ctx).screenshot(
1064
- input.sessionId,
1065
- owner(ctx),
1066
- { fullPage: input.fullPage, selector: input.selector },
1067
- opts.signal
1068
- )
1069
- );
1070
- }
1071
- };
1072
- function selectorTool(name, description, run) {
1073
- return {
1074
- name,
1075
- description,
1076
- usageHint: `${name}({ sessionId, selector })`,
1077
- permission: "confirm",
1078
- // WS-046: the element/expression acted on, not the sessionId — a trust
1079
- // rule must distinguish clicking one control from another.
1080
- subjectKey: "selector",
1081
- mutating: true,
1082
- riskTier: "standard",
1083
- capabilities: [ToolCapabilities.NET_OUTBOUND],
1084
- inputSchema: {
1085
- type: "object",
1086
- properties: { sessionId: sessionIdSchema, selector: { type: "string", minLength: 1 } },
1087
- required: ["sessionId", "selector"]
1088
- },
1089
- async execute(input, ctx, opts) {
1090
- await run(managerFor(ctx), input, owner(ctx), opts.signal);
1091
- return "ok";
1092
- }
1093
- };
1094
- }
1095
- var browserClickTool = selectorTool(
1096
- "browser_click",
1097
- "Click an element in an owned browser session.",
1098
- (manager, input, ownerId, signal) => manager.click(input.sessionId, ownerId, input.selector, signal)
1099
- );
1100
- var browserHoverTool = selectorTool(
1101
- "browser_hover",
1102
- "Hover an element in an owned browser session.",
1103
- (manager, input, ownerId, signal) => manager.hover(input.sessionId, ownerId, input.selector, signal)
1104
- );
1105
- var browserTypeTool = {
1106
- name: "browser_type",
1107
- description: "Fill an input in an owned browser session. Use secretEnv instead of text for credentials so values stay out of tool arguments and session audit.",
1108
- usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
1109
- permission: "confirm",
1110
- // WS-046: the element/expression acted on, not the sessionId — a trust
1111
- // rule must distinguish clicking one control from another.
1112
- subjectKey: "selector",
1113
- mutating: true,
1114
- riskTier: "standard",
1115
- capabilities: [ToolCapabilities.NET_OUTBOUND],
1116
- inputSchema: {
1117
- type: "object",
1118
- properties: {
1119
- sessionId: sessionIdSchema,
1120
- selector: { type: "string", minLength: 1 },
1121
- text: { type: "string" },
1122
- secretEnv: {
1123
- type: "string",
1124
- pattern: "^[A-Z_][A-Z0-9_]*$",
1125
- description: "Host environment variable resolved only at execution time."
1126
- }
1127
- },
1128
- required: ["sessionId", "selector"]
1129
- },
1130
- validate(input) {
1131
- return input.text === void 0 === (input.secretEnv === void 0) ? ["exactly one of text or secretEnv is required"] : [];
1132
- },
1133
- async execute(input, ctx, opts) {
1134
- const value = input.text ?? (input.secretEnv ? process.env[input.secretEnv] : void 0);
1135
- if (value === void 0) {
1136
- throw new Error(`browser: secret environment variable "${input.secretEnv ?? ""}" is not set`);
1137
- }
1138
- await managerFor(ctx).type(input.sessionId, owner(ctx), input.selector, value, opts.signal);
1139
- return "ok";
1140
- }
1141
- };
1142
- var browserSelectTool = {
1143
- name: "browser_select",
1144
- description: "Select an option in an owned browser session.",
1145
- usageHint: "browser_select({ sessionId, selector, value })",
1146
- permission: "confirm",
1147
- // WS-046: the element/expression acted on, not the sessionId — a trust
1148
- // rule must distinguish clicking one control from another.
1149
- subjectKey: "selector",
1150
- mutating: true,
1151
- riskTier: "standard",
1152
- capabilities: [ToolCapabilities.NET_OUTBOUND],
1153
- inputSchema: {
1154
- type: "object",
1155
- properties: {
1156
- sessionId: sessionIdSchema,
1157
- selector: { type: "string", minLength: 1 },
1158
- value: { type: "string" }
1159
- },
1160
- required: ["sessionId", "selector", "value"]
1161
- },
1162
- async execute(input, ctx, opts) {
1163
- await managerFor(ctx).select(
1164
- input.sessionId,
1165
- owner(ctx),
1166
- input.selector,
1167
- input.value,
1168
- opts.signal
1169
- );
1170
- return "ok";
1171
- }
1172
- };
1173
- var browserPressTool = {
1174
- name: "browser_press",
1175
- description: "Press a keyboard key in an owned browser session.",
1176
- usageHint: "browser_press({ sessionId, key })",
1177
- permission: "confirm",
1178
- // WS-046: the element/expression acted on, not the sessionId — a trust
1179
- // rule must distinguish clicking one control from another.
1180
- subjectKey: "key",
1181
- mutating: true,
1182
- riskTier: "standard",
1183
- capabilities: [ToolCapabilities.NET_OUTBOUND],
1184
- inputSchema: {
1185
- type: "object",
1186
- properties: { sessionId: sessionIdSchema, key: { type: "string", minLength: 1 } },
1187
- required: ["sessionId", "key"]
1188
- },
1189
- async execute(input, ctx, opts) {
1190
- await managerFor(ctx).press(input.sessionId, owner(ctx), input.key, opts.signal);
1191
- return "ok";
1192
- }
1193
- };
1194
- var browserDragTool = {
1195
- name: "browser_drag",
1196
- description: "Drag one element to another in an owned browser session.",
1197
- usageHint: "browser_drag({ sessionId, from, to })",
1198
- permission: "confirm",
1199
- // WS-046: the element/expression acted on, not the sessionId — a trust
1200
- // rule must distinguish clicking one control from another.
1201
- subjectKey: "from",
1202
- mutating: true,
1203
- riskTier: "standard",
1204
- capabilities: [ToolCapabilities.NET_OUTBOUND],
1205
- inputSchema: {
1206
- type: "object",
1207
- properties: {
1208
- sessionId: sessionIdSchema,
1209
- from: { type: "string", minLength: 1 },
1210
- to: { type: "string", minLength: 1 }
1211
- },
1212
- required: ["sessionId", "from", "to"]
1213
- },
1214
- async execute(input, ctx, opts) {
1215
- await managerFor(ctx).drag(input.sessionId, owner(ctx), input.from, input.to, opts.signal);
1216
- return "ok";
1217
- }
1218
- };
1219
- var browserWaitTool = {
1220
- name: "browser_wait",
1221
- description: "Wait for an element or a bounded duration in an owned browser session.",
1222
- usageHint: "browser_wait({ sessionId, selector?, timeoutMs? })",
1223
- permission: "auto",
1224
- mutating: false,
1225
- riskTier: "safe",
1226
- capabilities: [ToolCapabilities.NET_OUTBOUND],
1227
- inputSchema: {
1228
- type: "object",
1229
- properties: {
1230
- sessionId: sessionIdSchema,
1231
- selector: { type: "string", minLength: 1 },
1232
- timeoutMs: { type: "integer", minimum: 0, maximum: 3e4 }
1233
- },
1234
- required: ["sessionId"]
1235
- },
1236
- async execute(input, ctx, opts) {
1237
- await managerFor(ctx).wait(
1238
- input.sessionId,
1239
- owner(ctx),
1240
- { selector: input.selector, timeoutMs: input.timeoutMs },
1241
- opts.signal
1242
- );
1243
- return "ok";
1244
- }
1245
- };
1246
- var browserEvaluateTool = {
1247
- name: "browser_evaluate",
1248
- description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
1249
- usageHint: "browser_evaluate({ sessionId, expression })",
1250
- permission: "confirm",
1251
- // WS-046: the element/expression acted on, not the sessionId — a trust
1252
- // rule must distinguish clicking one control from another.
1253
- subjectKey: "expression",
1254
- mutating: true,
1255
- riskTier: "destructive",
1256
- capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
1257
- inputSchema: {
1258
- type: "object",
1259
- properties: {
1260
- sessionId: sessionIdSchema,
1261
- expression: { type: "string", minLength: 1, maxLength: 16384 }
1262
- },
1263
- required: ["sessionId", "expression"]
1264
- },
1265
- async execute(input, ctx, opts) {
1266
- return json(
1267
- await managerFor(ctx).evaluate(input.sessionId, owner(ctx), input.expression, opts.signal)
1268
- );
1269
- }
1270
- };
1271
- var browserUploadTool = {
1272
- name: "browser_upload",
1273
- description: "Upload project-local files through a file input in an owned browser session.",
1274
- usageHint: "browser_upload({ sessionId, selector, files })",
1275
- permission: "confirm",
1276
- // WS-046: the element/expression acted on, not the sessionId — a trust
1277
- // rule must distinguish clicking one control from another.
1278
- subjectKey: "selector",
1279
- mutating: true,
1280
- riskTier: "destructive",
1281
- capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
1282
- inputSchema: {
1283
- type: "object",
1284
- properties: {
1285
- sessionId: sessionIdSchema,
1286
- selector: { type: "string", minLength: 1 },
1287
- files: { type: "array", minItems: 1, maxItems: 20, items: { type: "string", minLength: 1 } }
1288
- },
1289
- required: ["sessionId", "selector", "files"]
1290
- },
1291
- async execute(input, ctx, opts) {
1292
- await managerFor(ctx).upload(
1293
- input.sessionId,
1294
- owner(ctx),
1295
- input.selector,
1296
- input.files,
1297
- ctx.projectRoot,
1298
- opts.signal
1299
- );
1300
- return "ok";
1301
- }
1302
- };
1303
- var browserCloseTool = {
1304
- name: "browser_close",
1305
- description: "Close an owned browser session, reclaim its context, and return sensitive trace metadata.",
1306
- usageHint: "browser_close({ sessionId })",
1307
- permission: "auto",
1308
- mutating: false,
1309
- riskTier: "safe",
1310
- capabilities: [ToolCapabilities.NET_OUTBOUND],
1311
- inputSchema: {
1312
- type: "object",
1313
- properties: { sessionId: sessionIdSchema },
1314
- required: ["sessionId"]
1315
- },
1316
- async execute(input, ctx) {
1317
- return json(await managerFor(ctx).close(input.sessionId, owner(ctx)));
1318
- }
1319
- };
1320
- var browserTools = [
1321
- browserOpenTool,
1322
- browserStatusTool,
1
+ import "../chunk-Z6DAKQ3U.js";
2
+ import {
3
+ BrowserArtifactStore,
4
+ BrowserNetworkGuardProxy,
5
+ BrowserSessionManager,
6
+ assertBrowserUrlAllowed,
7
+ browserClickTool,
8
+ browserCloseTool,
9
+ browserDragTool,
10
+ browserEvaluateTool,
11
+ browserHoverTool,
12
+ browserInstallationDiagnostics,
1323
13
  browserListTool,
1324
14
  browserNavigateTool,
1325
- browserSnapshotTool,
15
+ browserOpenTool,
16
+ browserPressTool,
1326
17
  browserScreenshotTool,
1327
- browserClickTool,
1328
- browserTypeTool,
1329
18
  browserSelectTool,
1330
- browserPressTool,
1331
- browserHoverTool,
1332
- browserDragTool,
1333
- browserWaitTool,
1334
- browserEvaluateTool,
19
+ browserSnapshotTool,
20
+ browserStatusTool,
21
+ browserTools,
22
+ browserTypeTool,
1335
23
  browserUploadTool,
1336
- browserCloseTool
1337
- ];
1338
- for (const tool of browserTools) tool.icon = "web";
1339
- for (const tool of browserTools) tool.timeoutMs ??= 6e4;
1340
- async function shutdownBrowserTools() {
1341
- const active = [...managers.values()];
1342
- managers.clear();
1343
- await Promise.all(active.map((manager) => manager.dispose()));
1344
- }
24
+ browserWaitTool,
25
+ parsePrivateOriginAllowlist,
26
+ redactBrowserText,
27
+ resolvePinnedBrowserTarget,
28
+ safeBrowserUrl,
29
+ shutdownBrowserTools
30
+ } from "../chunk-XMIFYKUT.js";
1345
31
  export {
1346
32
  BrowserArtifactStore,
1347
33
  BrowserNetworkGuardProxy,