surf-cli 2.14.0 → 2.15.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/README.md +63 -34
- package/agents/gpt-pro.md +19 -0
- package/dist/service-worker/index.js +14 -14
- package/dist/service-worker/index.js.map +1 -1
- package/native/aistudio-parser.cjs +5 -2
- package/native/browser-scheduler.cjs +348 -0
- package/native/browser-session-store.cjs +271 -0
- package/native/cli.cjs +331 -60
- package/native/do-executor.cjs +5 -0
- package/native/host-helpers.cjs +19 -3
- package/native/host-sessions.cjs +8 -1
- package/native/host.cjs +766 -19
- package/native/playbook-cli.cjs +16 -3
- package/native/surf-error.cjs +47 -0
- package/native/tool-scope.cjs +107 -0
- package/native/workflow-definition.cjs +7 -0
- package/package.json +12 -5
- package/pi-extension/surf.ts +80 -52
- package/skills/surf/SKILL.md +49 -22
package/native/host.cjs
CHANGED
|
@@ -48,6 +48,10 @@ const {
|
|
|
48
48
|
const { resolveArgs, runPlaybookOp } = require("./playbook-runtime.cjs");
|
|
49
49
|
const { resolveOp } = require("./playbooks.cjs");
|
|
50
50
|
const { commandMetadata, redactCommandArgs } = require("./workflow-definition.cjs");
|
|
51
|
+
const { BrowserScheduler } = require("./browser-scheduler.cjs");
|
|
52
|
+
const { BrowserSessionStore, validateSessionName } = require("./browser-session-store.cjs");
|
|
53
|
+
const { classifyTool } = require("./tool-scope.cjs");
|
|
54
|
+
const { fromExtensionError, surfError } = require("./surf-error.cjs");
|
|
51
55
|
const MAX_CLIENT_FRAME_BYTES = MAX_FRAME_BYTES;
|
|
52
56
|
const TEST_REQUEST_DEADLINE_MS = process.env.SURF_TEST_MODE === "1" && Number.isFinite(Number(process.env.SURF_TEST_REQUEST_DEADLINE_MS))
|
|
53
57
|
? Number(process.env.SURF_TEST_REQUEST_DEADLINE_MS)
|
|
@@ -379,6 +383,77 @@ const activeStreams = new Map();
|
|
|
379
383
|
const socketContexts = new WeakMap();
|
|
380
384
|
const socketWriters = new WeakMap();
|
|
381
385
|
let requestCounter = 0;
|
|
386
|
+
const browserSessionStore = new BrowserSessionStore();
|
|
387
|
+
let browserIdentity = null;
|
|
388
|
+
const browserIdentityWaiters = new Set();
|
|
389
|
+
const transientFrameContexts = new Map();
|
|
390
|
+
|
|
391
|
+
function setBrowserIdentity(value) {
|
|
392
|
+
if (!value?.browserInstanceId || !value?.browserEpoch) return;
|
|
393
|
+
const identityChanged = browserIdentity && (
|
|
394
|
+
browserIdentity.browserInstanceId !== value.browserInstanceId ||
|
|
395
|
+
browserIdentity.browserEpoch !== value.browserEpoch
|
|
396
|
+
);
|
|
397
|
+
if (identityChanged) transientFrameContexts.clear();
|
|
398
|
+
browserIdentity = {
|
|
399
|
+
browserInstanceId: value.browserInstanceId,
|
|
400
|
+
browserEpoch: value.browserEpoch,
|
|
401
|
+
extensionVersion: value.extensionVersion,
|
|
402
|
+
protocolVersion: value.protocolVersion,
|
|
403
|
+
capabilities: Array.isArray(value.capabilities) ? value.capabilities : [],
|
|
404
|
+
};
|
|
405
|
+
for (const waiter of browserIdentityWaiters) waiter.resolve(browserIdentity);
|
|
406
|
+
browserIdentityWaiters.clear();
|
|
407
|
+
log(`Browser identity connected: ${browserIdentity.browserInstanceId} epoch=${browserIdentity.browserEpoch}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function requireBrowserIdentity(timeoutMs = 5000) {
|
|
411
|
+
if (browserIdentity) return Promise.resolve(browserIdentity);
|
|
412
|
+
return new Promise((resolve, reject) => {
|
|
413
|
+
const waiter = { resolve, reject, timer: null };
|
|
414
|
+
waiter.timer = setTimeout(() => {
|
|
415
|
+
browserIdentityWaiters.delete(waiter);
|
|
416
|
+
reject(surfError("extension_identity_missing", "Surf extension identity is unavailable. Restart the browser and retry."));
|
|
417
|
+
}, timeoutMs);
|
|
418
|
+
waiter.resolve = (identity) => {
|
|
419
|
+
clearTimeout(waiter.timer);
|
|
420
|
+
resolve(identity);
|
|
421
|
+
};
|
|
422
|
+
browserIdentityWaiters.add(waiter);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function handleTargetEvent(message) {
|
|
427
|
+
if (!browserIdentity) return;
|
|
428
|
+
try {
|
|
429
|
+
if (message.event === "tab-removed" && Number.isInteger(message.tabId)) {
|
|
430
|
+
clearTransientFrameContextsByTab(message.tabId);
|
|
431
|
+
browserSessionStore.invalidateByTab(browserIdentity, message.tabId, "tab_gone");
|
|
432
|
+
for (const [streamId, stream] of activeStreams) {
|
|
433
|
+
if (stream.tabId === message.tabId) stopActiveStream(streamId, { notifyExtension: false });
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (message.event === "window-removed" && Number.isInteger(message.windowId)) {
|
|
438
|
+
clearTransientFrameContextsByWindow(message.windowId);
|
|
439
|
+
browserSessionStore.invalidateByWindow(browserIdentity, message.windowId, "window_gone");
|
|
440
|
+
for (const [streamId, stream] of activeStreams) {
|
|
441
|
+
if (stream.windowId === message.windowId) stopActiveStream(streamId, { notifyExtension: false });
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if ((message.event === "navigation" || message.event === "frame-reset") && Number.isInteger(message.tabId)) {
|
|
446
|
+
clearFrameContextsByTab(message.tabId, message.reason || message.event);
|
|
447
|
+
if (message.event === "frame-reset") return;
|
|
448
|
+
const patch = { lastValidatedAt: new Date().toISOString() };
|
|
449
|
+
if (typeof message.url === "string") patch.lastUrl = message.url;
|
|
450
|
+
if (typeof message.title === "string") patch.lastTitle = message.title;
|
|
451
|
+
browserSessionStore.updateTabMetadata(browserIdentity, message.tabId, patch);
|
|
452
|
+
}
|
|
453
|
+
} catch (error) {
|
|
454
|
+
log(`Target event state update failed: ${error.message}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
382
457
|
|
|
383
458
|
function auditSession(event) {
|
|
384
459
|
const context = event.context;
|
|
@@ -392,10 +467,19 @@ function auditSession(event) {
|
|
|
392
467
|
peer: context?.socket?.remoteAddress || "local",
|
|
393
468
|
requestId: request?.id,
|
|
394
469
|
tool: request?.tool,
|
|
470
|
+
session: request?.target?.session || event.session,
|
|
471
|
+
laneKey: request?.laneKey || event.laneKey,
|
|
472
|
+
scope: request?.scope || event.scope,
|
|
473
|
+
resourceKeys: request?.resourceKeys || event.resourceKeys,
|
|
474
|
+
queueMs: event.queueMs,
|
|
395
475
|
elapsedMs: event.elapsedMs,
|
|
396
476
|
})}`);
|
|
397
477
|
}
|
|
398
478
|
|
|
479
|
+
const browserScheduler = new BrowserScheduler({
|
|
480
|
+
audit: (event) => auditSession(event),
|
|
481
|
+
});
|
|
482
|
+
|
|
399
483
|
aiQueue = new BoundedAiQueue({
|
|
400
484
|
maxQueued: 8,
|
|
401
485
|
audit: (event) => auditSession(event),
|
|
@@ -458,9 +542,575 @@ function requestCallExtension(request, tool, message, timeoutMs = 30000, cleanup
|
|
|
458
542
|
});
|
|
459
543
|
}
|
|
460
544
|
|
|
545
|
+
|
|
546
|
+
async function requestExtensionOrThrow(request, tool, message, timeoutMs = 30000, cleanup = false) {
|
|
547
|
+
const result = await requestCallExtension(request, tool, message, timeoutMs, cleanup);
|
|
548
|
+
const error = fromExtensionError(result);
|
|
549
|
+
if (error) throw error;
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function positiveId(value, name) {
|
|
554
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
555
|
+
const parsed = Number(value);
|
|
556
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
557
|
+
throw surfError("target_invalid", `${name} must be a positive integer`);
|
|
558
|
+
}
|
|
559
|
+
return parsed;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function targetLaneKey(identity, tabId) {
|
|
563
|
+
return `tab:${identity.browserEpoch}:${tabId}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const FRAME_CONTEXT_MESSAGE_TYPES = new Set([
|
|
567
|
+
"CLICK_TYPE", "CLICK_TYPE_SUBMIT", "AUTOCOMPLETE_SELECT", "SMART_TYPE",
|
|
568
|
+
"READ_PAGE", "GET_ELEMENT_COORDINATES", "FORM_INPUT", "EVAL_IN_PAGE",
|
|
569
|
+
"SCROLL_TO_ELEMENT", "LOCATE_ROLE", "LOCATE_TEXT", "LOCATE_LABEL",
|
|
570
|
+
"GET_ELEMENT_STYLES", "SELECT_OPTION", "CLICK_REF", "HOVER_REF",
|
|
571
|
+
"CLICK_SELECTOR", "WAIT_FOR_ELEMENT", "FORM_FILL", "UPLOAD_FILE", "SEARCH_PAGE",
|
|
572
|
+
]);
|
|
573
|
+
|
|
574
|
+
function transientFrameContextKey(target) {
|
|
575
|
+
if (!target?.browserInstanceId || !target?.browserEpoch || !target?.tabId) return null;
|
|
576
|
+
return `${target.browserInstanceId}:${target.browserEpoch}:${target.tabId}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function clearTransientFrameContextsByTab(tabId) {
|
|
580
|
+
for (const [key, context] of transientFrameContexts) {
|
|
581
|
+
if (context.tabId === tabId) transientFrameContexts.delete(key);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function clearTransientFrameContextsByWindow(windowId) {
|
|
586
|
+
for (const [key, context] of transientFrameContexts) {
|
|
587
|
+
if (context.windowId === windowId) transientFrameContexts.delete(key);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function clearFrameContextsByTab(tabId, reason) {
|
|
592
|
+
clearTransientFrameContextsByTab(tabId);
|
|
593
|
+
if (!browserIdentity) return;
|
|
594
|
+
browserSessionStore.updateTabMetadata(browserIdentity, tabId, {
|
|
595
|
+
frameContext: null,
|
|
596
|
+
frameContextResetReason: reason,
|
|
597
|
+
frameContextResetAt: new Date().toISOString(),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function currentFrameContext(request) {
|
|
602
|
+
const target = request?.target;
|
|
603
|
+
if (!target?.tabId) return null;
|
|
604
|
+
if (target.session) {
|
|
605
|
+
const record = browserSessionStore.get(request.browserIdentity, target.session);
|
|
606
|
+
const context = record?.frameContext;
|
|
607
|
+
if (
|
|
608
|
+
context &&
|
|
609
|
+
context.browserEpoch === request.browserIdentity?.browserEpoch &&
|
|
610
|
+
context.tabId === target.tabId &&
|
|
611
|
+
Number.isInteger(context.frameId) &&
|
|
612
|
+
context.frameId > 0
|
|
613
|
+
) return context;
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
const key = transientFrameContextKey(target);
|
|
617
|
+
return key ? transientFrameContexts.get(key) || null : null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function applyFrameContextToMessage(request, extensionMessage) {
|
|
621
|
+
if (!extensionMessage || !FRAME_CONTEXT_MESSAGE_TYPES.has(extensionMessage.type)) return;
|
|
622
|
+
const context = currentFrameContext(request);
|
|
623
|
+
if (context) extensionMessage.frameId = context.frameId;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function persistFrameContext(request, frameId, url) {
|
|
627
|
+
const target = request?.target;
|
|
628
|
+
if (!target?.tabId || !Number.isInteger(frameId) || frameId <= 0) return;
|
|
629
|
+
const context = {
|
|
630
|
+
frameId,
|
|
631
|
+
url,
|
|
632
|
+
tabId: target.tabId,
|
|
633
|
+
windowId: target.windowId,
|
|
634
|
+
browserEpoch: request.browserIdentity?.browserEpoch,
|
|
635
|
+
selectedAt: new Date().toISOString(),
|
|
636
|
+
};
|
|
637
|
+
if (target.session) {
|
|
638
|
+
browserSessionStore.update(request.browserIdentity, target.session, {
|
|
639
|
+
frameContext: context,
|
|
640
|
+
frameContextResetReason: null,
|
|
641
|
+
frameContextResetAt: null,
|
|
642
|
+
});
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const key = transientFrameContextKey(target);
|
|
646
|
+
if (key) transientFrameContexts.set(key, context);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function clearFrameContextForRequest(request, reason) {
|
|
650
|
+
const target = request?.target;
|
|
651
|
+
if (!target?.tabId) return;
|
|
652
|
+
if (target.session) {
|
|
653
|
+
const record = browserSessionStore.get(request.browserIdentity, target.session);
|
|
654
|
+
if (record) {
|
|
655
|
+
browserSessionStore.update(request.browserIdentity, target.session, {
|
|
656
|
+
frameContext: null,
|
|
657
|
+
frameContextResetReason: reason,
|
|
658
|
+
frameContextResetAt: new Date().toISOString(),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const key = transientFrameContextKey(target);
|
|
664
|
+
if (key) transientFrameContexts.delete(key);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function updateFrameContextFromResult(request, tool, result) {
|
|
668
|
+
if (!request || !result || result.error) return;
|
|
669
|
+
if (tool === "frame.switch" && Number.isInteger(result.frameId) && result.frameId > 0) {
|
|
670
|
+
persistFrameContext(request, result.frameId, result.url);
|
|
671
|
+
} else if (tool === "frame.main") {
|
|
672
|
+
clearFrameContextForRequest(request, "explicit-main-frame");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function handleFrameContextFailure(request, result) {
|
|
677
|
+
if (!request || result?.errorCode !== "frame_context_reset") return;
|
|
678
|
+
clearFrameContextForRequest(request, result.errorDetails?.reason || "frame-context-reset");
|
|
679
|
+
if (request.target?.session) {
|
|
680
|
+
result.errorDetails = {
|
|
681
|
+
...(result.errorDetails || {}),
|
|
682
|
+
session: request.target.session,
|
|
683
|
+
recoveryCommand: `surf --session ${request.target.session} frame.list`,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function sessionQueueState(identity, record) {
|
|
689
|
+
const laneKey = targetLaneKey(identity, record.tabId);
|
|
690
|
+
const stats = browserScheduler.stats({ laneKey });
|
|
691
|
+
const activeOthers = stats.activeTabLanes.filter((entry) => entry.laneKey !== laneKey);
|
|
692
|
+
return {
|
|
693
|
+
laneKey,
|
|
694
|
+
active: stats.lane?.active || false,
|
|
695
|
+
queued: stats.lane?.queued || 0,
|
|
696
|
+
blockedBy: stats.lane?.blockedBy || null,
|
|
697
|
+
browserWriter: stats.writer,
|
|
698
|
+
queuedBrowserWriters: stats.queuedWriters,
|
|
699
|
+
otherActiveTabLanes: activeOthers,
|
|
700
|
+
totalQueued: stats.queued,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function inspectBrowserTab(request, tabId) {
|
|
705
|
+
return requestExtensionOrThrow(request, "target.inspect", {
|
|
706
|
+
type: "TARGET_INSPECT",
|
|
707
|
+
tabId,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function sessionFailure(code, message, record) {
|
|
712
|
+
return surfError(code, message, {
|
|
713
|
+
session: record.name,
|
|
714
|
+
lastUrl: record.lastUrl,
|
|
715
|
+
target: { tabId: record.tabId, windowId: record.windowId },
|
|
716
|
+
browserEpoch: browserIdentity?.browserEpoch,
|
|
717
|
+
expectedBrowserEpoch: record.browserEpoch,
|
|
718
|
+
recoveryCommand: `surf session.reopen ${record.name}`,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function resolveSessionTarget(request, identity, name) {
|
|
723
|
+
validateSessionName(name);
|
|
724
|
+
const record = browserSessionStore.get(identity, name);
|
|
725
|
+
if (!record) {
|
|
726
|
+
throw surfError("session_unknown", `Unknown session: ${name}`, {
|
|
727
|
+
session: name,
|
|
728
|
+
recoveryCommand: `surf session.ensure ${name} about:blank`,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
if (record.browserEpoch !== identity.browserEpoch) {
|
|
732
|
+
throw sessionFailure(
|
|
733
|
+
"session_epoch_stale",
|
|
734
|
+
`Session ${record.name} belongs to an earlier browser run.`,
|
|
735
|
+
record,
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
if (record.invalidReason === "tab_gone" || record.invalidReason === "window_gone") {
|
|
739
|
+
throw sessionFailure("tab_gone", `The tab for session ${record.name} is gone.`, record);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
let inspected;
|
|
743
|
+
try {
|
|
744
|
+
inspected = await inspectBrowserTab(request, record.tabId);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (error?.code === "tab_gone") {
|
|
747
|
+
browserSessionStore.invalidateByTab(identity, record.tabId, "tab_gone");
|
|
748
|
+
throw sessionFailure("tab_gone", `The tab for session ${record.name} is gone.`, record);
|
|
749
|
+
}
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
752
|
+
if (record.windowId && inspected.windowId !== record.windowId) {
|
|
753
|
+
throw surfError("binding_mismatch", `Session ${record.name} moved from window ${record.windowId} to ${inspected.windowId}.`, {
|
|
754
|
+
session: record.name,
|
|
755
|
+
target: { tabId: record.tabId, windowId: inspected.windowId },
|
|
756
|
+
recoveryCommand: `surf session.rebind ${record.name} --tab-id ${record.tabId} --replace`,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
const updated = browserSessionStore.replace(identity, record.name, {
|
|
760
|
+
...record,
|
|
761
|
+
lastUrl: inspected.url || record.lastUrl,
|
|
762
|
+
lastTitle: inspected.title || record.lastTitle,
|
|
763
|
+
lastValidatedAt: new Date().toISOString(),
|
|
764
|
+
});
|
|
765
|
+
return {
|
|
766
|
+
source: "session",
|
|
767
|
+
session: updated.name,
|
|
768
|
+
strict: true,
|
|
769
|
+
tabId: inspected.tabId,
|
|
770
|
+
windowId: inspected.windowId,
|
|
771
|
+
browserInstanceId: identity.browserInstanceId,
|
|
772
|
+
browserEpoch: identity.browserEpoch,
|
|
773
|
+
url: inspected.url,
|
|
774
|
+
title: inspected.title,
|
|
775
|
+
restricted: inspected.restricted,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function resolveRequestTarget(msg, request, classification) {
|
|
780
|
+
if (classification.targetUse === "host") {
|
|
781
|
+
return { identity: browserIdentity, target: null };
|
|
782
|
+
}
|
|
783
|
+
const identity = await requireBrowserIdentity();
|
|
784
|
+
const args = msg.params?.args || {};
|
|
785
|
+
let sessionName = msg.target?.session || msg.session;
|
|
786
|
+
const sessionSource = msg.target?.source || msg.sessionSource || "explicit";
|
|
787
|
+
const rawTabId = msg.tabId ?? msg.params?.tabId ?? args.tabId;
|
|
788
|
+
const rawWindowId = msg.windowId ?? msg.params?.windowId ?? args.windowId;
|
|
789
|
+
const explicitTabId = positiveId(rawTabId, "tabId");
|
|
790
|
+
const explicitWindowId = positiveId(rawWindowId, "windowId");
|
|
791
|
+
|
|
792
|
+
if (classification.targetUse !== "default-tab") {
|
|
793
|
+
if (sessionName && sessionSource === "explicit" && !String(request.tool).startsWith("session.")) {
|
|
794
|
+
throw surfError("target_not_applicable", `--session does not apply to ${request.tool}`);
|
|
795
|
+
}
|
|
796
|
+
return { identity, target: null };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (sessionName && (explicitTabId || explicitWindowId)) {
|
|
800
|
+
if (sessionSource === "environment") sessionName = undefined;
|
|
801
|
+
else {
|
|
802
|
+
throw surfError("ambiguous_target", "Use either --session or --tab-id/--window-id, not both.", {
|
|
803
|
+
session: sessionName,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
let target;
|
|
809
|
+
if (sessionName) {
|
|
810
|
+
target = await resolveSessionTarget(request, identity, String(sessionName));
|
|
811
|
+
} else if (explicitTabId) {
|
|
812
|
+
const inspected = await inspectBrowserTab(request, explicitTabId);
|
|
813
|
+
target = {
|
|
814
|
+
source: "explicit-tab",
|
|
815
|
+
strict: true,
|
|
816
|
+
tabId: inspected.tabId,
|
|
817
|
+
windowId: inspected.windowId,
|
|
818
|
+
browserInstanceId: identity.browserInstanceId,
|
|
819
|
+
browserEpoch: identity.browserEpoch,
|
|
820
|
+
url: inspected.url,
|
|
821
|
+
title: inspected.title,
|
|
822
|
+
restricted: inspected.restricted,
|
|
823
|
+
};
|
|
824
|
+
} else {
|
|
825
|
+
const inspected = await requestExtensionOrThrow(request, "target.resolve", {
|
|
826
|
+
type: "TARGET_RESOLVE",
|
|
827
|
+
windowId: explicitWindowId,
|
|
828
|
+
allowCreate: true,
|
|
829
|
+
});
|
|
830
|
+
target = {
|
|
831
|
+
source: explicitWindowId ? "explicit-window" : "legacy-implicit",
|
|
832
|
+
strict: false,
|
|
833
|
+
tabId: inspected.tabId,
|
|
834
|
+
windowId: inspected.windowId,
|
|
835
|
+
browserInstanceId: identity.browserInstanceId,
|
|
836
|
+
browserEpoch: identity.browserEpoch,
|
|
837
|
+
url: inspected.url,
|
|
838
|
+
title: inspected.title,
|
|
839
|
+
restricted: inspected.restricted,
|
|
840
|
+
autoCreated: inspected.autoCreated,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
msg.tabId = target.tabId;
|
|
844
|
+
msg.windowId = target.windowId;
|
|
845
|
+
return { identity, target };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function prepareToolRequest(msg, request) {
|
|
849
|
+
const args = msg.params?.args || {};
|
|
850
|
+
const classification = classifyTool(request.tool, args);
|
|
851
|
+
request.scope = classification.scope;
|
|
852
|
+
request.classification = classification;
|
|
853
|
+
request.resourceKeys = classification.resourceKeys || [];
|
|
854
|
+
const { identity, target } = await resolveRequestTarget(msg, request, classification);
|
|
855
|
+
request.browserIdentity = identity;
|
|
856
|
+
request.target = target;
|
|
857
|
+
request.laneKey = target?.tabId ? targetLaneKey(identity, target.tabId) : undefined;
|
|
858
|
+
if (classification.scope === "provider") {
|
|
859
|
+
request.notice = `${request.tool} uses exclusive browser access; other Surf sessions will queue until it finishes.`;
|
|
860
|
+
}
|
|
861
|
+
request.admissionToken = await browserScheduler.acquire({
|
|
862
|
+
scope: classification.scope,
|
|
863
|
+
laneKey: request.laneKey,
|
|
864
|
+
resourceKeys: request.resourceKeys,
|
|
865
|
+
session: target?.session,
|
|
866
|
+
wait: msg.admission?.wait !== false,
|
|
867
|
+
signal: request.signal,
|
|
868
|
+
request,
|
|
869
|
+
});
|
|
870
|
+
request.queuedMs = Math.max(0, request.admissionToken.acquiredAt - request.admissionToken.queuedAt);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function releaseBrowserAdmission(request) {
|
|
874
|
+
if (!request?.admissionToken) return;
|
|
875
|
+
const token = request.admissionToken;
|
|
876
|
+
request.admissionToken = null;
|
|
877
|
+
token.release();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function sessionRecordStatus(identity, request, record, refresh = false) {
|
|
881
|
+
let status = "live";
|
|
882
|
+
let inspected = null;
|
|
883
|
+
if (record.browserEpoch !== identity.browserEpoch) status = "epoch_stale";
|
|
884
|
+
else if (record.invalidReason === "tab_gone" || record.invalidReason === "window_gone") status = "tab_gone";
|
|
885
|
+
else if (refresh) {
|
|
886
|
+
try {
|
|
887
|
+
inspected = await inspectBrowserTab(request, record.tabId);
|
|
888
|
+
if (record.windowId && inspected.windowId !== record.windowId) status = "binding_mismatch";
|
|
889
|
+
else {
|
|
890
|
+
browserSessionStore.replace(identity, record.name, {
|
|
891
|
+
...record,
|
|
892
|
+
lastUrl: inspected.url || record.lastUrl,
|
|
893
|
+
lastTitle: inspected.title || record.lastTitle,
|
|
894
|
+
lastValidatedAt: new Date().toISOString(),
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (error?.code === "tab_gone") {
|
|
899
|
+
browserSessionStore.invalidateByTab(identity, record.tabId, "tab_gone");
|
|
900
|
+
status = "tab_gone";
|
|
901
|
+
} else throw error;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
...record,
|
|
906
|
+
status,
|
|
907
|
+
currentUrl: inspected?.url,
|
|
908
|
+
currentTitle: inspected?.title,
|
|
909
|
+
queue: sessionQueueState(identity, record),
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async function createSessionBinding(request, identity, name, args, previous = null) {
|
|
914
|
+
const mode = args.tab === true ? "tab" : args.window === true ? "window" : previous?.mode || "window";
|
|
915
|
+
if (args.tab === true && args.window === true) {
|
|
916
|
+
throw surfError("session_mode_ambiguous", "Use either --window or --tab, not both.", { session: name });
|
|
917
|
+
}
|
|
918
|
+
const url = args.url || previous?.lastUrl || "about:blank";
|
|
919
|
+
const created = await requestExtensionOrThrow(request, "session.create", {
|
|
920
|
+
type: "SESSION_CREATE_TARGET",
|
|
921
|
+
name,
|
|
922
|
+
url,
|
|
923
|
+
mode,
|
|
924
|
+
focused: args.focused === true,
|
|
925
|
+
windowId: mode === "tab" ? positiveId(args.windowId ?? args["window-id"], "windowId") : undefined,
|
|
926
|
+
});
|
|
927
|
+
try {
|
|
928
|
+
const values = {
|
|
929
|
+
tabId: created.tabId,
|
|
930
|
+
windowId: created.windowId,
|
|
931
|
+
browserEpoch: identity.browserEpoch,
|
|
932
|
+
mode,
|
|
933
|
+
ownership: "surf-created",
|
|
934
|
+
lastUrl: created.url || url,
|
|
935
|
+
lastTitle: created.title,
|
|
936
|
+
groupId: created.groupId,
|
|
937
|
+
frameContext: null,
|
|
938
|
+
frameContextResetReason: null,
|
|
939
|
+
frameContextResetAt: null,
|
|
940
|
+
};
|
|
941
|
+
return previous
|
|
942
|
+
? browserSessionStore.replace(identity, name, values)
|
|
943
|
+
: browserSessionStore.create(identity, name, values);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
await requestExtensionOrThrow(request, "session.cleanup", {
|
|
946
|
+
type: "SESSION_CLOSE_TARGET",
|
|
947
|
+
tabId: created.tabId,
|
|
948
|
+
}, 30000, true).catch(() => {});
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async function handleBrowserSessionCommand(tool, args, request) {
|
|
954
|
+
const identity = await requireBrowserIdentity();
|
|
955
|
+
const name = args.name;
|
|
956
|
+
if (tool !== "session.list") validateSessionName(name);
|
|
957
|
+
|
|
958
|
+
if (tool === "session.new") {
|
|
959
|
+
if (browserSessionStore.get(identity, name)) {
|
|
960
|
+
throw surfError("session_exists", `Session already exists: ${name}`, {
|
|
961
|
+
session: name,
|
|
962
|
+
recoveryCommand: `surf session.ensure ${name}${args.url ? ` ${args.url}` : ""}`,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
const record = await createSessionBinding(request, identity, name, args);
|
|
966
|
+
return { session: await sessionRecordStatus(identity, request, record, false), created: true };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (tool === "session.ensure") {
|
|
970
|
+
const existing = browserSessionStore.get(identity, name);
|
|
971
|
+
if (!existing) {
|
|
972
|
+
const record = await createSessionBinding(request, identity, name, args);
|
|
973
|
+
return { session: await sessionRecordStatus(identity, request, record, false), created: true };
|
|
974
|
+
}
|
|
975
|
+
if (existing.browserEpoch === identity.browserEpoch && !existing.invalidReason) {
|
|
976
|
+
try {
|
|
977
|
+
const target = await resolveSessionTarget(request, identity, name);
|
|
978
|
+
const record = browserSessionStore.get(identity, name);
|
|
979
|
+
return { session: await sessionRecordStatus(identity, request, record, false), created: false, target };
|
|
980
|
+
} catch (error) {
|
|
981
|
+
if (error?.code !== "tab_gone" && error?.code !== "session_epoch_stale") throw error;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const record = await createSessionBinding(request, identity, name, args, existing);
|
|
985
|
+
return { session: await sessionRecordStatus(identity, request, record, false), created: false, reopened: true };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (tool === "session.list") {
|
|
989
|
+
const records = browserSessionStore.list(identity);
|
|
990
|
+
const sessions = [];
|
|
991
|
+
for (const record of records) sessions.push(await sessionRecordStatus(identity, request, record, args.refresh === true));
|
|
992
|
+
return { sessions, browser: identity, scheduler: browserScheduler.stats() };
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const existing = browserSessionStore.get(identity, name);
|
|
996
|
+
if (!existing) {
|
|
997
|
+
throw surfError("session_unknown", `Unknown session: ${name}`, {
|
|
998
|
+
session: name,
|
|
999
|
+
recoveryCommand: `surf session.ensure ${name} about:blank`,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (tool === "session.info") {
|
|
1004
|
+
const session = await sessionRecordStatus(identity, request, existing, args.refresh === true);
|
|
1005
|
+
return {
|
|
1006
|
+
session,
|
|
1007
|
+
browser: identity,
|
|
1008
|
+
sharedProfile: "Cookies, authentication, same-origin storage, downloads, history, bookmarks, and other profile state are shared across Surf sessions.",
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (tool === "session.close") {
|
|
1013
|
+
const shouldCloseTarget = args["keep-target"] !== true && (
|
|
1014
|
+
args["close-target"] === true || existing.ownership === "surf-created"
|
|
1015
|
+
);
|
|
1016
|
+
if (shouldCloseTarget && existing.browserEpoch === identity.browserEpoch) {
|
|
1017
|
+
await requestExtensionOrThrow(request, "session.close", {
|
|
1018
|
+
type: "SESSION_CLOSE_TARGET",
|
|
1019
|
+
tabId: existing.tabId,
|
|
1020
|
+
}, 30000, true).catch((error) => {
|
|
1021
|
+
if (error?.code !== "tab_gone") throw error;
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
browserSessionStore.remove(identity, name);
|
|
1025
|
+
return { success: true, name: existing.name, tabId: existing.tabId, targetClosed: shouldCloseTarget };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (tool === "session.rebind") {
|
|
1029
|
+
const tabId = positiveId(args.tabId ?? args["tab-id"], "tabId");
|
|
1030
|
+
if (!tabId) throw surfError("target_required", "session.rebind requires --tab-id <id>", { session: name });
|
|
1031
|
+
if (existing.browserEpoch === identity.browserEpoch && !existing.invalidReason && args.replace !== true) {
|
|
1032
|
+
throw surfError("session_live", `Session ${name} still has a live binding. Pass --replace to rebind it.`, {
|
|
1033
|
+
session: name,
|
|
1034
|
+
recoveryCommand: `surf session.rebind ${name} --tab-id ${tabId} --replace`,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
const conflict = browserSessionStore.findByTab(identity, tabId, name);
|
|
1038
|
+
if (conflict) {
|
|
1039
|
+
throw surfError("tab_already_bound", `Tab ${tabId} is already bound to session ${conflict.name}.`, {
|
|
1040
|
+
session: conflict.name,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
const inspected = await inspectBrowserTab(request, tabId);
|
|
1044
|
+
const record = browserSessionStore.replace(identity, name, {
|
|
1045
|
+
tabId,
|
|
1046
|
+
windowId: inspected.windowId,
|
|
1047
|
+
browserEpoch: identity.browserEpoch,
|
|
1048
|
+
mode: "tab",
|
|
1049
|
+
ownership: "adopted",
|
|
1050
|
+
lastUrl: inspected.url,
|
|
1051
|
+
lastTitle: inspected.title,
|
|
1052
|
+
frameContext: null,
|
|
1053
|
+
frameContextResetReason: null,
|
|
1054
|
+
frameContextResetAt: null,
|
|
1055
|
+
});
|
|
1056
|
+
return { session: await sessionRecordStatus(identity, request, record, false), rebound: true };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (tool === "session.reopen") {
|
|
1060
|
+
if (existing.browserEpoch === identity.browserEpoch && !existing.invalidReason && args.replace !== true) {
|
|
1061
|
+
try {
|
|
1062
|
+
await inspectBrowserTab(request, existing.tabId);
|
|
1063
|
+
throw surfError("session_live", `Session ${name} is still live. Pass --replace to reopen it.`, {
|
|
1064
|
+
session: name,
|
|
1065
|
+
recoveryCommand: `surf session.reopen ${name} --replace`,
|
|
1066
|
+
});
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
if (error?.code !== "tab_gone") throw error;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (args.replace === true && existing.ownership === "surf-created" && existing.browserEpoch === identity.browserEpoch) {
|
|
1072
|
+
await requestExtensionOrThrow(request, "session.close", {
|
|
1073
|
+
type: "SESSION_CLOSE_TARGET",
|
|
1074
|
+
tabId: existing.tabId,
|
|
1075
|
+
}, 30000, true).catch(() => {});
|
|
1076
|
+
}
|
|
1077
|
+
const record = await createSessionBinding(request, identity, name, args, existing);
|
|
1078
|
+
return { session: await sessionRecordStatus(identity, request, record, false), reopened: true };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
throw surfError("unknown_tool", `Unknown browser session command: ${tool}`);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async function handleNamedTabCommand(tool, args, request) {
|
|
1085
|
+
const identity = await requireBrowserIdentity();
|
|
1086
|
+
if (tool === "tab.name" || tool === "tabs_register") {
|
|
1087
|
+
const name = args.name;
|
|
1088
|
+
validateSessionName(name);
|
|
1089
|
+
const target = request.target;
|
|
1090
|
+
if (!target?.tabId) throw surfError("target_required", "tab.name requires a resolved tab");
|
|
1091
|
+
return browserSessionStore.setNamedTab(identity, name, {
|
|
1092
|
+
tabId: target.tabId,
|
|
1093
|
+
windowId: target.windowId,
|
|
1094
|
+
lastUrl: target.url,
|
|
1095
|
+
lastTitle: target.title,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
if (tool === "tab.unname" || tool === "tabs_unregister") {
|
|
1099
|
+
const removed = browserSessionStore.removeNamedTab(identity, args.name);
|
|
1100
|
+
if (!removed) throw surfError("named_tab_unknown", `No named tab: ${args.name}`);
|
|
1101
|
+
return { success: true, name: removed.name };
|
|
1102
|
+
}
|
|
1103
|
+
if (tool === "tab.named" || tool === "tabs_list_named") {
|
|
1104
|
+
return { tabs: browserSessionStore.listNamedTabs(identity) };
|
|
1105
|
+
}
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
461
1109
|
async function executeMappedHostTool(request, tool, args, tabId) {
|
|
462
1110
|
const extensionMsg = mapToolToMessage(tool, args, tabId);
|
|
463
1111
|
if (!extensionMsg) throw new Error(`Unknown tool: ${tool}`);
|
|
1112
|
+
if (request.target?.strict) extensionMsg.strictTarget = true;
|
|
1113
|
+
applyFrameContextToMessage(request, extensionMsg);
|
|
464
1114
|
if (extensionMsg.type === "UNSUPPORTED_ACTION") throw new Error(extensionMsg.message);
|
|
465
1115
|
if (extensionMsg.type === "LOCAL_WAIT") {
|
|
466
1116
|
await abortableDelay(extensionMsg.seconds * 1000, request.signal);
|
|
@@ -469,7 +1119,7 @@ async function executeMappedHostTool(request, tool, args, tabId) {
|
|
|
469
1119
|
if (extensionMsg.type === "BATCH_EXECUTE" || extensionMsg.type.endsWith("_QUERY")) {
|
|
470
1120
|
throw new Error(`tool ${tool} is not available inside a host-owned workflow`);
|
|
471
1121
|
}
|
|
472
|
-
return
|
|
1122
|
+
return requestExtensionOrThrow(request, tool, extensionMsg, resolveRequestDeadlineMs(tool, args));
|
|
473
1123
|
}
|
|
474
1124
|
|
|
475
1125
|
async function executeNativePlaybook(request, handler, args, options = {}) {
|
|
@@ -595,6 +1245,7 @@ const sessionManager = new HostSessionManager({
|
|
|
595
1245
|
};
|
|
596
1246
|
cleanupRequestTransfers(request)
|
|
597
1247
|
.then(() => {
|
|
1248
|
+
releaseBrowserAdmission(request);
|
|
598
1249
|
sessionManager.complete(context, request.id, "hard-timeout");
|
|
599
1250
|
if (!context.closed) return sendSocket(context.socket, response);
|
|
600
1251
|
})
|
|
@@ -646,12 +1297,14 @@ function completeOwnedRequest(context, id, outcome) {
|
|
|
646
1297
|
request.completionOutcome = outcome;
|
|
647
1298
|
request.completionPromise = new Promise((resolve) => {
|
|
648
1299
|
pendingToolRequests.onDrain(request, () => {
|
|
1300
|
+
releaseBrowserAdmission(request);
|
|
649
1301
|
sessionManager.complete(context, id, request.completionOutcome);
|
|
650
1302
|
resolve();
|
|
651
1303
|
});
|
|
652
1304
|
});
|
|
653
1305
|
return request.completionPromise;
|
|
654
1306
|
}
|
|
1307
|
+
releaseBrowserAdmission(request);
|
|
655
1308
|
sessionManager.complete(context, id, outcome);
|
|
656
1309
|
return Promise.resolve();
|
|
657
1310
|
}
|
|
@@ -715,6 +1368,17 @@ function sendToolResponse(socket, id, result, error) {
|
|
|
715
1368
|
: finalError ? "error" : "completed";
|
|
716
1369
|
await completeOwnedRequest(context, id, outcome);
|
|
717
1370
|
const response = { type: "tool_response", id };
|
|
1371
|
+
if (request?.target) {
|
|
1372
|
+
response.target = {
|
|
1373
|
+
source: request.target.source,
|
|
1374
|
+
session: request.target.session,
|
|
1375
|
+
tabId: request.target.tabId,
|
|
1376
|
+
windowId: request.target.windowId,
|
|
1377
|
+
browserEpoch: request.target.browserEpoch,
|
|
1378
|
+
queuedMs: request.queuedMs || 0,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
if (request?.notice) response.notice = request.notice;
|
|
718
1382
|
if (formattedError) response.error = formattedError;
|
|
719
1383
|
else response.result = { content: formatToolContent(output, log, { suppressImages: Boolean(context?.isRemote) }) };
|
|
720
1384
|
if (!context?.closed) await sendSocket(socket, response);
|
|
@@ -729,15 +1393,28 @@ function stopActiveStream(streamId, { notifyExtension = true } = {}) {
|
|
|
729
1393
|
if (notifyExtension) writeMessage({ type: "STREAM_STOP", streamId });
|
|
730
1394
|
}
|
|
731
1395
|
|
|
732
|
-
function
|
|
1396
|
+
async function resolveStreamRequest(msg) {
|
|
1397
|
+
const tool = msg.streamType === "STREAM_CONSOLE" ? "console" : "network";
|
|
1398
|
+
const controller = new AbortController();
|
|
1399
|
+
const request = { tool, signal: controller.signal };
|
|
1400
|
+
const classification = classifyTool(tool, msg.options || {});
|
|
1401
|
+
const { target } = await resolveRequestTarget(msg, request, classification);
|
|
1402
|
+
if (!target?.tabId) throw surfError("target_required", `${tool} stream requires a resolved tab`);
|
|
1403
|
+
return target;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function handleStreamRequest(msg, socket, target) {
|
|
733
1407
|
const { streamType, options, id: originalId } = msg;
|
|
734
|
-
const tabId =
|
|
1408
|
+
const tabId = target.tabId;
|
|
735
1409
|
const streamId = ++requestCounter;
|
|
736
1410
|
|
|
737
1411
|
activeStreams.set(streamId, {
|
|
738
1412
|
socket,
|
|
739
1413
|
originalId,
|
|
740
1414
|
streamType,
|
|
1415
|
+
tabId,
|
|
1416
|
+
windowId: target.windowId,
|
|
1417
|
+
session: target.session,
|
|
741
1418
|
});
|
|
742
1419
|
|
|
743
1420
|
writeMessage({
|
|
@@ -745,9 +1422,20 @@ function handleStreamRequest(msg, socket) {
|
|
|
745
1422
|
streamId,
|
|
746
1423
|
options: options || {},
|
|
747
1424
|
tabId,
|
|
1425
|
+
strictTarget: target.strict === true,
|
|
748
1426
|
});
|
|
749
1427
|
|
|
750
|
-
sendSocket(socket, {
|
|
1428
|
+
sendSocket(socket, {
|
|
1429
|
+
type: "stream_started",
|
|
1430
|
+
streamId,
|
|
1431
|
+
target: {
|
|
1432
|
+
source: target.source,
|
|
1433
|
+
session: target.session,
|
|
1434
|
+
tabId: target.tabId,
|
|
1435
|
+
windowId: target.windowId,
|
|
1436
|
+
browserEpoch: target.browserEpoch,
|
|
1437
|
+
},
|
|
1438
|
+
}, { stream: true }).catch((error) => {
|
|
751
1439
|
log(`Error sending stream_started: ${error.message}`);
|
|
752
1440
|
stopActiveStream(streamId);
|
|
753
1441
|
socket.destroy(error);
|
|
@@ -788,6 +1476,18 @@ function handleToolRequest(msg, socket, requestContext = requestStorage.getStore
|
|
|
788
1476
|
requestContext.args = args || {};
|
|
789
1477
|
requestContext.activityStartedAt = new Date().toISOString();
|
|
790
1478
|
if (!tool.startsWith("playbook.")) journalCommand(tool, args || {}, { tabId });
|
|
1479
|
+
if (tool.startsWith("session.")) {
|
|
1480
|
+
handleBrowserSessionCommand(tool, args || {}, requestContext)
|
|
1481
|
+
.then((result) => sendToolResponse(socket, originalId, result, null))
|
|
1482
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error));
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (["tab.name", "tabs_register", "tab.unname", "tabs_unregister", "tab.named", "tabs_list_named"].includes(tool)) {
|
|
1486
|
+
handleNamedTabCommand(tool, args || {}, requestContext)
|
|
1487
|
+
.then((result) => sendToolResponse(socket, originalId, result, null))
|
|
1488
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error));
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
791
1491
|
if (tool === "playbook.run") {
|
|
792
1492
|
runHostPlaybook(msg, requestContext)
|
|
793
1493
|
.then((result) => sendToolResponse(socket, originalId, { output: JSON.stringify(result) }, null))
|
|
@@ -806,6 +1506,8 @@ function handleToolRequest(msg, socket, requestContext = requestStorage.getStore
|
|
|
806
1506
|
sendToolResponse(socket, originalId, null, `Unknown tool: ${tool}`);
|
|
807
1507
|
return;
|
|
808
1508
|
}
|
|
1509
|
+
if (requestContext.target?.strict) extensionMsg.strictTarget = true;
|
|
1510
|
+
applyFrameContextToMessage(requestContext, extensionMsg);
|
|
809
1511
|
|
|
810
1512
|
if (extensionMsg.type === "UNSUPPORTED_ACTION") {
|
|
811
1513
|
sendToolResponse(socket, originalId, null, extensionMsg.message);
|
|
@@ -1535,25 +2237,39 @@ function handleToolRequest(msg, socket, requestContext = requestStorage.getStore
|
|
|
1535
2237
|
|
|
1536
2238
|
if (extensionMsg.type === "NAMED_TAB_SWITCH" || extensionMsg.type === "NAMED_TAB_CLOSE") {
|
|
1537
2239
|
const { name, type: opType } = extensionMsg;
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
2240
|
+
(async () => {
|
|
2241
|
+
const identity = await requireBrowserIdentity();
|
|
2242
|
+
const named = browserSessionStore.getNamedTab(identity, name);
|
|
2243
|
+
if (!named) {
|
|
2244
|
+
throw surfError("named_tab_unknown", `No named tab: ${name}`, {
|
|
2245
|
+
recoveryCommand: "surf tab.named",
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
if (named.browserEpoch !== identity.browserEpoch) {
|
|
2249
|
+
browserSessionStore.removeNamedTab(identity, name);
|
|
2250
|
+
throw surfError("named_tab_stale", `Named tab ${name} belongs to an earlier browser run.`, {
|
|
2251
|
+
recoveryCommand: `surf tab.name ${name} --tab-id ${named.tabId}`,
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
try {
|
|
2255
|
+
await inspectBrowserTab(requestContext, named.tabId);
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
if (error?.code === "tab_gone") browserSessionStore.removeNamedTab(identity, name);
|
|
2258
|
+
throw error;
|
|
1545
2259
|
}
|
|
1546
2260
|
const actionType = opType === "NAMED_TAB_SWITCH" ? "SWITCH_TAB" : "CLOSE_TAB";
|
|
1547
2261
|
const actionTool = opType === "NAMED_TAB_SWITCH" ? "switch_tab" : "close_tab";
|
|
1548
|
-
|
|
2262
|
+
const result = await requestExtensionOrThrow(
|
|
1549
2263
|
requestContext,
|
|
1550
2264
|
actionTool,
|
|
1551
|
-
{ type: actionType, tabId:
|
|
2265
|
+
{ type: actionType, tabId: named.tabId },
|
|
1552
2266
|
30000,
|
|
1553
2267
|
actionTool === "close_tab",
|
|
1554
2268
|
);
|
|
1555
|
-
|
|
1556
|
-
|
|
2269
|
+
if (actionTool === "close_tab") browserSessionStore.removeNamedTab(identity, name);
|
|
2270
|
+
return result;
|
|
2271
|
+
})().then((result) => sendToolResponse(socket, originalId, result, null))
|
|
2272
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error));
|
|
1557
2273
|
return;
|
|
1558
2274
|
}
|
|
1559
2275
|
|
|
@@ -1605,6 +2321,8 @@ function executeBatch(actions, tabId, socket, originalId, requestContext = reque
|
|
|
1605
2321
|
const toolArgs = mapBatchActionToArgs(action);
|
|
1606
2322
|
|
|
1607
2323
|
const extensionMsg = mapToolToMessage(toolName, toolArgs, tabId);
|
|
2324
|
+
if (requestContext.target?.strict && extensionMsg) extensionMsg.strictTarget = true;
|
|
2325
|
+
applyFrameContextToMessage(requestContext, extensionMsg);
|
|
1608
2326
|
if (!extensionMsg || extensionMsg.type === "UNSUPPORTED_ACTION") {
|
|
1609
2327
|
results.push({ index: currentIndex, type: action.type, success: false, error: "Unsupported action" });
|
|
1610
2328
|
sendToolResponse(socket, originalId, {
|
|
@@ -1706,6 +2424,16 @@ function processInput() {
|
|
|
1706
2424
|
try {
|
|
1707
2425
|
const msg = JSON.parse(jsonStr);
|
|
1708
2426
|
log(`Received from extension: ${msg.type || "unknown"}${msg.id !== undefined ? ` id=${msg.id}` : ""}`);
|
|
2427
|
+
|
|
2428
|
+
if (msg.type === "EXTENSION_HELLO") {
|
|
2429
|
+
setBrowserIdentity(msg);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
if (msg.type === "TARGET_EVENT") {
|
|
2434
|
+
handleTargetEvent(msg);
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
1709
2437
|
|
|
1710
2438
|
if (msg.type === "GET_AUTH") {
|
|
1711
2439
|
log("Handling GET_AUTH from extension");
|
|
@@ -1786,6 +2514,8 @@ function processInput() {
|
|
|
1786
2514
|
}
|
|
1787
2515
|
return;
|
|
1788
2516
|
}
|
|
2517
|
+
handleFrameContextFailure(pending.request, msg);
|
|
2518
|
+
updateFrameContextFromResult(pending.request, pending.tool, msg);
|
|
1789
2519
|
if (pending.resolve || pending.onComplete) {
|
|
1790
2520
|
pendingToolRequests.resolve(msg.id, msg);
|
|
1791
2521
|
return;
|
|
@@ -1857,7 +2587,7 @@ function processInput() {
|
|
|
1857
2587
|
.then(() => requestCallExtension(
|
|
1858
2588
|
pending.request,
|
|
1859
2589
|
"screenshot",
|
|
1860
|
-
{ type: "EXECUTE_SCREENSHOT", tabId },
|
|
2590
|
+
{ type: "EXECUTE_SCREENSHOT", tabId, strictTarget: pending.request?.target?.strict === true },
|
|
1861
2591
|
))
|
|
1862
2592
|
.then((screenshotMsg) => {
|
|
1863
2593
|
if (screenshotMsg.base64) {
|
|
@@ -1916,7 +2646,7 @@ function processInput() {
|
|
|
1916
2646
|
!msg.output && !msg.messages && !msg.requests;
|
|
1917
2647
|
|
|
1918
2648
|
if (isPureError) {
|
|
1919
|
-
sendToolResponse(socket, originalId, null, msg.error);
|
|
2649
|
+
sendToolResponse(socket, originalId, null, fromExtensionError(msg) || msg.error);
|
|
1920
2650
|
} else {
|
|
1921
2651
|
sendToolResponse(socket, originalId, msg, null);
|
|
1922
2652
|
}
|
|
@@ -1946,6 +2676,12 @@ const connectedSockets = new Set();
|
|
|
1946
2676
|
|
|
1947
2677
|
process.stdin.on("end", () => {
|
|
1948
2678
|
log("stdin ended (extension disconnected), notifying clients");
|
|
2679
|
+
browserIdentity = null;
|
|
2680
|
+
for (const waiter of browserIdentityWaiters) {
|
|
2681
|
+
clearTimeout(waiter.timer);
|
|
2682
|
+
waiter.reject(surfError("extension_disconnected", "Surf extension disconnected."));
|
|
2683
|
+
}
|
|
2684
|
+
browserIdentityWaiters.clear();
|
|
1949
2685
|
for (const socket of Array.from(connectedSockets)) {
|
|
1950
2686
|
sendSocket(socket, {
|
|
1951
2687
|
type: "extension_disconnected",
|
|
@@ -2066,8 +2802,9 @@ const handleClient = (socket) => {
|
|
|
2066
2802
|
let request;
|
|
2067
2803
|
try {
|
|
2068
2804
|
const deadlineMs = TEST_REQUEST_DEADLINE_MS || resolveRequestDeadlineMs(tool, msg.params?.args);
|
|
2069
|
-
request = await sessionManager.beginRequest(context, { id: msg.id, tool, deadlineMs });
|
|
2805
|
+
request = await sessionManager.beginRequest(context, { id: msg.id, tool, deadlineMs, skipLease: true });
|
|
2070
2806
|
request.context = context;
|
|
2807
|
+
request.args = msg.params?.args || {};
|
|
2071
2808
|
} catch (error) {
|
|
2072
2809
|
if (transferState) await discardRequestTransfers(msg, transferState);
|
|
2073
2810
|
await sendSocket(socket, { type: "tool_response", id: msg.id || null, error: { content: [{ type: "text", text: error.message }] } }).catch(() => {});
|
|
@@ -2078,8 +2815,11 @@ const handleClient = (socket) => {
|
|
|
2078
2815
|
if (tool.startsWith("oracle.")) oracleHost.assertLocal(request);
|
|
2079
2816
|
if (isRemote) {
|
|
2080
2817
|
await applyRequestTransfers(msg, request, transferState, ensureTransferState);
|
|
2818
|
+
request.args = msg.params?.args || {};
|
|
2081
2819
|
}
|
|
2082
2820
|
throwIfAborted(request.signal, "Request cancelled");
|
|
2821
|
+
await prepareToolRequest(msg, request);
|
|
2822
|
+
throwIfAborted(request.signal, "Request cancelled");
|
|
2083
2823
|
requestStorage.run(request, () => handleToolRequest(msg, socket, request));
|
|
2084
2824
|
} catch (e) {
|
|
2085
2825
|
await discardRequestTransfers(msg, transferState);
|
|
@@ -2100,7 +2840,14 @@ const handleClient = (socket) => {
|
|
|
2100
2840
|
return;
|
|
2101
2841
|
}
|
|
2102
2842
|
log(`Handling stream_request: ${msg.streamType}`);
|
|
2103
|
-
|
|
2843
|
+
try {
|
|
2844
|
+
const target = await resolveStreamRequest(msg);
|
|
2845
|
+
handleStreamRequest(msg, socket, target);
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
sessionManager.stopStream(context);
|
|
2848
|
+
const formatted = formatToolError(error);
|
|
2849
|
+
await sendSocket(socket, { type: "stream_error", error: formatted }).catch(() => {});
|
|
2850
|
+
}
|
|
2104
2851
|
return;
|
|
2105
2852
|
}
|
|
2106
2853
|
|