@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.
- package/dist/_edit-match.d.ts +3 -2
- package/dist/_output-spool.d.ts +4 -3
- package/dist/_shell-pick.d.ts +2 -1
- package/dist/_spawn-stream.d.ts +2 -1
- package/dist/_syntax-check.d.ts +2 -1
- package/dist/audit.js +10 -4055
- package/dist/bash-kill-guard.d.ts +3 -6
- package/dist/bash.js +9 -2308
- package/dist/batch-tool-use.js +2 -187
- package/dist/browser/index.js +25 -1339
- package/dist/browser/network-guard-proxy.d.ts +2 -1
- package/dist/builtin.js +69 -33949
- package/dist/chunk-22B7GE5T.js +260 -0
- package/dist/chunk-2VSZC6QM.js +113 -0
- package/dist/chunk-3PVYQFRQ.js +472 -0
- package/dist/chunk-42BHI7TN.js +337 -0
- package/dist/chunk-4BHUSMNL.js +104 -0
- package/dist/chunk-4I4LWFOO.js +431 -0
- package/dist/chunk-4UESMQDP.js +122 -0
- package/dist/chunk-553IQUMO.js +478 -0
- package/dist/chunk-5H4GSJAI.js +223 -0
- package/dist/chunk-6RNIFLG4.js +305 -0
- package/dist/chunk-7EY5DCRO.js +434 -0
- package/dist/chunk-A6BRWIXN.js +317 -0
- package/dist/chunk-AHHDWNXC.js +200 -0
- package/dist/chunk-BSLVR56S.js +199 -0
- package/dist/chunk-BTSFTGIL.js +7 -0
- package/dist/chunk-D33S4RQG.js +222 -0
- package/dist/chunk-DA4KNE2H.js +4412 -0
- package/dist/chunk-DR7JOQTC.js +520 -0
- package/dist/chunk-EEOFQDT6.js +898 -0
- package/dist/chunk-EJWSCQ6D.js +137 -0
- package/dist/chunk-EMRGN4KM.js +170 -0
- package/dist/chunk-FQZOHLG3.js +89 -0
- package/dist/chunk-GOIZNNOA.js +221 -0
- package/dist/chunk-HFXSNKCV.js +196 -0
- package/dist/chunk-HYX2TZ7O.js +1272 -0
- package/dist/chunk-I64BS3VC.js +15 -0
- package/dist/chunk-IVWV2UM6.js +955 -0
- package/dist/chunk-IXMNTNW5.js +238 -0
- package/dist/chunk-IYK7XGDZ.js +892 -0
- package/dist/chunk-J5SKD6LN.js +235 -0
- package/dist/chunk-JNNVRHMV.js +2111 -0
- package/dist/chunk-JOHXDF3D.js +295 -0
- package/dist/chunk-JRJJX3L5.js +424 -0
- package/dist/chunk-K3WNSHUF.js +1371 -0
- package/dist/chunk-L25CVUVR.js +2830 -0
- package/dist/chunk-LIJQMYYX.js +59 -0
- package/dist/chunk-LWKUTMD5.js +197 -0
- package/dist/chunk-MQX2KXVW.js +54 -0
- package/dist/chunk-MZZN7VCD.js +359 -0
- package/dist/chunk-NBDZMMG2.js +169 -0
- package/dist/chunk-NKJW5GFO.js +366 -0
- package/dist/chunk-NKNOBUBU.js +522 -0
- package/dist/chunk-O4MBV4AX.js +134 -0
- package/dist/chunk-OUBDL3WH.js +3515 -0
- package/dist/chunk-PQKK5JH7.js +101 -0
- package/dist/chunk-PY2IOS7C.js +1000 -0
- package/dist/chunk-Q5JPPCZC.js +12 -0
- package/dist/chunk-QBV725YG.js +128 -0
- package/dist/chunk-QZQSPUBF.js +315 -0
- package/dist/chunk-RGISBZBD.js +231 -0
- package/dist/chunk-RJZQWEMS.js +84 -0
- package/dist/chunk-RXISBFR5.js +315 -0
- package/dist/chunk-S5CCLZL3.js +99 -0
- package/dist/chunk-SDLVH5RZ.js +178 -0
- package/dist/chunk-T2F4S7U4.js +1723 -0
- package/dist/chunk-TEBQ4VPZ.js +163 -0
- package/dist/chunk-U4D5S3VQ.js +21 -0
- package/dist/chunk-U4VAATKP.js +193 -0
- package/dist/chunk-UCYQTZ7F.js +113 -0
- package/dist/chunk-UKU2ZX4C.js +203 -0
- package/dist/chunk-UNSKG6DC.js +159 -0
- package/dist/chunk-V43EQ6VR.js +68 -0
- package/dist/chunk-VB3MJGCV.js +610 -0
- package/dist/chunk-VFKPWSOX.js +154 -0
- package/dist/chunk-VJNHFZ7J.js +190 -0
- package/dist/chunk-VXECAJJ7.js +1091 -0
- package/dist/chunk-WHIIIREL.js +223 -0
- package/dist/chunk-Z6DAKQ3U.js +1 -0
- package/dist/chunk-ZJQ4IGXK.js +474 -0
- package/dist/chunk-ZLTFLC25.js +385 -0
- package/dist/circuit-breaker.js +3 -191
- package/dist/codebase-index/ast-invariant-engine.d.ts +5 -4
- package/dist/codebase-index/binary-frame.d.ts +0 -2
- package/dist/codebase-index/bm25.d.ts +1 -1
- package/dist/codebase-index/circuit-breaker.d.ts +2 -1
- package/dist/codebase-index/codebase-ast-replace-tool.d.ts +3 -2
- package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +4 -3
- package/dist/codebase-index/codebase-invariant-check-tool.d.ts +3 -2
- package/dist/codebase-index/codebase-repo-map-tool.d.ts +3 -2
- package/dist/codebase-index/codebase-skeleton-tool.d.ts +3 -2
- package/dist/codebase-index/codebase-targeted-test-tool.d.ts +3 -2
- package/dist/codebase-index/content-hash.d.ts +0 -2
- package/dist/codebase-index/import-extractor.d.ts +0 -13
- package/dist/codebase-index/index-service.d.ts +1 -1
- package/dist/codebase-index/index.js +84 -13688
- package/dist/codebase-index/languages.d.ts +2 -1
- package/dist/codebase-index/module-roots.d.ts +3 -2
- package/dist/codebase-index/parser-batch.d.ts +0 -5
- package/dist/codebase-index/parser-dispatch.d.ts +2 -1
- package/dist/codebase-index/parser-output.d.ts +4 -3
- package/dist/codebase-index/parser-worker-pool.d.ts +0 -2
- package/dist/codebase-index/parser-worker-script.d.ts +1 -16
- package/dist/codebase-index/parser-worker-script.js +8 -3995
- package/dist/codebase-index/project-server-client-state.d.ts +2 -2
- package/dist/codebase-index/project-server-endpoint.d.ts +0 -15
- package/dist/codebase-index/project-server.js +60 -8548
- package/dist/codebase-index/tree-sitter/visitor.d.ts +2 -1
- package/dist/codebase-index/tree-sitter-parser.d.ts +0 -2
- package/dist/codebase-index/ts-parser.d.ts +1 -1
- package/dist/codebase-index/vector-search.d.ts +2 -1
- package/dist/codebase-index/wal-maintenance.d.ts +3 -2
- package/dist/codebase-index/worker.js +20 -8416
- package/dist/codebase-index/writer-admin.d.ts +0 -1
- package/dist/codebase-index/writer-search.d.ts +0 -9
- package/dist/design.js +2 -299
- package/dist/diff.js +4 -256
- package/dist/document.js +4 -222
- package/dist/e2e.js +5 -476
- package/dist/edit.js +14 -10200
- package/dist/exec.js +16 -3131
- package/dist/fetch.js +8 -398
- package/dist/format.js +10 -3750
- package/dist/generic-parser-VUXDIAOZ.js +13 -0
- package/dist/git.js +4 -380
- package/dist/glob.js +6 -280
- package/dist/go-parser-JXYKJHRC.js +469 -0
- package/dist/grep.js +7 -604
- package/dist/index.js +318 -35387
- package/dist/install.js +10 -4091
- package/dist/json-parser-IBHZ4BZC.js +213 -0
- package/dist/json.js +4 -665
- package/dist/kanban-contract-actions.d.ts +1 -1
- package/dist/kanban-detail-actions.d.ts +2 -1
- package/dist/kanban-presence.d.ts +2 -1
- package/dist/kanban-serializer.d.ts +0 -8
- package/dist/kanban-split-task-handler.d.ts +2 -2
- package/dist/kanban-tool-types.d.ts +2 -1
- package/dist/kanban.js +3 -2096
- package/dist/languages/diagnostics.d.ts +2 -1
- package/dist/languages/index.js +23 -4664
- package/dist/languages/legacy-bridge.d.ts +3 -2
- package/dist/languages/types.d.ts +2 -1
- package/dist/legacy-bridge-7M6QLUQN.js +18 -0
- package/dist/lint.js +10 -3706
- package/dist/logs.js +4 -287
- package/dist/memory.js +6 -215
- package/dist/mode.js +3 -99
- package/dist/outdated.js +4 -5170
- package/dist/pack.js +65 -33897
- package/dist/parser-dispatch-QH4XPGG6.js +14 -0
- package/dist/patch.js +13 -9935
- package/dist/plan.js +5 -3286
- package/dist/process-registry.js +8 -651
- package/dist/ps-slash.js +12 -1439
- package/dist/pwsh.d.ts +0 -1
- package/dist/py-parser-IS4HKYMF.js +16 -0
- package/dist/read.js +13 -9816
- package/dist/replace.js +15 -10014
- package/dist/rs-parser-XHUUO2KZ.js +81 -0
- package/dist/scaffold.js +4 -253
- package/dist/search.js +5 -585
- package/dist/session-kanban-sync.d.ts +0 -6
- package/dist/session-kanban.js +30 -972
- package/dist/skill.js +2 -215
- package/dist/spawn-background.d.ts +1 -1
- package/dist/task.js +5 -3410
- package/dist/test.js +10 -3773
- package/dist/todo.js +4 -2891
- package/dist/tool-help.js +3 -123
- package/dist/tool-search.js +3 -79
- package/dist/tool-tier.js +68 -33984
- package/dist/tool-use.js +2 -93
- package/dist/tree-sitter-parser-AIERW3YI.js +15 -0
- package/dist/tree.js +4 -288
- package/dist/ts-parser-RCTYKSVI.js +232 -0
- package/dist/typecheck.js +10 -3753
- package/dist/win32.js +6 -60
- package/dist/write.js +14 -9882
- package/dist/yaml-parser-4SAXV3PL.js +157 -0
- package/package.json +5 -5
package/dist/browser/index.js
CHANGED
|
@@ -1,1347 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
+
browserOpenTool,
|
|
16
|
+
browserPressTool,
|
|
1326
17
|
browserScreenshotTool,
|
|
1327
|
-
browserClickTool,
|
|
1328
|
-
browserTypeTool,
|
|
1329
18
|
browserSelectTool,
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
browserEvaluateTool,
|
|
19
|
+
browserSnapshotTool,
|
|
20
|
+
browserStatusTool,
|
|
21
|
+
browserTools,
|
|
22
|
+
browserTypeTool,
|
|
1335
23
|
browserUploadTool,
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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-K3WNSHUF.js";
|
|
1345
31
|
export {
|
|
1346
32
|
BrowserArtifactStore,
|
|
1347
33
|
BrowserNetworkGuardProxy,
|