browser-pilot-cli 0.3.0-rc.1 → 0.3.0-rc.3
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/README.md +2 -1
- package/dist/cli.js +1 -1
- package/dist/daemon.js +289 -253
- package/docs/architecture/browser-pilot-platform-spec.md +27 -16
- package/docs/integration/stdio-bridge.md +22 -11
- package/docs/plans/profile-context-routing.md +20 -18
- package/docs/plans/universal-agent-integration.md +8 -6
- package/docs/plans/v0.3.0-stabilization.md +49 -0
- package/docs/releases/v0.3.0-rc.1.md +9 -3
- package/docs/releases/v0.3.0-rc.2.md +58 -0
- package/docs/releases/v0.3.0-rc.3.md +56 -0
- package/package.json +5 -1
package/dist/daemon.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import http2 from "http";
|
|
3
3
|
import { unlinkSync as unlinkSync2, chmodSync as chmodSync2 } from "fs";
|
|
4
4
|
import { createHash as createHash5, randomUUID as randomUUID14 } from "crypto";
|
|
5
|
-
import { join as
|
|
5
|
+
import { join as join5 } from "path";
|
|
6
6
|
|
|
7
7
|
// src/paths.ts
|
|
8
8
|
import { createHash } from "crypto";
|
|
@@ -2344,10 +2344,10 @@ var MemoryCommandRuntime = class {
|
|
|
2344
2344
|
throw new BrowserPilotError("internal_error", "Invalid or duplicate Command ID");
|
|
2345
2345
|
}
|
|
2346
2346
|
const acceptedAt = this.now();
|
|
2347
|
-
let
|
|
2347
|
+
let resolve2;
|
|
2348
2348
|
let reject;
|
|
2349
2349
|
const completion = new Promise((resolvePromise, rejectPromise) => {
|
|
2350
|
-
|
|
2350
|
+
resolve2 = resolvePromise;
|
|
2351
2351
|
reject = rejectPromise;
|
|
2352
2352
|
});
|
|
2353
2353
|
const record = {
|
|
@@ -2368,7 +2368,7 @@ var MemoryCommandRuntime = class {
|
|
|
2368
2368
|
scopeKey,
|
|
2369
2369
|
cancellation: input.cancellation,
|
|
2370
2370
|
abortController: new AbortController(),
|
|
2371
|
-
resolve:
|
|
2371
|
+
resolve: resolve2,
|
|
2372
2372
|
reject,
|
|
2373
2373
|
completion,
|
|
2374
2374
|
settled: false
|
|
@@ -2988,8 +2988,8 @@ var MemoryBrokerRuntime = class {
|
|
|
2988
2988
|
if (connection.notificationWaiter) {
|
|
2989
2989
|
throw invalidArgument("Only one notification poll may be active per bridge Connection");
|
|
2990
2990
|
}
|
|
2991
|
-
return new Promise((
|
|
2992
|
-
const waiter = { resolve:
|
|
2991
|
+
return new Promise((resolve2) => {
|
|
2992
|
+
const waiter = { resolve: resolve2, signal: options.signal };
|
|
2993
2993
|
const finish = () => this.resolveNotificationWaiter(connection, void 0);
|
|
2994
2994
|
waiter.timer = setTimeout(finish, waitMs);
|
|
2995
2995
|
waiter.timer.unref();
|
|
@@ -5458,8 +5458,8 @@ async function waitForLoad(transport, sessionId, timeout = 3e4) {
|
|
|
5458
5458
|
let interactiveSince = null;
|
|
5459
5459
|
const deadlineReached = /* @__PURE__ */ Symbol("page-load-deadline");
|
|
5460
5460
|
let deadlineTimer;
|
|
5461
|
-
const deadline = new Promise((
|
|
5462
|
-
deadlineTimer = setTimeout(() =>
|
|
5461
|
+
const deadline = new Promise((resolve2) => {
|
|
5462
|
+
deadlineTimer = setTimeout(() => resolve2(deadlineReached), timeout);
|
|
5463
5463
|
});
|
|
5464
5464
|
const injectBorder = async () => {
|
|
5465
5465
|
await Promise.race([
|
|
@@ -5491,7 +5491,7 @@ async function waitForLoad(transport, sessionId, timeout = 3e4) {
|
|
|
5491
5491
|
}
|
|
5492
5492
|
}
|
|
5493
5493
|
const sleep = await Promise.race([
|
|
5494
|
-
new Promise((
|
|
5494
|
+
new Promise((resolve2) => setTimeout(() => resolve2("poll"), 200)),
|
|
5495
5495
|
deadline
|
|
5496
5496
|
]);
|
|
5497
5497
|
if (sleep === deadlineReached) throw new PageLoadTimeoutError(timeout);
|
|
@@ -5675,7 +5675,7 @@ var InputDispatcher = class {
|
|
|
5675
5675
|
}
|
|
5676
5676
|
hooks.afterCharacter?.({ character: char, index });
|
|
5677
5677
|
index += 1;
|
|
5678
|
-
if (delayMs > 0) await new Promise((
|
|
5678
|
+
if (delayMs > 0) await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
5679
5679
|
}
|
|
5680
5680
|
}
|
|
5681
5681
|
async insertText(text) {
|
|
@@ -5719,7 +5719,7 @@ var ObservationService = class {
|
|
|
5719
5719
|
}
|
|
5720
5720
|
async observeAfterAction(limit = OBSERVATION_V1_LIMITS.defaultElements) {
|
|
5721
5721
|
if (this.settleDelayMs > 0) {
|
|
5722
|
-
await new Promise((
|
|
5722
|
+
await new Promise((resolve2) => setTimeout(resolve2, this.settleDelayMs));
|
|
5723
5723
|
}
|
|
5724
5724
|
await this.loadWaiter(this.transport, this.sessionId, this.loadTimeoutMs);
|
|
5725
5725
|
return this.observe(limit);
|
|
@@ -6076,7 +6076,7 @@ var ActionService = class {
|
|
|
6076
6076
|
await this.input.click(point.x, point.y, { button, clickCount });
|
|
6077
6077
|
this.markDispatched(run);
|
|
6078
6078
|
if (this.readbackDelayMs > 0) {
|
|
6079
|
-
await new Promise((
|
|
6079
|
+
await new Promise((resolve2) => setTimeout(resolve2, this.readbackDelayMs));
|
|
6080
6080
|
}
|
|
6081
6081
|
let after;
|
|
6082
6082
|
try {
|
|
@@ -6128,7 +6128,7 @@ var ActionService = class {
|
|
|
6128
6128
|
await this.input.press(key);
|
|
6129
6129
|
this.markDispatched(run);
|
|
6130
6130
|
if (this.readbackDelayMs > 0) {
|
|
6131
|
-
await new Promise((
|
|
6131
|
+
await new Promise((resolve2) => setTimeout(resolve2, this.readbackDelayMs));
|
|
6132
6132
|
}
|
|
6133
6133
|
let after;
|
|
6134
6134
|
try {
|
|
@@ -6194,7 +6194,7 @@ var ActionService = class {
|
|
|
6194
6194
|
this.markDispatched(run);
|
|
6195
6195
|
}
|
|
6196
6196
|
if (this.readbackDelayMs > 0) {
|
|
6197
|
-
await new Promise((
|
|
6197
|
+
await new Promise((resolve2) => setTimeout(resolve2, this.readbackDelayMs));
|
|
6198
6198
|
}
|
|
6199
6199
|
const after = await this.readElementState(objectId);
|
|
6200
6200
|
evidence = inputEvidence("type", before, after, text, !!options.clear);
|
|
@@ -6226,7 +6226,7 @@ var ActionService = class {
|
|
|
6226
6226
|
this.willDispatch();
|
|
6227
6227
|
await this.input.click(location.x, location.y);
|
|
6228
6228
|
this.markDispatched(run);
|
|
6229
|
-
if (this.focusDelayMs > 0) await new Promise((
|
|
6229
|
+
if (this.focusDelayMs > 0) await new Promise((resolve2) => setTimeout(resolve2, this.focusDelayMs));
|
|
6230
6230
|
}
|
|
6231
6231
|
await this.captureFocus(run, "capture_input_focus");
|
|
6232
6232
|
const before = await this.readActiveState();
|
|
@@ -6248,7 +6248,7 @@ var ActionService = class {
|
|
|
6248
6248
|
},
|
|
6249
6249
|
afterCharacter: () => this.markDispatched(run)
|
|
6250
6250
|
});
|
|
6251
|
-
if (this.readbackDelayMs > 0) await new Promise((
|
|
6251
|
+
if (this.readbackDelayMs > 0) await new Promise((resolve2) => setTimeout(resolve2, this.readbackDelayMs));
|
|
6252
6252
|
const after = await this.readActiveState();
|
|
6253
6253
|
const evidence = inputEvidence("keyboard", before, after, text, !!options.clear);
|
|
6254
6254
|
this.requireVerification(evidence, options.verification);
|
|
@@ -6608,8 +6608,8 @@ var CdpActionContinuityGuard = class _CdpActionContinuityGuard {
|
|
|
6608
6608
|
|
|
6609
6609
|
// src/services/download-controller.ts
|
|
6610
6610
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
6611
|
-
import {
|
|
6612
|
-
import {
|
|
6611
|
+
import { lstat } from "fs/promises";
|
|
6612
|
+
import { isAbsolute as isAbsolute3 } from "path";
|
|
6613
6613
|
|
|
6614
6614
|
// src/services/agent-hint-service.ts
|
|
6615
6615
|
var MAX_HINT_REFS = 32;
|
|
@@ -6710,6 +6710,15 @@ function repeatedActionAgentHint(streak, value) {
|
|
|
6710
6710
|
// src/services/download-controller.ts
|
|
6711
6711
|
var MAX_EVENT_URL_LENGTH = 16384;
|
|
6712
6712
|
var MAX_FILE_NAME_LENGTH = 4096;
|
|
6713
|
+
var MAX_FILE_PATH_LENGTH = 32768;
|
|
6714
|
+
var DownloadFinalizeError = class extends Error {
|
|
6715
|
+
code;
|
|
6716
|
+
constructor(code) {
|
|
6717
|
+
super(code);
|
|
6718
|
+
this.name = "DownloadFinalizeError";
|
|
6719
|
+
this.code = code;
|
|
6720
|
+
}
|
|
6721
|
+
};
|
|
6713
6722
|
function boundedString(value, maxLength) {
|
|
6714
6723
|
return typeof value === "string" ? value.slice(0, maxLength) : "";
|
|
6715
6724
|
}
|
|
@@ -6718,14 +6727,13 @@ function finiteBytes(value) {
|
|
|
6718
6727
|
}
|
|
6719
6728
|
function safeFileName(value) {
|
|
6720
6729
|
const candidate = boundedString(value, MAX_FILE_NAME_LENGTH).replace(/[\u0000-\u001f\u007f]/g, "").trim();
|
|
6721
|
-
const leaf =
|
|
6730
|
+
const leaf = candidate.split(/[\\/]/).at(-1) ?? "";
|
|
6722
6731
|
return leaf && leaf !== "." && leaf !== ".." ? leaf : "download";
|
|
6723
6732
|
}
|
|
6724
6733
|
var DownloadController = class {
|
|
6725
6734
|
constructor(transport, artifacts, options = {}) {
|
|
6726
6735
|
this.transport = transport;
|
|
6727
6736
|
this.artifacts = artifacts;
|
|
6728
|
-
this.directory = resolve(options.directory ?? DOWNLOAD_DIR);
|
|
6729
6737
|
this.maxDownloadBytes = options.maxDownloadBytes ?? artifacts.maxItemBytes;
|
|
6730
6738
|
this.maxActivePerSession = options.maxActivePerSession ?? 8;
|
|
6731
6739
|
this.maxActivePerWorkspace = options.maxActivePerWorkspace ?? 32;
|
|
@@ -6737,102 +6745,87 @@ var DownloadController = class {
|
|
|
6737
6745
|
this.maxActivePerWorkspace,
|
|
6738
6746
|
this.maxActiveTotal
|
|
6739
6747
|
].some((value) => !Number.isSafeInteger(value) || value <= 0) || this.maxDownloadBytes > artifacts.maxItemBytes) {
|
|
6740
|
-
throw new Error("Invalid
|
|
6748
|
+
throw new Error("Invalid DownloadController limit");
|
|
6741
6749
|
}
|
|
6742
6750
|
this.installHandlers();
|
|
6743
6751
|
}
|
|
6744
6752
|
sessions = /* @__PURE__ */ new Map();
|
|
6745
6753
|
attachments = /* @__PURE__ */ new Map();
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
directory;
|
|
6754
|
+
downloadsByGuid = /* @__PURE__ */ new Map();
|
|
6755
|
+
configurations = /* @__PURE__ */ new Map();
|
|
6749
6756
|
maxDownloadBytes;
|
|
6750
6757
|
maxActivePerSession;
|
|
6751
6758
|
maxActivePerWorkspace;
|
|
6752
6759
|
maxActiveTotal;
|
|
6753
6760
|
publishEvent;
|
|
6754
|
-
|
|
6761
|
+
latestConfigurationGeneration = 0;
|
|
6755
6762
|
attachSession(context) {
|
|
6756
|
-
if (this.sessions.has(context.sessionId)) {
|
|
6757
|
-
return Promise.resolve(this.sessions.get(context.sessionId).available);
|
|
6758
|
-
}
|
|
6759
6763
|
const pending = this.attachments.get(context.sessionId);
|
|
6760
6764
|
if (pending) return pending;
|
|
6765
|
+
const existing = this.sessions.get(context.sessionId);
|
|
6766
|
+
if (existing) return Promise.resolve(existing.available);
|
|
6761
6767
|
const attachment = this.configureSession(context).finally(() => {
|
|
6762
6768
|
if (this.attachments.get(context.sessionId) === attachment) {
|
|
6763
6769
|
this.attachments.delete(context.sessionId);
|
|
6764
6770
|
}
|
|
6765
|
-
this.attachmentContexts.delete(context.sessionId);
|
|
6766
|
-
this.cancelledAttachments.delete(context.sessionId);
|
|
6767
6771
|
});
|
|
6768
6772
|
this.attachments.set(context.sessionId, attachment);
|
|
6769
|
-
this.attachmentContexts.set(context.sessionId, { ...context });
|
|
6770
6773
|
return attachment;
|
|
6771
6774
|
}
|
|
6772
6775
|
async configureSession(context) {
|
|
6773
|
-
const directory = join4(this.directory, randomUUID6());
|
|
6774
6776
|
const session = {
|
|
6775
6777
|
context: { ...context },
|
|
6776
|
-
directory,
|
|
6777
6778
|
available: false,
|
|
6778
6779
|
closed: false,
|
|
6779
6780
|
downloads: /* @__PURE__ */ new Map()
|
|
6780
6781
|
};
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
});
|
|
6787
|
-
stagingReady = true;
|
|
6788
|
-
if (this.cancelledAttachments.has(context.sessionId)) {
|
|
6789
|
-
await rm(directory, { recursive: true, force: true }).catch(() => {
|
|
6790
|
-
});
|
|
6791
|
-
return false;
|
|
6792
|
-
}
|
|
6793
|
-
this.sessions.set(context.sessionId, session);
|
|
6794
|
-
await this.transport.send("Page.setDownloadBehavior", {
|
|
6795
|
-
behavior: "allowAndName",
|
|
6796
|
-
downloadPath: directory,
|
|
6797
|
-
eventsEnabled: true
|
|
6798
|
-
}, context.sessionId);
|
|
6799
|
-
if (session.closed || this.sessions.get(context.sessionId) !== session) {
|
|
6800
|
-
await rm(directory, { recursive: true, force: true }).catch(() => {
|
|
6801
|
-
});
|
|
6802
|
-
return false;
|
|
6803
|
-
}
|
|
6804
|
-
session.available = true;
|
|
6805
|
-
return true;
|
|
6806
|
-
} catch {
|
|
6807
|
-
this.sessions.delete(context.sessionId);
|
|
6808
|
-
session.closed = true;
|
|
6809
|
-
await rm(directory, { recursive: true, force: true }).catch(() => {
|
|
6810
|
-
});
|
|
6782
|
+
this.sessions.set(context.sessionId, session);
|
|
6783
|
+
const available = await this.enableBrowserDownloadEvents(context);
|
|
6784
|
+
if (session.closed || this.sessions.get(context.sessionId) !== session) return false;
|
|
6785
|
+
session.available = available;
|
|
6786
|
+
if (!available) {
|
|
6811
6787
|
this.publish(session, {
|
|
6812
6788
|
state: "capture_unavailable",
|
|
6813
|
-
reason:
|
|
6789
|
+
reason: "browser_download_events_unavailable"
|
|
6814
6790
|
});
|
|
6815
|
-
return false;
|
|
6816
6791
|
}
|
|
6792
|
+
return available;
|
|
6793
|
+
}
|
|
6794
|
+
enableBrowserDownloadEvents(context) {
|
|
6795
|
+
if (context.browserConnectionGeneration > this.latestConfigurationGeneration) {
|
|
6796
|
+
this.latestConfigurationGeneration = context.browserConnectionGeneration;
|
|
6797
|
+
for (const generation of this.configurations.keys()) {
|
|
6798
|
+
if (generation < context.browserConnectionGeneration) this.configurations.delete(generation);
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6801
|
+
let contexts = this.configurations.get(context.browserConnectionGeneration);
|
|
6802
|
+
if (!contexts) {
|
|
6803
|
+
contexts = /* @__PURE__ */ new Map();
|
|
6804
|
+
this.configurations.set(context.browserConnectionGeneration, contexts);
|
|
6805
|
+
}
|
|
6806
|
+
const key = context.cdpBrowserContextId ?? "";
|
|
6807
|
+
const existing = contexts.get(key);
|
|
6808
|
+
if (existing) return existing;
|
|
6809
|
+
const configured = this.transport.send("Browser.setDownloadBehavior", {
|
|
6810
|
+
behavior: "default",
|
|
6811
|
+
eventsEnabled: true,
|
|
6812
|
+
...context.cdpBrowserContextId ? { browserContextId: context.cdpBrowserContextId } : {}
|
|
6813
|
+
}).then(() => true, () => {
|
|
6814
|
+
if (contexts?.get(key) === configured) contexts.delete(key);
|
|
6815
|
+
return false;
|
|
6816
|
+
});
|
|
6817
|
+
contexts.set(key, configured);
|
|
6818
|
+
return configured;
|
|
6817
6819
|
}
|
|
6818
6820
|
detachSession(sessionId, reason2) {
|
|
6819
|
-
if (this.attachments.has(sessionId)) this.cancelledAttachments.add(sessionId);
|
|
6820
6821
|
const session = this.sessions.get(sessionId);
|
|
6821
6822
|
if (!session) return;
|
|
6822
6823
|
this.sessions.delete(sessionId);
|
|
6823
6824
|
session.closed = true;
|
|
6824
|
-
|
|
6825
|
-
void this.transport.send("Page.setDownloadBehavior", {
|
|
6826
|
-
behavior: "default",
|
|
6827
|
-
eventsEnabled: false
|
|
6828
|
-
}, sessionId).catch(() => {
|
|
6829
|
-
});
|
|
6830
|
-
}
|
|
6831
|
-
for (const download of session.downloads.values()) {
|
|
6825
|
+
for (const download of [...session.downloads.values()]) {
|
|
6832
6826
|
if (download.state === "terminal") continue;
|
|
6833
6827
|
download.state = "terminal";
|
|
6834
|
-
|
|
6835
|
-
});
|
|
6828
|
+
this.removeBinding(session, download);
|
|
6836
6829
|
this.publish(session, {
|
|
6837
6830
|
downloadId: download.id,
|
|
6838
6831
|
state: "cancelled",
|
|
@@ -6841,21 +6834,13 @@ var DownloadController = class {
|
|
|
6841
6834
|
...download.totalBytes !== void 0 ? { totalBytes: download.totalBytes } : {}
|
|
6842
6835
|
});
|
|
6843
6836
|
}
|
|
6844
|
-
void rm(session.directory, { recursive: true, force: true }).catch(() => {
|
|
6845
|
-
});
|
|
6846
6837
|
}
|
|
6847
6838
|
releaseLease(leaseId) {
|
|
6848
|
-
for (const context of [...this.attachmentContexts.values()]) {
|
|
6849
|
-
if (context.leaseId === leaseId) this.detachSession(context.sessionId, "lease_released");
|
|
6850
|
-
}
|
|
6851
6839
|
for (const session of [...this.sessions.values()]) {
|
|
6852
6840
|
if (session.context.leaseId === leaseId) this.detachSession(session.context.sessionId, "lease_released");
|
|
6853
6841
|
}
|
|
6854
6842
|
}
|
|
6855
6843
|
releaseWorkspace(workspaceId) {
|
|
6856
|
-
for (const context of [...this.attachmentContexts.values()]) {
|
|
6857
|
-
if (context.workspaceId === workspaceId) this.detachSession(context.sessionId, "workspace_released");
|
|
6858
|
-
}
|
|
6859
6844
|
for (const session of [...this.sessions.values()]) {
|
|
6860
6845
|
if (session.context.workspaceId === workspaceId) {
|
|
6861
6846
|
this.detachSession(session.context.sessionId, "workspace_released");
|
|
@@ -6868,13 +6853,20 @@ var DownloadController = class {
|
|
|
6868
6853
|
const session = this.sessions.get(sessionId);
|
|
6869
6854
|
const guid = boundedString(params?.guid, 512);
|
|
6870
6855
|
if (!session || !session.available || session.closed || !/^[A-Za-z0-9._-]{1,512}$/.test(guid)) return;
|
|
6871
|
-
|
|
6872
|
-
if (
|
|
6873
|
-
|
|
6874
|
-
|
|
6856
|
+
const collision = this.downloadsByGuid.get(guid);
|
|
6857
|
+
if (collision) {
|
|
6858
|
+
if (collision.session === session) return;
|
|
6859
|
+
this.failDownload(collision.session, collision.download, "download_identity_collision");
|
|
6860
|
+
this.publish(session, {
|
|
6861
|
+
downloadId: `download:${randomUUID6()}`,
|
|
6862
|
+
state: "failed",
|
|
6863
|
+
reason: "download_identity_collision"
|
|
6875
6864
|
});
|
|
6865
|
+
return;
|
|
6866
|
+
}
|
|
6867
|
+
if (!this.hasDownloadCapacity(session)) {
|
|
6876
6868
|
this.publish(session, {
|
|
6877
|
-
downloadId
|
|
6869
|
+
downloadId: `download:${randomUUID6()}`,
|
|
6878
6870
|
state: "failed",
|
|
6879
6871
|
reason: "concurrency_limit_exceeded"
|
|
6880
6872
|
});
|
|
@@ -6883,13 +6875,13 @@ var DownloadController = class {
|
|
|
6883
6875
|
const download = {
|
|
6884
6876
|
id: `download:${randomUUID6()}`,
|
|
6885
6877
|
guid,
|
|
6886
|
-
path: join4(session.directory, guid),
|
|
6887
6878
|
url: boundedString(params?.url, MAX_EVENT_URL_LENGTH),
|
|
6888
6879
|
fileName: safeFileName(params?.suggestedFilename),
|
|
6889
6880
|
receivedBytes: 0,
|
|
6890
6881
|
state: "active"
|
|
6891
6882
|
};
|
|
6892
6883
|
session.downloads.set(guid, download);
|
|
6884
|
+
this.downloadsByGuid.set(guid, { session, download });
|
|
6893
6885
|
this.publish(session, {
|
|
6894
6886
|
downloadId: download.id,
|
|
6895
6887
|
state: "started",
|
|
@@ -6899,63 +6891,49 @@ var DownloadController = class {
|
|
|
6899
6891
|
});
|
|
6900
6892
|
this.transport.on?.("Page.downloadProgress", (params, sessionId) => {
|
|
6901
6893
|
if (!sessionId) return;
|
|
6902
|
-
const
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
if (
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
if (totalBytes !== void 0) download.totalBytes = totalBytes;
|
|
6910
|
-
if (download.state === "terminal") {
|
|
6911
|
-
if (params?.state === "completed" || params?.state === "canceled") {
|
|
6912
|
-
session.downloads.delete(download.guid);
|
|
6913
|
-
void unlink(download.path).catch(() => {
|
|
6914
|
-
});
|
|
6915
|
-
}
|
|
6916
|
-
return;
|
|
6917
|
-
}
|
|
6918
|
-
if (download.receivedBytes > this.maxDownloadBytes || download.totalBytes !== void 0 && download.totalBytes > this.maxDownloadBytes) {
|
|
6919
|
-
this.failOversized(session, download);
|
|
6920
|
-
return;
|
|
6894
|
+
const binding = this.downloadsByGuid.get(boundedString(params?.guid, 512));
|
|
6895
|
+
if (!binding || binding.session.context.sessionId !== sessionId) return;
|
|
6896
|
+
this.updateBytes(binding.download, params);
|
|
6897
|
+
if (this.isOversized(binding.download)) {
|
|
6898
|
+
this.failOversized(binding.session, binding.download);
|
|
6899
|
+
} else if (params?.state === "canceled") {
|
|
6900
|
+
this.cancelDownload(binding.session, binding.download, "browser_cancelled");
|
|
6921
6901
|
}
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6902
|
+
});
|
|
6903
|
+
this.transport.on?.("Browser.downloadProgress", (params) => {
|
|
6904
|
+
const binding = this.downloadsByGuid.get(boundedString(params?.guid, 512));
|
|
6905
|
+
if (!binding) return;
|
|
6906
|
+
this.updateBytes(binding.download, params);
|
|
6907
|
+
if (this.isOversized(binding.download)) {
|
|
6908
|
+
this.failOversized(binding.session, binding.download);
|
|
6927
6909
|
return;
|
|
6928
6910
|
}
|
|
6929
|
-
if (params?.state === "completed" && download.state === "active") {
|
|
6930
|
-
download.state = "finalizing";
|
|
6931
|
-
void this.finalize(session, download);
|
|
6911
|
+
if (params?.state === "completed" && binding.download.state === "active") {
|
|
6912
|
+
binding.download.state = "finalizing";
|
|
6913
|
+
void this.finalize(binding.session, binding.download, params?.filePath);
|
|
6932
6914
|
} else if (params?.state === "canceled") {
|
|
6933
|
-
|
|
6934
|
-
this.publish(session, {
|
|
6935
|
-
downloadId: download.id,
|
|
6936
|
-
state: "cancelled",
|
|
6937
|
-
reason: "browser_cancelled",
|
|
6938
|
-
receivedBytes: download.receivedBytes,
|
|
6939
|
-
...download.totalBytes !== void 0 ? { totalBytes: download.totalBytes } : {}
|
|
6940
|
-
});
|
|
6941
|
-
void unlink(download.path).catch(() => {
|
|
6942
|
-
});
|
|
6943
|
-
session.downloads.delete(download.guid);
|
|
6915
|
+
this.cancelDownload(binding.session, binding.download, "browser_cancelled");
|
|
6944
6916
|
}
|
|
6945
6917
|
});
|
|
6946
6918
|
}
|
|
6919
|
+
updateBytes(download, params) {
|
|
6920
|
+
const receivedBytes = finiteBytes(params?.receivedBytes);
|
|
6921
|
+
const totalBytes = finiteBytes(params?.totalBytes);
|
|
6922
|
+
if (receivedBytes !== void 0) download.receivedBytes = receivedBytes;
|
|
6923
|
+
if (totalBytes !== void 0) download.totalBytes = totalBytes;
|
|
6924
|
+
}
|
|
6925
|
+
isOversized(download) {
|
|
6926
|
+
return download.receivedBytes > this.maxDownloadBytes || download.totalBytes !== void 0 && download.totalBytes > this.maxDownloadBytes;
|
|
6927
|
+
}
|
|
6947
6928
|
failOversized(session, download) {
|
|
6948
6929
|
this.failDownload(session, download, "size_limit_exceeded", {
|
|
6949
6930
|
maxDownloadBytes: this.maxDownloadBytes
|
|
6950
6931
|
});
|
|
6951
6932
|
}
|
|
6952
|
-
failDownload(session, download, reason2, limits) {
|
|
6933
|
+
failDownload(session, download, reason2, limits = {}) {
|
|
6953
6934
|
if (download.state === "terminal") return;
|
|
6954
6935
|
download.state = "terminal";
|
|
6955
|
-
|
|
6956
|
-
});
|
|
6957
|
-
void unlink(download.path).catch(() => {
|
|
6958
|
-
});
|
|
6936
|
+
this.removeBinding(session, download);
|
|
6959
6937
|
this.publish(session, {
|
|
6960
6938
|
downloadId: download.id,
|
|
6961
6939
|
state: "failed",
|
|
@@ -6965,48 +6943,77 @@ var DownloadController = class {
|
|
|
6965
6943
|
...limits
|
|
6966
6944
|
});
|
|
6967
6945
|
}
|
|
6968
|
-
|
|
6946
|
+
cancelDownload(session, download, reason2) {
|
|
6947
|
+
if (download.state === "terminal") return;
|
|
6948
|
+
download.state = "terminal";
|
|
6949
|
+
this.removeBinding(session, download);
|
|
6950
|
+
this.publish(session, {
|
|
6951
|
+
downloadId: download.id,
|
|
6952
|
+
state: "cancelled",
|
|
6953
|
+
reason: reason2,
|
|
6954
|
+
receivedBytes: download.receivedBytes,
|
|
6955
|
+
...download.totalBytes !== void 0 ? { totalBytes: download.totalBytes } : {}
|
|
6956
|
+
});
|
|
6957
|
+
}
|
|
6958
|
+
async finalize(session, download, rawFilePath) {
|
|
6969
6959
|
try {
|
|
6970
|
-
const
|
|
6971
|
-
if (
|
|
6972
|
-
|
|
6973
|
-
if (!info.isFile()) throw new Error("download_not_regular_file");
|
|
6974
|
-
if (info.size > this.maxDownloadBytes) {
|
|
6975
|
-
download.receivedBytes = info.size;
|
|
6960
|
+
const completedFile = await this.validateCompletedFile(rawFilePath);
|
|
6961
|
+
if (completedFile.size > this.maxDownloadBytes) {
|
|
6962
|
+
download.receivedBytes = completedFile.size;
|
|
6976
6963
|
this.failOversized(session, download);
|
|
6977
6964
|
return;
|
|
6978
6965
|
}
|
|
6979
|
-
|
|
6966
|
+
if (!this.isTracked(session, download)) return;
|
|
6967
|
+
const record = await this.artifacts.ingestDownloadCopy(
|
|
6980
6968
|
session.context.workspaceId,
|
|
6981
|
-
|
|
6969
|
+
completedFile.path,
|
|
6982
6970
|
download.fileName
|
|
6983
6971
|
);
|
|
6984
|
-
if (session
|
|
6972
|
+
if (!this.isTracked(session, download)) {
|
|
6985
6973
|
await this.artifacts.release(session.context.workspaceId, record.descriptor.id);
|
|
6986
6974
|
return;
|
|
6987
6975
|
}
|
|
6988
6976
|
download.state = "terminal";
|
|
6977
|
+
this.removeBinding(session, download);
|
|
6989
6978
|
this.publish(session, {
|
|
6990
6979
|
downloadId: download.id,
|
|
6991
6980
|
state: "completed",
|
|
6992
6981
|
artifact: record.descriptor
|
|
6993
6982
|
});
|
|
6994
|
-
session.downloads.delete(download.guid);
|
|
6995
6983
|
} catch (error) {
|
|
6996
6984
|
if (download.state === "terminal") return;
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
6985
|
+
const code = error instanceof DownloadFinalizeError ? error.code : error instanceof Error && "code" in error && error.code === "result_too_large" ? "artifact_quota_exceeded" : error instanceof Error && "code" in error && error.code === "invalid_argument" ? "download_file_unavailable" : "artifact_ingest_failed";
|
|
6986
|
+
this.failDownload(session, download, code);
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
async validateCompletedFile(rawFilePath) {
|
|
6990
|
+
if (typeof rawFilePath !== "string" || rawFilePath.length === 0) {
|
|
6991
|
+
throw new DownloadFinalizeError("download_file_path_unavailable");
|
|
6992
|
+
}
|
|
6993
|
+
if (rawFilePath.length > MAX_FILE_PATH_LENGTH || rawFilePath.includes("\0") || !isAbsolute3(rawFilePath)) {
|
|
6994
|
+
throw new DownloadFinalizeError("download_file_path_invalid");
|
|
6995
|
+
}
|
|
6996
|
+
try {
|
|
6997
|
+
const info = await lstat(rawFilePath);
|
|
6998
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
6999
|
+
throw new DownloadFinalizeError("download_not_regular_file");
|
|
7000
|
+
}
|
|
7001
|
+
return { path: rawFilePath, size: info.size };
|
|
7002
|
+
} catch (error) {
|
|
7003
|
+
if (error instanceof DownloadFinalizeError) throw error;
|
|
7004
|
+
throw new DownloadFinalizeError("download_file_unavailable");
|
|
7008
7005
|
}
|
|
7009
7006
|
}
|
|
7007
|
+
removeBinding(session, download) {
|
|
7008
|
+
if (session.downloads.get(download.guid) === download) session.downloads.delete(download.guid);
|
|
7009
|
+
const binding = this.downloadsByGuid.get(download.guid);
|
|
7010
|
+
if (binding?.session === session && binding.download === download) {
|
|
7011
|
+
this.downloadsByGuid.delete(download.guid);
|
|
7012
|
+
}
|
|
7013
|
+
}
|
|
7014
|
+
isTracked(session, download) {
|
|
7015
|
+
return !session.closed && session.downloads.get(download.guid) === download && this.downloadsByGuid.get(download.guid)?.download === download;
|
|
7016
|
+
}
|
|
7010
7017
|
publish(session, payload) {
|
|
7011
7018
|
if (!this.publishEvent) return;
|
|
7012
7019
|
const hint = this.agentHint(payload);
|
|
@@ -7044,44 +7051,12 @@ var DownloadController = class {
|
|
|
7044
7051
|
return void 0;
|
|
7045
7052
|
}
|
|
7046
7053
|
hasDownloadCapacity(session) {
|
|
7047
|
-
const
|
|
7048
|
-
const sessionCount = [...session.downloads.values()].filter(active).length;
|
|
7054
|
+
const sessionCount = session.downloads.size;
|
|
7049
7055
|
let workspaceCount = 0;
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
for (const download of candidate.downloads.values()) {
|
|
7053
|
-
if (!active(download)) continue;
|
|
7054
|
-
totalCount += 1;
|
|
7055
|
-
if (candidate.context.workspaceId === session.context.workspaceId) workspaceCount += 1;
|
|
7056
|
-
}
|
|
7057
|
-
}
|
|
7058
|
-
return sessionCount < this.maxActivePerSession && workspaceCount < this.maxActivePerWorkspace && totalCount < this.maxActiveTotal;
|
|
7059
|
-
}
|
|
7060
|
-
exceedsStagingQuota(currentSession) {
|
|
7061
|
-
let workspaceBytes = 0;
|
|
7062
|
-
let totalBytes = 0;
|
|
7063
|
-
for (const session of this.sessions.values()) {
|
|
7064
|
-
for (const download of session.downloads.values()) {
|
|
7065
|
-
if (download.state === "terminal") continue;
|
|
7066
|
-
const bytes = Math.max(download.receivedBytes, download.totalBytes ?? 0);
|
|
7067
|
-
totalBytes += bytes;
|
|
7068
|
-
if (session.context.workspaceId === currentSession.context.workspaceId) workspaceBytes += bytes;
|
|
7069
|
-
}
|
|
7056
|
+
for (const binding of this.downloadsByGuid.values()) {
|
|
7057
|
+
if (binding.session.context.workspaceId === session.context.workspaceId) workspaceCount += 1;
|
|
7070
7058
|
}
|
|
7071
|
-
return
|
|
7072
|
-
}
|
|
7073
|
-
initialize() {
|
|
7074
|
-
if (!this.initializePromise) {
|
|
7075
|
-
this.initializePromise = (async () => {
|
|
7076
|
-
await mkdir(this.directory, { recursive: true, mode: 448 });
|
|
7077
|
-
await chmod(this.directory, 448).catch(() => {
|
|
7078
|
-
});
|
|
7079
|
-
const entries = await readdir(this.directory, { withFileTypes: true });
|
|
7080
|
-
await Promise.all(entries.map((entry) => rm(join4(this.directory, entry.name), { recursive: true, force: true }).catch(() => {
|
|
7081
|
-
})));
|
|
7082
|
-
})();
|
|
7083
|
-
}
|
|
7084
|
-
return this.initializePromise;
|
|
7059
|
+
return sessionCount < this.maxActivePerSession && workspaceCount < this.maxActivePerWorkspace && this.downloadsByGuid.size < this.maxActiveTotal;
|
|
7085
7060
|
}
|
|
7086
7061
|
};
|
|
7087
7062
|
|
|
@@ -9511,7 +9486,7 @@ var TargetInventoryService = class {
|
|
|
9511
9486
|
};
|
|
9512
9487
|
|
|
9513
9488
|
// src/services/upload-service.ts
|
|
9514
|
-
import { basename as
|
|
9489
|
+
import { basename as basename2 } from "path";
|
|
9515
9490
|
var FILE_INPUT_BLOCK_REASONS = /* @__PURE__ */ new Set([
|
|
9516
9491
|
"detached",
|
|
9517
9492
|
"disabled",
|
|
@@ -9634,7 +9609,7 @@ var UploadService = class {
|
|
|
9634
9609
|
backendNodeId
|
|
9635
9610
|
}, this.sessionId);
|
|
9636
9611
|
if (this.readbackDelayMs > 0) {
|
|
9637
|
-
await new Promise((
|
|
9612
|
+
await new Promise((resolve2) => setTimeout(resolve2, this.readbackDelayMs));
|
|
9638
9613
|
}
|
|
9639
9614
|
let after;
|
|
9640
9615
|
try {
|
|
@@ -9656,7 +9631,7 @@ var UploadService = class {
|
|
|
9656
9631
|
reason: "target_unavailable"
|
|
9657
9632
|
};
|
|
9658
9633
|
}
|
|
9659
|
-
const nameMatched = after.firstFileName ===
|
|
9634
|
+
const nameMatched = after.firstFileName === basename2(filePath);
|
|
9660
9635
|
const status = after.fileCount === 1 && nameMatched ? "verified" : "mismatch";
|
|
9661
9636
|
const reason2 = after.fileCount !== 1 ? "file_count_mismatch" : !nameMatched ? "file_name_mismatch" : void 0;
|
|
9662
9637
|
return {
|
|
@@ -11753,13 +11728,13 @@ var BrowserToolService = class {
|
|
|
11753
11728
|
await this.transport.send("Target.closeTarget", { targetId });
|
|
11754
11729
|
} catch {
|
|
11755
11730
|
}
|
|
11756
|
-
for (let attempt = 0; attempt <
|
|
11731
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
11757
11732
|
try {
|
|
11758
11733
|
const current = await this.transport.send("Target.getTargets");
|
|
11759
11734
|
if (Array.isArray(current?.targetInfos) && !current.targetInfos.some((target) => target?.targetId === targetId)) return true;
|
|
11760
11735
|
} catch {
|
|
11761
11736
|
}
|
|
11762
|
-
if (attempt <
|
|
11737
|
+
if (attempt < 19) await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
11763
11738
|
}
|
|
11764
11739
|
return false;
|
|
11765
11740
|
}
|
|
@@ -11861,9 +11836,9 @@ var BrowserToolService = class {
|
|
|
11861
11836
|
context: { profileContextId: profile.id }
|
|
11862
11837
|
});
|
|
11863
11838
|
}
|
|
11864
|
-
for (let attempt = 0; attempt <
|
|
11839
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
11865
11840
|
const current = await this.transport.send("Target.getTargets");
|
|
11866
|
-
const markerTargets = Array.isArray(current?.targetInfos) ? current.targetInfos.filter((target) => target && typeof target.targetId === "string" && !existing.has(target.targetId) && target.type === "page" && target.
|
|
11841
|
+
const markerTargets = Array.isArray(current?.targetInfos) ? current.targetInfos.filter((target) => target && typeof target.targetId === "string" && !existing.has(target.targetId) && target.type === "page" && target.url === marker) : [];
|
|
11867
11842
|
for (const target of markerTargets) spawnedTargetIds.add(target.targetId);
|
|
11868
11843
|
if (markerTargets.length > 1) {
|
|
11869
11844
|
throw new BrowserPilotError("profile_context_unavailable", "Chrome created ambiguous managed window targets", {
|
|
@@ -11889,10 +11864,25 @@ var BrowserToolService = class {
|
|
|
11889
11864
|
}
|
|
11890
11865
|
);
|
|
11891
11866
|
}
|
|
11867
|
+
if (candidate.openerId !== representative.cdpTargetId && !await this.targetWindowNameMatches(candidate.targetId, windowName)) {
|
|
11868
|
+
throw new BrowserPilotError(
|
|
11869
|
+
"profile_context_unavailable",
|
|
11870
|
+
"Chrome could not prove ownership of the managed Profile window",
|
|
11871
|
+
{
|
|
11872
|
+
retryable: true,
|
|
11873
|
+
context: { profileContextId: profile.id },
|
|
11874
|
+
remediation: {
|
|
11875
|
+
code: "relist_profile_contexts",
|
|
11876
|
+
message: "List Profile contexts again and retry with a current Profile context.",
|
|
11877
|
+
actionRequired: true
|
|
11878
|
+
}
|
|
11879
|
+
}
|
|
11880
|
+
);
|
|
11881
|
+
}
|
|
11892
11882
|
candidateTargetId = candidate.targetId;
|
|
11893
11883
|
break;
|
|
11894
11884
|
}
|
|
11895
|
-
await new Promise((
|
|
11885
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
11896
11886
|
}
|
|
11897
11887
|
if (!candidateTargetId) {
|
|
11898
11888
|
throw this.managedTargetUnknownOutcome(profile);
|
|
@@ -11912,6 +11902,37 @@ var BrowserToolService = class {
|
|
|
11912
11902
|
if (cleanup2.some((closed) => !closed)) throw this.managedTargetUnknownOutcome(profile);
|
|
11913
11903
|
}
|
|
11914
11904
|
}
|
|
11905
|
+
async targetWindowNameMatches(targetId, expected) {
|
|
11906
|
+
let sessionId;
|
|
11907
|
+
try {
|
|
11908
|
+
const attached = await this.transport.send("Target.attachToTarget", {
|
|
11909
|
+
targetId,
|
|
11910
|
+
flatten: true
|
|
11911
|
+
});
|
|
11912
|
+
if (typeof attached?.sessionId !== "string") return false;
|
|
11913
|
+
sessionId = attached.sessionId;
|
|
11914
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
11915
|
+
try {
|
|
11916
|
+
const result = await this.transport.send("Runtime.evaluate", {
|
|
11917
|
+
expression: "window.name",
|
|
11918
|
+
returnByValue: true
|
|
11919
|
+
}, sessionId);
|
|
11920
|
+
if (result?.result?.value === expected) return true;
|
|
11921
|
+
if (result?.result?.value !== void 0) return false;
|
|
11922
|
+
} catch {
|
|
11923
|
+
}
|
|
11924
|
+
if (attempt < 9) await new Promise((resolve2) => setTimeout(resolve2, 25));
|
|
11925
|
+
}
|
|
11926
|
+
return false;
|
|
11927
|
+
} catch {
|
|
11928
|
+
return false;
|
|
11929
|
+
} finally {
|
|
11930
|
+
if (sessionId) {
|
|
11931
|
+
await this.transport.send("Target.detachFromTarget", { sessionId }).catch(() => {
|
|
11932
|
+
});
|
|
11933
|
+
}
|
|
11934
|
+
}
|
|
11935
|
+
}
|
|
11915
11936
|
async resolveTargetSession(context, targetId, operation) {
|
|
11916
11937
|
const inventoryContext = this.inventoryContext(context);
|
|
11917
11938
|
await this.inventory.refresh(inventoryContext);
|
|
@@ -11948,13 +11969,15 @@ var BrowserToolService = class {
|
|
|
11948
11969
|
if (typeof attached?.sessionId !== "string") {
|
|
11949
11970
|
throw new BrowserPilotError("internal_error", "Chrome returned an invalid CDP session");
|
|
11950
11971
|
}
|
|
11972
|
+
const profile = this.profileContexts.forTarget(cdpTargetId, context.browserConnectionGeneration);
|
|
11951
11973
|
const session = {
|
|
11952
11974
|
workspaceId: context.workspaceId,
|
|
11953
11975
|
leaseId: context.leaseId,
|
|
11954
11976
|
targetId,
|
|
11955
11977
|
browserConnectionGeneration: context.browserConnectionGeneration,
|
|
11956
11978
|
cdpTargetId,
|
|
11957
|
-
sessionId: attached.sessionId
|
|
11979
|
+
sessionId: attached.sessionId,
|
|
11980
|
+
...profile?.cdpBrowserContextId ? { cdpBrowserContextId: profile.cdpBrowserContextId } : {}
|
|
11958
11981
|
};
|
|
11959
11982
|
this.sessions.set(key, session);
|
|
11960
11983
|
this.ownedSessionIds.add(attached.sessionId);
|
|
@@ -13137,19 +13160,19 @@ var BrowserToolRouter = class {
|
|
|
13137
13160
|
// src/services/artifact-store.ts
|
|
13138
13161
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
13139
13162
|
import {
|
|
13140
|
-
chmod
|
|
13163
|
+
chmod,
|
|
13141
13164
|
copyFile,
|
|
13142
13165
|
lstat as lstat2,
|
|
13143
|
-
mkdir
|
|
13166
|
+
mkdir,
|
|
13144
13167
|
realpath,
|
|
13145
|
-
readdir
|
|
13146
|
-
rm
|
|
13147
|
-
stat
|
|
13148
|
-
unlink
|
|
13168
|
+
readdir,
|
|
13169
|
+
rm,
|
|
13170
|
+
stat,
|
|
13171
|
+
unlink,
|
|
13149
13172
|
writeFile
|
|
13150
13173
|
} from "fs/promises";
|
|
13151
13174
|
import { constants as fsConstants } from "fs";
|
|
13152
|
-
import { basename as
|
|
13175
|
+
import { basename as basename3, dirname as dirname2, extname, isAbsolute as isAbsolute4, join as join4, relative, resolve, sep } from "path";
|
|
13153
13176
|
var ARTIFACT_ID_PATTERN2 = /^artifact:[A-Za-z0-9][A-Za-z0-9._:-]{0,118}$/;
|
|
13154
13177
|
function clone2(record) {
|
|
13155
13178
|
return { descriptor: { ...record.descriptor }, path: record.path };
|
|
@@ -13187,7 +13210,7 @@ var ArtifactStore = class {
|
|
|
13187
13210
|
initialized = false;
|
|
13188
13211
|
mutationTail = Promise.resolve();
|
|
13189
13212
|
constructor(options = {}) {
|
|
13190
|
-
this.directory =
|
|
13213
|
+
this.directory = resolve(options.directory ?? ARTIFACT_DIR);
|
|
13191
13214
|
this.ttlMs = options.ttlMs ?? 15 * 6e4;
|
|
13192
13215
|
this.retainedTtlMs = options.retainedTtlMs ?? 24 * 60 * 6e4;
|
|
13193
13216
|
this.maxArtifactBytes = options.maxArtifactBytes ?? 100 * 1024 * 1024;
|
|
@@ -13223,10 +13246,10 @@ var ArtifactStore = class {
|
|
|
13223
13246
|
kind: "upload_input",
|
|
13224
13247
|
sensitivity: "user_file",
|
|
13225
13248
|
mimeType,
|
|
13226
|
-
|
|
13249
|
+
rejectSymbolicLink: false
|
|
13227
13250
|
});
|
|
13228
13251
|
}
|
|
13229
|
-
async
|
|
13252
|
+
async ingestDownloadCopy(workspaceId, sourcePath, fileName, mimeType) {
|
|
13230
13253
|
return this.ingestFile({
|
|
13231
13254
|
workspaceId,
|
|
13232
13255
|
sourcePath,
|
|
@@ -13234,7 +13257,7 @@ var ArtifactStore = class {
|
|
|
13234
13257
|
sensitivity: "user_file",
|
|
13235
13258
|
fileName,
|
|
13236
13259
|
mimeType,
|
|
13237
|
-
|
|
13260
|
+
rejectSymbolicLink: true
|
|
13238
13261
|
});
|
|
13239
13262
|
}
|
|
13240
13263
|
async initialize() {
|
|
@@ -13244,8 +13267,8 @@ var ArtifactStore = class {
|
|
|
13244
13267
|
return this.withLock(async () => clone2(await this.requireRecordUnlocked(workspaceId, artifactId)));
|
|
13245
13268
|
}
|
|
13246
13269
|
async export(workspaceId, artifactId, destination, overwrite = false) {
|
|
13247
|
-
if (!
|
|
13248
|
-
const target =
|
|
13270
|
+
if (!isAbsolute4(destination)) throw invalidArgument("Artifact export path must be absolute", "path");
|
|
13271
|
+
const target = resolve(destination);
|
|
13249
13272
|
if (this.isInsideStore(target)) {
|
|
13250
13273
|
throw invalidArgument("Artifact export path must be outside Broker storage", "path");
|
|
13251
13274
|
}
|
|
@@ -13262,7 +13285,7 @@ var ArtifactStore = class {
|
|
|
13262
13285
|
});
|
|
13263
13286
|
}
|
|
13264
13287
|
const canonicalStore = await realpath(this.directory);
|
|
13265
|
-
if (this.isWithin(canonicalStore,
|
|
13288
|
+
if (this.isWithin(canonicalStore, join4(canonicalParent, basename3(target)))) {
|
|
13266
13289
|
throw invalidArgument("Artifact export path must be outside Broker storage", "path");
|
|
13267
13290
|
}
|
|
13268
13291
|
if (overwrite) {
|
|
@@ -13282,7 +13305,7 @@ var ArtifactStore = class {
|
|
|
13282
13305
|
cause
|
|
13283
13306
|
});
|
|
13284
13307
|
});
|
|
13285
|
-
await
|
|
13308
|
+
await chmod(target, 384).catch(() => {
|
|
13286
13309
|
});
|
|
13287
13310
|
return { artifact: { ...record.descriptor }, path: target };
|
|
13288
13311
|
});
|
|
@@ -13339,8 +13362,8 @@ var ArtifactStore = class {
|
|
|
13339
13362
|
return this.maxTotalBytes;
|
|
13340
13363
|
}
|
|
13341
13364
|
async ingestFile(input) {
|
|
13342
|
-
if (!
|
|
13343
|
-
const source =
|
|
13365
|
+
if (!isAbsolute4(input.sourcePath)) throw invalidArgument("Artifact import path must be absolute", "path");
|
|
13366
|
+
const source = resolve(input.sourcePath);
|
|
13344
13367
|
if (this.isInsideStore(source)) {
|
|
13345
13368
|
throw invalidArgument("Artifact import path must be outside Broker storage", "path");
|
|
13346
13369
|
}
|
|
@@ -13348,6 +13371,21 @@ var ArtifactStore = class {
|
|
|
13348
13371
|
await this.sweepUnlocked();
|
|
13349
13372
|
await this.ensureDirectory();
|
|
13350
13373
|
const canonicalStore = await realpath(this.directory);
|
|
13374
|
+
if (input.rejectSymbolicLink) {
|
|
13375
|
+
let sourceLinkInfo;
|
|
13376
|
+
try {
|
|
13377
|
+
sourceLinkInfo = await lstat2(source);
|
|
13378
|
+
} catch (cause) {
|
|
13379
|
+
throw new BrowserPilotError("invalid_argument", "Artifact import file is not accessible", {
|
|
13380
|
+
context: { field: "path" },
|
|
13381
|
+
rpcCode: -32602,
|
|
13382
|
+
cause
|
|
13383
|
+
});
|
|
13384
|
+
}
|
|
13385
|
+
if (sourceLinkInfo.isSymbolicLink() || !sourceLinkInfo.isFile()) {
|
|
13386
|
+
throw invalidArgument("Artifact import path must identify a regular file", "path");
|
|
13387
|
+
}
|
|
13388
|
+
}
|
|
13351
13389
|
let canonicalSource;
|
|
13352
13390
|
try {
|
|
13353
13391
|
canonicalSource = await realpath(source);
|
|
@@ -13363,7 +13401,7 @@ var ArtifactStore = class {
|
|
|
13363
13401
|
}
|
|
13364
13402
|
let sourceInfo;
|
|
13365
13403
|
try {
|
|
13366
|
-
sourceInfo = await
|
|
13404
|
+
sourceInfo = await stat(canonicalSource);
|
|
13367
13405
|
} catch (cause) {
|
|
13368
13406
|
throw new BrowserPilotError("invalid_argument", "Artifact import file is not accessible", {
|
|
13369
13407
|
context: { field: "path" },
|
|
@@ -13373,8 +13411,8 @@ var ArtifactStore = class {
|
|
|
13373
13411
|
}
|
|
13374
13412
|
if (!sourceInfo.isFile()) throw invalidArgument("Artifact import path must identify a regular file", "path");
|
|
13375
13413
|
this.assertQuota(input.workspaceId, sourceInfo.size);
|
|
13376
|
-
const fileName = input.fileName ??
|
|
13377
|
-
if (!fileName || fileName === "." || fileName === ".." ||
|
|
13414
|
+
const fileName = input.fileName ?? basename3(source);
|
|
13415
|
+
if (!fileName || fileName === "." || fileName === ".." || basename3(fileName) !== fileName || fileName.length > 4096 || /[\u0000-\u001f\u007f]/.test(fileName)) {
|
|
13378
13416
|
throw invalidArgument("Artifact import filename is invalid or too long", "path");
|
|
13379
13417
|
}
|
|
13380
13418
|
const resolvedMimeType = input.mimeType ?? inferredMimeType(fileName);
|
|
@@ -13382,14 +13420,14 @@ var ArtifactStore = class {
|
|
|
13382
13420
|
throw invalidArgument("Artifact MIME type is invalid or too long", "mimeType");
|
|
13383
13421
|
}
|
|
13384
13422
|
const id = this.nextArtifactId();
|
|
13385
|
-
const storageDirectory =
|
|
13386
|
-
const destination =
|
|
13387
|
-
await
|
|
13423
|
+
const storageDirectory = join4(this.directory, randomUUID12());
|
|
13424
|
+
const destination = join4(storageDirectory, fileName);
|
|
13425
|
+
await mkdir(storageDirectory, { mode: 448 });
|
|
13388
13426
|
try {
|
|
13389
13427
|
await copyFile(canonicalSource, destination, fsConstants.COPYFILE_EXCL);
|
|
13390
|
-
await
|
|
13428
|
+
await chmod(destination, 384).catch(() => {
|
|
13391
13429
|
});
|
|
13392
|
-
const copied = await
|
|
13430
|
+
const copied = await stat(destination);
|
|
13393
13431
|
if (!copied.isFile() || copied.size !== sourceInfo.size) {
|
|
13394
13432
|
throw new BrowserPilotError("invalid_argument", "Artifact import file changed while it was being copied", {
|
|
13395
13433
|
context: { field: "path" },
|
|
@@ -13411,11 +13449,9 @@ var ArtifactStore = class {
|
|
|
13411
13449
|
};
|
|
13412
13450
|
const record = { descriptor: descriptor2, path: destination };
|
|
13413
13451
|
this.records.set(id, record);
|
|
13414
|
-
if (input.removeSource) await unlink2(canonicalSource).catch(() => {
|
|
13415
|
-
});
|
|
13416
13452
|
return clone2(record);
|
|
13417
13453
|
} catch (error) {
|
|
13418
|
-
await
|
|
13454
|
+
await rm(storageDirectory, { recursive: true, force: true }).catch(() => {
|
|
13419
13455
|
});
|
|
13420
13456
|
throw error;
|
|
13421
13457
|
}
|
|
@@ -13427,10 +13463,10 @@ var ArtifactStore = class {
|
|
|
13427
13463
|
this.assertQuota(input.workspaceId, byteSize);
|
|
13428
13464
|
await this.ensureDirectory();
|
|
13429
13465
|
const id = this.nextArtifactId();
|
|
13430
|
-
const file =
|
|
13466
|
+
const file = join4(this.directory, `${randomUUID12()}.bin`);
|
|
13431
13467
|
const createdAt = this.now();
|
|
13432
13468
|
await writeFile(file, input.bytes, { mode: 384, flag: "wx" });
|
|
13433
|
-
await
|
|
13469
|
+
await chmod(file, 384).catch(() => {
|
|
13434
13470
|
});
|
|
13435
13471
|
const descriptor2 = {
|
|
13436
13472
|
id,
|
|
@@ -13460,7 +13496,7 @@ var ArtifactStore = class {
|
|
|
13460
13496
|
throw this.notFound(artifactId);
|
|
13461
13497
|
}
|
|
13462
13498
|
try {
|
|
13463
|
-
const info = await
|
|
13499
|
+
const info = await stat(record.path);
|
|
13464
13500
|
if (!info.isFile() || info.size !== record.descriptor.byteSize) throw new Error("Artifact file mismatch");
|
|
13465
13501
|
} catch {
|
|
13466
13502
|
this.records.delete(artifactId);
|
|
@@ -13484,15 +13520,15 @@ var ArtifactStore = class {
|
|
|
13484
13520
|
}
|
|
13485
13521
|
async ensureDirectory() {
|
|
13486
13522
|
if (!this.initialized) {
|
|
13487
|
-
await
|
|
13523
|
+
await mkdir(this.directory, { recursive: true, mode: 448 });
|
|
13488
13524
|
if (this.purgeOrphansOnInitialize) {
|
|
13489
|
-
const entries = await
|
|
13490
|
-
await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".bin") || entry.isDirectory()).map((entry) =>
|
|
13525
|
+
const entries = await readdir(this.directory, { withFileTypes: true });
|
|
13526
|
+
await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".bin") || entry.isDirectory()).map((entry) => rm(join4(this.directory, entry.name), { recursive: true, force: true }).catch(() => {
|
|
13491
13527
|
})));
|
|
13492
13528
|
}
|
|
13493
13529
|
this.initialized = true;
|
|
13494
13530
|
}
|
|
13495
|
-
await
|
|
13531
|
+
await chmod(this.directory, 448).catch(() => {
|
|
13496
13532
|
});
|
|
13497
13533
|
}
|
|
13498
13534
|
notFound(artifactId) {
|
|
@@ -13507,11 +13543,11 @@ var ArtifactStore = class {
|
|
|
13507
13543
|
}
|
|
13508
13544
|
isInsideStore(path) {
|
|
13509
13545
|
const child = relative(this.directory, path);
|
|
13510
|
-
return child === "" || child !== ".." && !child.startsWith(`..${sep}`) && !
|
|
13546
|
+
return child === "" || child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute4(child);
|
|
13511
13547
|
}
|
|
13512
13548
|
isWithin(parent, path) {
|
|
13513
13549
|
const child = relative(parent, path);
|
|
13514
|
-
return child === "" || child !== ".." && !child.startsWith(`..${sep}`) && !
|
|
13550
|
+
return child === "" || child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute4(child);
|
|
13515
13551
|
}
|
|
13516
13552
|
nextArtifactId() {
|
|
13517
13553
|
const id = this.idFactory();
|
|
@@ -13539,11 +13575,11 @@ var ArtifactStore = class {
|
|
|
13539
13575
|
}
|
|
13540
13576
|
}
|
|
13541
13577
|
async removeStoredFile(path) {
|
|
13542
|
-
await
|
|
13578
|
+
await unlink(path).catch(() => {
|
|
13543
13579
|
});
|
|
13544
13580
|
const parent = dirname2(path);
|
|
13545
13581
|
if (parent !== this.directory && this.isInsideStore(parent)) {
|
|
13546
|
-
await
|
|
13582
|
+
await rm(parent, { recursive: true, force: true }).catch(() => {
|
|
13547
13583
|
});
|
|
13548
13584
|
}
|
|
13549
13585
|
}
|
|
@@ -13755,8 +13791,8 @@ var ManagedTargetJanitorClient = class {
|
|
|
13755
13791
|
});
|
|
13756
13792
|
this.worker = worker;
|
|
13757
13793
|
this.ready = false;
|
|
13758
|
-
const ready = new Promise((
|
|
13759
|
-
this.readyResolve =
|
|
13794
|
+
const ready = new Promise((resolve2, reject) => {
|
|
13795
|
+
this.readyResolve = resolve2;
|
|
13760
13796
|
this.readyReject = reject;
|
|
13761
13797
|
});
|
|
13762
13798
|
worker.on("message", (value) => this.handleWorkerMessage(worker, value));
|
|
@@ -13861,7 +13897,7 @@ var ManagedTargetJanitorClient = class {
|
|
|
13861
13897
|
}
|
|
13862
13898
|
const id = this.nextRequestId++;
|
|
13863
13899
|
const request = { id, method, params };
|
|
13864
|
-
return new Promise((
|
|
13900
|
+
return new Promise((resolve2, reject) => {
|
|
13865
13901
|
const timer = setTimeout(() => {
|
|
13866
13902
|
this.pendingRequests.delete(id);
|
|
13867
13903
|
reject(method === "create" ? new BrowserPilotError(
|
|
@@ -13871,7 +13907,7 @@ var ManagedTargetJanitorClient = class {
|
|
|
13871
13907
|
) : new Error(`Managed browser connection timeout: ${method}`));
|
|
13872
13908
|
}, REQUEST_TIMEOUT_MS);
|
|
13873
13909
|
timer.unref();
|
|
13874
|
-
this.pendingRequests.set(id, { resolve:
|
|
13910
|
+
this.pendingRequests.set(id, { resolve: resolve2, reject, timer, method });
|
|
13875
13911
|
worker.send?.(request, (error) => {
|
|
13876
13912
|
if (!error) return;
|
|
13877
13913
|
const pending = this.pendingRequests.get(id);
|
|
@@ -13901,19 +13937,19 @@ var ManagedTargetJanitorClient = class {
|
|
|
13901
13937
|
this.expectedExits.add(worker);
|
|
13902
13938
|
if (cleanup2 && worker.stdin && !worker.stdin.destroyed) worker.stdin.end();
|
|
13903
13939
|
else worker.kill("SIGTERM");
|
|
13904
|
-
await new Promise((
|
|
13940
|
+
await new Promise((resolve2) => {
|
|
13905
13941
|
if (worker.exitCode !== null || worker.signalCode !== null) {
|
|
13906
|
-
|
|
13942
|
+
resolve2();
|
|
13907
13943
|
return;
|
|
13908
13944
|
}
|
|
13909
13945
|
const timer = setTimeout(() => {
|
|
13910
13946
|
worker.kill("SIGKILL");
|
|
13911
|
-
|
|
13947
|
+
resolve2();
|
|
13912
13948
|
}, STOP_TIMEOUT_MS);
|
|
13913
13949
|
timer.unref();
|
|
13914
13950
|
worker.once("exit", () => {
|
|
13915
13951
|
clearTimeout(timer);
|
|
13916
|
-
|
|
13952
|
+
resolve2();
|
|
13917
13953
|
});
|
|
13918
13954
|
});
|
|
13919
13955
|
if (this.worker === worker) this.failWorker(worker, this.unavailableError());
|
|
@@ -13943,7 +13979,7 @@ var ManagedTargetJanitorClient = class {
|
|
|
13943
13979
|
};
|
|
13944
13980
|
|
|
13945
13981
|
// src/version.ts
|
|
13946
|
-
var BROWSER_PILOT_VERSION = "0.3.0-rc.
|
|
13982
|
+
var BROWSER_PILOT_VERSION = "0.3.0-rc.3";
|
|
13947
13983
|
|
|
13948
13984
|
// src/daemon.ts
|
|
13949
13985
|
var CLI_EXECUTABLE_PATH = publicExecutablePath(import.meta.url);
|
|
@@ -13957,7 +13993,7 @@ var browserProfile = process.argv[4] || "";
|
|
|
13957
13993
|
ensureBrokerDirectoriesSync();
|
|
13958
13994
|
for (const legacyFile of ["state.json", "refs.json"]) {
|
|
13959
13995
|
try {
|
|
13960
|
-
unlinkSync2(
|
|
13996
|
+
unlinkSync2(join5(STATE_DIR, legacyFile));
|
|
13961
13997
|
} catch {
|
|
13962
13998
|
}
|
|
13963
13999
|
}
|
|
@@ -13977,7 +14013,7 @@ function cleanup(brokerProcessIdentity) {
|
|
|
13977
14013
|
removeBrokerLocatorSync(brokerProcessIdentity);
|
|
13978
14014
|
}
|
|
13979
14015
|
function readBody(req, maxBytes = 5 * 1024 * 1024) {
|
|
13980
|
-
return new Promise((
|
|
14016
|
+
return new Promise((resolve2, reject) => {
|
|
13981
14017
|
const chunks = [];
|
|
13982
14018
|
let byteLength = 0;
|
|
13983
14019
|
req.on("data", (value) => {
|
|
@@ -13990,7 +14026,7 @@ function readBody(req, maxBytes = 5 * 1024 * 1024) {
|
|
|
13990
14026
|
}
|
|
13991
14027
|
chunks.push(chunk);
|
|
13992
14028
|
});
|
|
13993
|
-
req.on("end", () =>
|
|
14029
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
13994
14030
|
req.on("error", reject);
|
|
13995
14031
|
});
|
|
13996
14032
|
}
|
|
@@ -14057,8 +14093,6 @@ async function syncFetchAll(cdp, currentSessionId) {
|
|
|
14057
14093
|
async function main() {
|
|
14058
14094
|
const startedAt = Date.now();
|
|
14059
14095
|
const brokerProcessIdentity = `${process.pid}:${startedAt}`;
|
|
14060
|
-
writeBrokerStartingSync({ pid: process.pid, startedAt, brokerProcessIdentity });
|
|
14061
|
-
process.once("exit", () => cleanup(brokerProcessIdentity));
|
|
14062
14096
|
const cdp = new ManagedTargetJanitorClient({
|
|
14063
14097
|
onLog: (message) => process.stderr.write(`Managed browser connection: ${message}
|
|
14064
14098
|
`)
|
|
@@ -14075,8 +14109,10 @@ async function main() {
|
|
|
14075
14109
|
const requestTermination = () => {
|
|
14076
14110
|
void terminate();
|
|
14077
14111
|
};
|
|
14112
|
+
process.once("exit", () => cleanup(brokerProcessIdentity));
|
|
14078
14113
|
process.on("SIGTERM", requestTermination);
|
|
14079
14114
|
process.on("SIGINT", requestTermination);
|
|
14115
|
+
writeBrokerStartingSync({ pid: process.pid, startedAt, brokerProcessIdentity });
|
|
14080
14116
|
if (initialWsUrl) await cdp.connect(initialWsUrl);
|
|
14081
14117
|
let activeSessionId;
|
|
14082
14118
|
let currentWsUrl = initialWsUrl;
|