@melaya/runner 1.1.42 → 1.1.45
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/browserBridge.js +139 -8
- package/dist/localDbProbe.py +295 -252
- package/dist/uploadFetch.d.ts +39 -0
- package/dist/uploadFetch.js +393 -0
- package/localDbProbe.py +295 -252
- package/package.json +1 -1
package/dist/browserBridge.js
CHANGED
|
@@ -39,6 +39,8 @@ import { SessionManager, SessionError, spaceUserDataDir, } from "./sessionManage
|
|
|
39
39
|
import { ensureEngine } from "./browserProvisioner.js";
|
|
40
40
|
import { runSandboxedScript } from "./codeWorker.js";
|
|
41
41
|
import { classifyControlEffect, writeGatedInMode } from "./publishGate.js";
|
|
42
|
+
import { applyUploadResize, parseUploadResize, resolveDataUrlSource, resolveHandleSource, resolvePathSource, resolveUrlSource, UploadSourceError, } from "./uploadFetch.js";
|
|
43
|
+
import { unlink } from "node:fs/promises";
|
|
42
44
|
// A wall-clock around a CDP command. Playwright's CDPSession.send has NO default
|
|
43
45
|
// timeout, so a wedged/busy renderer means the promise NEVER settles (not a
|
|
44
46
|
// rejection - no catch fires) and the lease op-chain queues every later op
|
|
@@ -1772,14 +1774,136 @@ export async function startBrowserBridge(opts) {
|
|
|
1772
1774
|
};
|
|
1773
1775
|
}
|
|
1774
1776
|
case "upload_file": {
|
|
1775
|
-
//
|
|
1776
|
-
//
|
|
1777
|
-
//
|
|
1778
|
-
//
|
|
1779
|
-
//
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1777
|
+
// Attach file(s) to a page <input type=file> via CDP
|
|
1778
|
+
// DOM.setFileInputFiles. Sources, in precedence order: a staged
|
|
1779
|
+
// handle ("fh_<hex>_<ext>", from the server's browserUploadMedia
|
|
1780
|
+
// staging flow), a public image URL (guarded direct download — the
|
|
1781
|
+
// runner IS the executing side), or a LOCAL-RUNNER-ONLY file path.
|
|
1782
|
+
// v1 is images only (png/jpg/webp/gif), 10MB, magic-byte verified.
|
|
1783
|
+
//
|
|
1784
|
+
// GATE FIRST, download after: selecting a file onto a site is a
|
|
1785
|
+
// commit-shaped write (same family as submit), so in safe autonomy
|
|
1786
|
+
// it stages the approval card BEFORE any bytes move. browser.py's
|
|
1787
|
+
// poll loop re-issues the same act; the approved re-issue is the one
|
|
1788
|
+
// that downloads and attaches.
|
|
1789
|
+
await enforcePublishGate(reg, page, kind, "", String(args.ref ?? "file input"), args.hitl_mode, "publish");
|
|
1790
|
+
const handle = String(args.handle ?? args.value ?? (Array.isArray(args.fileHandles) ? args.fileHandles[0] : "") ?? "").trim();
|
|
1791
|
+
const srcUrl = String(args.url ?? "").trim();
|
|
1792
|
+
const srcPath = String(args.path ?? "").trim();
|
|
1793
|
+
const srcData = String(args.data ?? "").trim();
|
|
1794
|
+
const attachmentId = String(args.attachment_id ?? "").trim();
|
|
1795
|
+
let file;
|
|
1796
|
+
try {
|
|
1797
|
+
if (attachmentId) {
|
|
1798
|
+
// Per-turn chat attachments resolve to fh_ handles on the SERVER
|
|
1799
|
+
// (assistant stream / ext-bridge). A runner-LAUNCHED browser is a
|
|
1800
|
+
// pipeline transport with no chat turn to hold them, so the id is
|
|
1801
|
+
// unresolvable here — name the sources that do work.
|
|
1802
|
+
throw new BridgeError("act_args_invalid", "attachment_id is not available on the runner-launched browser transport — pass the image as " +
|
|
1803
|
+
"a public `url`, a base64 `data` URL, or a runner-local `path` instead.");
|
|
1804
|
+
}
|
|
1805
|
+
if (handle)
|
|
1806
|
+
file = await resolveHandleSource(String(reg.spec.serverBase || ""), handle);
|
|
1807
|
+
else if (srcUrl)
|
|
1808
|
+
file = await resolveUrlSource(srcUrl);
|
|
1809
|
+
else if (srcData)
|
|
1810
|
+
file = await resolveDataUrlSource(srcData);
|
|
1811
|
+
else if (srcPath)
|
|
1812
|
+
file = await resolvePathSource(srcPath);
|
|
1813
|
+
else {
|
|
1814
|
+
throw new BridgeError("act_args_invalid", "upload_file needs a file source: `url` (public http(s) image URL, max 10MB), `data` (a base64 " +
|
|
1815
|
+
"data URL), a staged `handle` (fh_...), or — on this local runner only — `path` (an image file " +
|
|
1816
|
+
"on this machine).");
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
catch (e) {
|
|
1820
|
+
if (e instanceof BridgeError)
|
|
1821
|
+
throw e;
|
|
1822
|
+
if (e instanceof UploadSourceError)
|
|
1823
|
+
throw new BridgeError("act_args_invalid", `upload_file: ${e.message}`);
|
|
1824
|
+
throw new BridgeError("unknown_outcome", `upload_file could not resolve the file source: ${e.message}`);
|
|
1825
|
+
}
|
|
1826
|
+
if (file.temp)
|
|
1827
|
+
reg.uploadTmpFiles.push(file.path);
|
|
1828
|
+
// Convert-before-attach (width/height/fit/format): a canvas job run
|
|
1829
|
+
// through the page's JS context — the runner has no image library,
|
|
1830
|
+
// but it HAS a browser. Pure computation; no DOM mutation.
|
|
1831
|
+
const resizeSpec = parseUploadResize(args);
|
|
1832
|
+
if (resizeSpec) {
|
|
1833
|
+
const cdpForResize = await getCdp(rec, page);
|
|
1834
|
+
try {
|
|
1835
|
+
file = await applyUploadResize(file, resizeSpec, async (expression) => {
|
|
1836
|
+
const r = await cdpRace(cdpForResize.send("Runtime.evaluate", {
|
|
1837
|
+
expression, awaitPromise: true, returnByValue: true,
|
|
1838
|
+
}), "Runtime.evaluate", 60_000);
|
|
1839
|
+
if (r.exceptionDetails)
|
|
1840
|
+
throw new Error(r.exceptionDetails.text || "page threw during the canvas conversion");
|
|
1841
|
+
return r.result?.value;
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
catch (e) {
|
|
1845
|
+
if (e instanceof UploadSourceError)
|
|
1846
|
+
throw new BridgeError("act_args_invalid", `upload_file: ${e.message}`);
|
|
1847
|
+
throw new BridgeError("unknown_outcome", `upload_file could not convert the image: ${e.message}`);
|
|
1848
|
+
}
|
|
1849
|
+
if (file.temp)
|
|
1850
|
+
reg.uploadTmpFiles.push(file.path);
|
|
1851
|
+
}
|
|
1852
|
+
// Locate the REAL <input type=file>. The ref may point at the input
|
|
1853
|
+
// itself, at a styled upload button that HIDES the input, or be
|
|
1854
|
+
// absent entirely — the common case, since hidden inputs rarely get
|
|
1855
|
+
// a ref in the tree. Search order: self, descendant, label control,
|
|
1856
|
+
// the enclosing form's input, the page's first file input.
|
|
1857
|
+
const FIND_FROM_REF = "function(){ var el = this;" +
|
|
1858
|
+
" var isFile = function(n){ return n && n.tagName && n.tagName.toLowerCase()==='input' && (n.getAttribute('type')||'').toLowerCase()==='file'; };" +
|
|
1859
|
+
" if (isFile(el)) return el;" +
|
|
1860
|
+
" var q = el && el.querySelector ? el.querySelector('input[type=file]') : null; if (q) return q;" +
|
|
1861
|
+
" if (el && el.tagName && el.tagName.toLowerCase()==='label') {" +
|
|
1862
|
+
" var c = el.control || (el.htmlFor ? document.getElementById(el.htmlFor) : null); if (isFile(c)) return c; }" +
|
|
1863
|
+
" var f = el && el.closest ? el.closest('form') : null;" +
|
|
1864
|
+
" var q2 = f ? f.querySelector('input[type=file]') : null; if (q2) return q2;" +
|
|
1865
|
+
" var all = document.querySelectorAll('input[type=file]'); return all.length ? all[0] : null; }";
|
|
1866
|
+
const FIND_ON_PAGE = "(function(){ var all = Array.prototype.slice.call(document.querySelectorAll('input[type=file]'));" +
|
|
1867
|
+
" if (!all.length) return null;" +
|
|
1868
|
+
" var vis = all.filter(function(i){ var r = i.getBoundingClientRect(); return r.width>0 && r.height>0; });" +
|
|
1869
|
+
" return vis[0] || all[0]; })()";
|
|
1870
|
+
const cdp = await getCdp(rec, page);
|
|
1871
|
+
let inputObjectId = null;
|
|
1872
|
+
if (args.ref) {
|
|
1873
|
+
const binding = sessions.resolveRef(lease, args.ref);
|
|
1874
|
+
const resolved = await cdpRace(cdp.send("DOM.resolveNode", { backendNodeId: binding.backendNodeId }), "DOM.resolveNode");
|
|
1875
|
+
const found = await cdpRace(cdp.send("Runtime.callFunctionOn", {
|
|
1876
|
+
objectId: resolved.object.objectId,
|
|
1877
|
+
functionDeclaration: FIND_FROM_REF,
|
|
1878
|
+
}), "Runtime.callFunctionOn");
|
|
1879
|
+
inputObjectId = found.result?.objectId && found.result.subtype !== "null" ? found.result.objectId : null;
|
|
1880
|
+
}
|
|
1881
|
+
else {
|
|
1882
|
+
const found = await cdpRace(cdp.send("Runtime.evaluate", {
|
|
1883
|
+
expression: FIND_ON_PAGE,
|
|
1884
|
+
}), "Runtime.evaluate");
|
|
1885
|
+
inputObjectId = found.result?.objectId && found.result.subtype !== "null" ? found.result.objectId : null;
|
|
1886
|
+
}
|
|
1887
|
+
if (!inputObjectId) {
|
|
1888
|
+
throw new BridgeError("act_args_invalid", "no <input type=file> found" + (args.ref ? ` at or near ref '${args.ref}'` : " on this page") + ". " +
|
|
1889
|
+
"Some uploaders only create the input after their Upload button is clicked — click it first, then " +
|
|
1890
|
+
"retry upload_file. If the page opens a NATIVE file-picker dialog instead, hand off with browser_ask_user.");
|
|
1891
|
+
}
|
|
1892
|
+
// Set the files, then fire the events a real selection fires — CDP's
|
|
1893
|
+
// DOM.setFileInputFiles changes .files WITHOUT dispatching them, and
|
|
1894
|
+
// every framework listens on input/change.
|
|
1895
|
+
await cdpRace(cdp.send("DOM.setFileInputFiles", { files: [file.path], objectId: inputObjectId }), "DOM.setFileInputFiles");
|
|
1896
|
+
await cdpRace(cdp.send("Runtime.callFunctionOn", {
|
|
1897
|
+
objectId: inputObjectId,
|
|
1898
|
+
functionDeclaration: "function(){ this.dispatchEvent(new Event('input',{bubbles:true})); this.dispatchEvent(new Event('change',{bubbles:true})); return this.files ? this.files.length : 0; }",
|
|
1899
|
+
returnByValue: true,
|
|
1900
|
+
}), "Runtime.callFunctionOn");
|
|
1901
|
+
return {
|
|
1902
|
+
uploaded: true,
|
|
1903
|
+
files: [file.name],
|
|
1904
|
+
bytes: file.bytes,
|
|
1905
|
+
note: "file attached to the input — the page's own submit/save control still commits it",
|
|
1906
|
+
};
|
|
1783
1907
|
}
|
|
1784
1908
|
default:
|
|
1785
1909
|
throw new BridgeError("act_kind_unknown", `unhandled kind '${kind}'`);
|
|
@@ -2423,6 +2547,7 @@ export async function startBrowserBridge(opts) {
|
|
|
2423
2547
|
sessionInit: null,
|
|
2424
2548
|
traces: [],
|
|
2425
2549
|
externalAttach: false,
|
|
2550
|
+
uploadTmpFiles: [],
|
|
2426
2551
|
};
|
|
2427
2552
|
byToken.set(token, reg);
|
|
2428
2553
|
byRunId.set(spec.runId, reg);
|
|
@@ -2451,6 +2576,12 @@ export async function startBrowserBridge(opts) {
|
|
|
2451
2576
|
for (const e of reg.enforcement.splice(0)) {
|
|
2452
2577
|
await e.dispose().catch(() => { });
|
|
2453
2578
|
}
|
|
2579
|
+
// Delete any upload temp files this run staged. Deferred to teardown on
|
|
2580
|
+
// purpose: the browser reads the file LAZILY when the page's form submits,
|
|
2581
|
+
// which can be several agent turns after the upload_file act.
|
|
2582
|
+
for (const p of reg.uploadTmpFiles.splice(0)) {
|
|
2583
|
+
await unlink(p).catch(() => { });
|
|
2584
|
+
}
|
|
2454
2585
|
byRunId.delete(runId);
|
|
2455
2586
|
byToken.delete(reg.token);
|
|
2456
2587
|
activePageByRunId.delete(runId);
|