@melaya/runner 1.0.118 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assistantHost.py +144 -8
- package/dist/browserAuthz.d.ts +78 -0
- package/dist/browserAuthz.js +504 -0
- package/dist/browserBridge.d.ts +50 -0
- package/dist/browserBridge.js +1021 -0
- package/dist/browserGrantVerify.d.ts +91 -0
- package/dist/browserGrantVerify.js +353 -0
- package/dist/browserProvisioner.d.ts +33 -0
- package/dist/browserProvisioner.js +266 -0
- package/dist/codeWorker.d.ts +49 -0
- package/dist/codeWorker.js +538 -0
- package/dist/connection.js +270 -3
- package/dist/sessionManager.d.ts +108 -0
- package/dist/sessionManager.js +290 -0
- package/package.json +4 -2
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// packages/runner/src/browserProvisioner.ts
|
|
2
|
+
//
|
|
3
|
+
// Melaya Browser, Phase 1 (plan Section 7): discovery + provisioning of
|
|
4
|
+
// controllable browser engines on the operator's box. Brave is a
|
|
5
|
+
// FIRST-CLASS, user-selectable engine alongside Chrome and Edge (user
|
|
6
|
+
// decision, plan Section 7). The runner ships NO browser binaries; this
|
|
7
|
+
// module only discovers what is installed, reports versions, and (only
|
|
8
|
+
// behind an explicit flag) provisions the Playwright-bundled Chromium.
|
|
9
|
+
// It never silently downloads anything.
|
|
10
|
+
//
|
|
11
|
+
// Brave quirks the capability report surfaces (plan Section 7): Shields
|
|
12
|
+
// can block or rewrite page requests, which may alter snapshots and
|
|
13
|
+
// trip origin policy in ways Chrome would not; Brave Rewards/Wallet
|
|
14
|
+
// surfaces add browser-internal pages; Brave uses a distinct
|
|
15
|
+
// user-data-dir layout. All three engines are Chromium-based, so CDP
|
|
16
|
+
// launch + connectOverCDP work identically.
|
|
17
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
18
|
+
import { execFile } from "node:child_process";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
const BRAVE_QUIRKS = [
|
|
22
|
+
"brave_shields_may_alter_requests_and_snapshots",
|
|
23
|
+
"brave_internal_surfaces_rewards_wallet",
|
|
24
|
+
"brave_distinct_user_data_dir",
|
|
25
|
+
];
|
|
26
|
+
// ---------------------------------------------------------------------
|
|
27
|
+
// Per-OS well-known install paths
|
|
28
|
+
// ---------------------------------------------------------------------
|
|
29
|
+
function windowsCandidates() {
|
|
30
|
+
const pf = process.env["ProgramFiles"] || "C:\\Program Files";
|
|
31
|
+
const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
|
32
|
+
const local = process.env["LOCALAPPDATA"] || join(os.homedir(), "AppData", "Local");
|
|
33
|
+
return [
|
|
34
|
+
{ engine: "chrome", path: join(pf, "Google", "Chrome", "Application", "chrome.exe"), channel: "chrome", quirks: [] },
|
|
35
|
+
{ engine: "chrome", path: join(pf86, "Google", "Chrome", "Application", "chrome.exe"), channel: "chrome", quirks: [] },
|
|
36
|
+
{ engine: "chrome", path: join(local, "Google", "Chrome", "Application", "chrome.exe"), channel: "chrome", quirks: [] },
|
|
37
|
+
{ engine: "edge", path: join(pf86, "Microsoft", "Edge", "Application", "msedge.exe"), channel: "msedge", quirks: [] },
|
|
38
|
+
{ engine: "edge", path: join(pf, "Microsoft", "Edge", "Application", "msedge.exe"), channel: "msedge", quirks: [] },
|
|
39
|
+
{ engine: "brave", path: join(pf, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), channel: null, quirks: BRAVE_QUIRKS },
|
|
40
|
+
{ engine: "brave", path: join(pf86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), channel: null, quirks: BRAVE_QUIRKS },
|
|
41
|
+
{ engine: "brave", path: join(local, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), channel: null, quirks: BRAVE_QUIRKS },
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function macCandidates() {
|
|
45
|
+
const userApps = join(os.homedir(), "Applications");
|
|
46
|
+
return [
|
|
47
|
+
// System-wide installs (/Applications) then per-user installs (~/Applications)
|
|
48
|
+
// for every engine, not just Brave: a user can install any of the three
|
|
49
|
+
// into their own Applications folder without admin rights.
|
|
50
|
+
{ engine: "chrome", path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", channel: "chrome", quirks: [] },
|
|
51
|
+
{ engine: "chrome", path: join(userApps, "Google Chrome.app", "Contents", "MacOS", "Google Chrome"), channel: "chrome", quirks: [] },
|
|
52
|
+
{ engine: "edge", path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", channel: "msedge", quirks: [] },
|
|
53
|
+
{ engine: "edge", path: join(userApps, "Microsoft Edge.app", "Contents", "MacOS", "Microsoft Edge"), channel: "msedge", quirks: [] },
|
|
54
|
+
{ engine: "brave", path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", channel: null, quirks: BRAVE_QUIRKS },
|
|
55
|
+
{ engine: "brave", path: join(userApps, "Brave Browser.app", "Contents", "MacOS", "Brave Browser"), channel: null, quirks: BRAVE_QUIRKS },
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
function linuxCandidates() {
|
|
59
|
+
const bins = [
|
|
60
|
+
["chrome", "google-chrome", "chrome", []],
|
|
61
|
+
["chrome", "google-chrome-stable", "chrome", []],
|
|
62
|
+
["edge", "microsoft-edge", "msedge", []],
|
|
63
|
+
["edge", "microsoft-edge-stable", "msedge", []],
|
|
64
|
+
["brave", "brave", null, BRAVE_QUIRKS],
|
|
65
|
+
["brave", "brave-browser", null, BRAVE_QUIRKS],
|
|
66
|
+
];
|
|
67
|
+
const dirs = ["/usr/bin", "/usr/local/bin", "/opt/brave.com/brave", "/snap/bin"];
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const [engine, bin, channel, quirks] of bins) {
|
|
70
|
+
for (const d of dirs)
|
|
71
|
+
out.push({ engine, path: join(d, bin), channel, quirks });
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------
|
|
76
|
+
// Version detection
|
|
77
|
+
// ---------------------------------------------------------------------
|
|
78
|
+
/** Windows: read the version-named subdirectory next to the executable
|
|
79
|
+
* (e.g. Application/139.0.7258.67/).
|
|
80
|
+
* macOS: try reading CFBundleShortVersionString from the bundle's
|
|
81
|
+
* Info.plist (reliable, no subprocess), then fall back to
|
|
82
|
+
* running the binary with --version if the plist is absent or
|
|
83
|
+
* malformed. Both paths avoid spawning a headed browser window.
|
|
84
|
+
* Linux: `<exe> --version` prints "Google Chrome 139.0...".
|
|
85
|
+
*
|
|
86
|
+
* On all platforms a best-effort empty string is returned rather than
|
|
87
|
+
* throwing, because a missing version must not block engine discovery. */
|
|
88
|
+
async function detectVersion(exePath) {
|
|
89
|
+
if (process.platform === "win32") {
|
|
90
|
+
try {
|
|
91
|
+
const appDir = exePath.replace(/[\\/][^\\/]+$/, "");
|
|
92
|
+
const versionDirs = readdirSync(appDir)
|
|
93
|
+
.filter((n) => /^\d+\.\d+\.\d+\.\d+$/.test(n))
|
|
94
|
+
.sort((a, b) => compareVersions(b, a));
|
|
95
|
+
return versionDirs[0] || "";
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (process.platform === "darwin") {
|
|
102
|
+
// Primary: read Info.plist inside the .app bundle. The executable is at
|
|
103
|
+
// <Bundle>.app/Contents/MacOS/<Binary>; the plist is at
|
|
104
|
+
// <Bundle>.app/Contents/Info.plist. No subprocess needed.
|
|
105
|
+
try {
|
|
106
|
+
// Walk up from the binary to the .app Contents/ directory.
|
|
107
|
+
// exePath = /Applications/Brave Browser.app/Contents/MacOS/Brave Browser
|
|
108
|
+
// plistPath = /Applications/Brave Browser.app/Contents/Info.plist
|
|
109
|
+
const contentsDir = join(exePath, "..", ".."); // MacOS/../.. = Contents/
|
|
110
|
+
const plistPath = join(contentsDir, "Info.plist");
|
|
111
|
+
const { readFileSync: _readFileSync } = await import("node:fs");
|
|
112
|
+
const plist = _readFileSync(plistPath, "utf-8");
|
|
113
|
+
// Scan for the key/string pair in the XML plist without a full parser.
|
|
114
|
+
const keyIdx = plist.indexOf("<key>CFBundleShortVersionString</key>");
|
|
115
|
+
if (keyIdx !== -1) {
|
|
116
|
+
const after = plist.slice(keyIdx + "<key>CFBundleShortVersionString</key>".length);
|
|
117
|
+
const m = after.match(/<string>([^<]+)<\/string>/);
|
|
118
|
+
if (m) {
|
|
119
|
+
const v = m[1].trim();
|
|
120
|
+
if (/^\d+/.test(v))
|
|
121
|
+
return v;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Fall through to --version subprocess below.
|
|
127
|
+
}
|
|
128
|
+
// Fallback: run the binary with --version. Some Chromium builds print
|
|
129
|
+
// "Chromium 139.0.7258.67" or "Google Chrome 139.0.7258.67" to stdout.
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
execFile(exePath, ["--version"], { timeout: 5000 }, (err, stdout) => {
|
|
132
|
+
if (err)
|
|
133
|
+
return resolve("");
|
|
134
|
+
const m = String(stdout || "").match(/(\d+\.\d+\.\d+(?:\.\d+)?)/);
|
|
135
|
+
resolve(m ? m[1] : "");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// Linux (and any other POSIX platform).
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
execFile(exePath, ["--version"], { timeout: 5000 }, (err, stdout) => {
|
|
142
|
+
if (err)
|
|
143
|
+
return resolve("");
|
|
144
|
+
const m = String(stdout || "").match(/(\d+\.\d+\.\d+(?:\.\d+)?)/);
|
|
145
|
+
resolve(m ? m[1] : "");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function compareVersions(a, b) {
|
|
150
|
+
const pa = a.split(".").map(Number);
|
|
151
|
+
const pb = b.split(".").map(Number);
|
|
152
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
153
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
154
|
+
if (d !== 0)
|
|
155
|
+
return d;
|
|
156
|
+
}
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------
|
|
160
|
+
// Discovery (cached; a runner's install set changes rarely)
|
|
161
|
+
// ---------------------------------------------------------------------
|
|
162
|
+
let _discoveryCache = null;
|
|
163
|
+
export function discoverEngines(forceRefresh = false) {
|
|
164
|
+
if (_discoveryCache && !forceRefresh)
|
|
165
|
+
return _discoveryCache;
|
|
166
|
+
_discoveryCache = (async () => {
|
|
167
|
+
const candidates = process.platform === "win32" ? windowsCandidates()
|
|
168
|
+
: process.platform === "darwin" ? macCandidates()
|
|
169
|
+
: linuxCandidates();
|
|
170
|
+
const found = new Map();
|
|
171
|
+
for (const c of candidates) {
|
|
172
|
+
if (found.has(c.engine))
|
|
173
|
+
continue; // first hit per engine wins
|
|
174
|
+
if (!existsSync(c.path))
|
|
175
|
+
continue;
|
|
176
|
+
const version = await detectVersion(c.path);
|
|
177
|
+
found.set(c.engine, {
|
|
178
|
+
engine: c.engine,
|
|
179
|
+
executablePath: c.path,
|
|
180
|
+
version,
|
|
181
|
+
channel: c.channel,
|
|
182
|
+
quirks: c.quirks,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// Playwright-bundled Chromium, when already provisioned on disk.
|
|
186
|
+
try {
|
|
187
|
+
const playwright = await import("playwright");
|
|
188
|
+
const p = playwright.chromium.executablePath();
|
|
189
|
+
if (p && existsSync(p)) {
|
|
190
|
+
found.set("chromium", {
|
|
191
|
+
engine: "chromium",
|
|
192
|
+
executablePath: p,
|
|
193
|
+
version: "",
|
|
194
|
+
channel: null,
|
|
195
|
+
quirks: [],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch { /* playwright unavailable -> no bundled engine */ }
|
|
200
|
+
return [...found.values()];
|
|
201
|
+
})();
|
|
202
|
+
return _discoveryCache;
|
|
203
|
+
}
|
|
204
|
+
export async function capabilityReport() {
|
|
205
|
+
return {
|
|
206
|
+
engines: await discoverEngines(),
|
|
207
|
+
cdp: true,
|
|
208
|
+
provisioner: "v1",
|
|
209
|
+
platform: process.platform,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// ---------------------------------------------------------------------
|
|
213
|
+
// Provisioning
|
|
214
|
+
// ---------------------------------------------------------------------
|
|
215
|
+
export class ProvisionError extends Error {
|
|
216
|
+
code;
|
|
217
|
+
instructions;
|
|
218
|
+
constructor(code, message, instructions) {
|
|
219
|
+
super(message);
|
|
220
|
+
this.name = "ProvisionError";
|
|
221
|
+
this.code = code;
|
|
222
|
+
this.instructions = instructions;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Resolve an engine to a launchable executable. NEVER downloads unless
|
|
226
|
+
* allowDownload is explicitly true, and even then only the
|
|
227
|
+
* Playwright-bundled Chromium is downloadable (Chrome/Edge/Brave are
|
|
228
|
+
* user-installed products we will not fetch on the user's behalf). */
|
|
229
|
+
export async function ensureEngine(engine, opts = {}) {
|
|
230
|
+
const engines = await discoverEngines();
|
|
231
|
+
const hit = engines.find((e) => e.engine === engine);
|
|
232
|
+
if (hit)
|
|
233
|
+
return hit;
|
|
234
|
+
if (engine !== "chromium") {
|
|
235
|
+
const urls = {
|
|
236
|
+
chrome: "https://www.google.com/chrome/",
|
|
237
|
+
edge: "https://www.microsoft.com/edge",
|
|
238
|
+
brave: "https://brave.com/download/",
|
|
239
|
+
};
|
|
240
|
+
throw new ProvisionError("engine_not_installed", `${engine} is not installed on this machine`, `Install ${engine} from ${urls[engine]} and reconnect the runner.`);
|
|
241
|
+
}
|
|
242
|
+
if (!opts.allowDownload) {
|
|
243
|
+
throw new ProvisionError("download_not_authorized", "bundled Chromium is not on disk and download was not authorized", "Re-run with the download flag enabled, or run: npx playwright install chromium");
|
|
244
|
+
}
|
|
245
|
+
// Explicitly authorized: install the bundled Chromium via Playwright's
|
|
246
|
+
// own installer (respects HTTPS_PROXY, resumable, disk-space checked
|
|
247
|
+
// by the installer itself).
|
|
248
|
+
opts.log?.("Downloading Playwright Chromium (explicitly authorized)...");
|
|
249
|
+
await new Promise((resolve, reject) => {
|
|
250
|
+
const child = execFile(process.execPath, [join("node_modules", "playwright", "cli.js"), "install", "chromium"], { cwd: packageRootGuess(), timeout: 10 * 60 * 1000 }, (err) => (err ? reject(err) : resolve()));
|
|
251
|
+
child.stdout?.on("data", (d) => opts.log?.(String(d).trim()));
|
|
252
|
+
}).catch((e) => {
|
|
253
|
+
throw new ProvisionError("download_failed", `Chromium download failed: ${e?.message || e}`);
|
|
254
|
+
});
|
|
255
|
+
const refreshed = await discoverEngines(true);
|
|
256
|
+
const post = refreshed.find((e) => e.engine === "chromium");
|
|
257
|
+
if (!post)
|
|
258
|
+
throw new ProvisionError("download_failed", "Chromium installed but executable not found");
|
|
259
|
+
return post;
|
|
260
|
+
}
|
|
261
|
+
function packageRootGuess() {
|
|
262
|
+
// dist/ layout: this file compiles to dist/browserProvisioner.js; the
|
|
263
|
+
// package root (with node_modules/playwright) is one level up. In the
|
|
264
|
+
// dev tree it is two up from src/, which the fallback covers.
|
|
265
|
+
return join(new URL(".", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"), "..");
|
|
266
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Facade the BRIDGE provides: executes one governed op. The bridge
|
|
2
|
+
* performs authz + tracing; the worker only ever sees op names. */
|
|
3
|
+
export type GovernedFacade = (op: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
4
|
+
export interface CodeWorkerLimits {
|
|
5
|
+
wallClockMs: number;
|
|
6
|
+
maxOps: number;
|
|
7
|
+
maxArgBytes: number;
|
|
8
|
+
maxConsoleLines: number;
|
|
9
|
+
maxOutputBytes: number;
|
|
10
|
+
maxScreenshots: number;
|
|
11
|
+
maxNavigations: number;
|
|
12
|
+
/** Heap cap (MB) passed as --max-old-space-size. Also controls the
|
|
13
|
+
* OS-level rlimit on Linux (bwrap --rlimit) and macOS (ulimit -v).
|
|
14
|
+
* Default 256 MB; hard cap 512 MB to prevent OOM on the host. */
|
|
15
|
+
maxMemoryMb: number;
|
|
16
|
+
/** CPU time limit in seconds for the macOS ulimit -t wrapper.
|
|
17
|
+
* Ignored on other platforms (wall-clock timer is the backstop).
|
|
18
|
+
* Default 60 (matches wallClockMs default). */
|
|
19
|
+
maxCpuSeconds: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_LIMITS: CodeWorkerLimits;
|
|
22
|
+
export interface SubActionTrace {
|
|
23
|
+
seq: number;
|
|
24
|
+
op: string;
|
|
25
|
+
ok: boolean;
|
|
26
|
+
ms: number;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface CodeRunResult {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
result?: unknown;
|
|
32
|
+
console: string[];
|
|
33
|
+
traces: SubActionTrace[];
|
|
34
|
+
error?: {
|
|
35
|
+
code: string;
|
|
36
|
+
message: string;
|
|
37
|
+
};
|
|
38
|
+
/** Which OS sandbox actually wrapped the child. */
|
|
39
|
+
sandbox: "bubblewrap" | "seatbelt" | "node-permission-only" | "none";
|
|
40
|
+
killVerified: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare function runSandboxedScript(opts: {
|
|
43
|
+
script: string;
|
|
44
|
+
scratchDir: string;
|
|
45
|
+
facade: GovernedFacade;
|
|
46
|
+
limits?: Partial<CodeWorkerLimits>;
|
|
47
|
+
onTrace?: (t: SubActionTrace) => void;
|
|
48
|
+
log?: (m: string) => void;
|
|
49
|
+
}): Promise<CodeRunResult>;
|