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