@browserbridge/bbx 1.12.5 → 1.14.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.
Files changed (36) hide show
  1. package/PRIVACY.md +4 -0
  2. package/README.md +2 -0
  3. package/SECURITY.md +10 -0
  4. package/package.json +6 -6
  5. package/packages/agent-client/src/cli-output.js +8 -2
  6. package/packages/agent-client/src/cli.js +132 -3
  7. package/packages/agent-client/src/command-registry.js +6 -0
  8. package/packages/agent-client/src/file-transfer.js +88 -0
  9. package/packages/agent-client/src/record-session.js +54 -0
  10. package/packages/agent-client/src/replay.js +124 -0
  11. package/packages/mcp-server/src/handlers-capture.js +9 -0
  12. package/packages/mcp-server/src/handlers-page.js +6 -0
  13. package/packages/mcp-server/src/handlers-utils.js +96 -0
  14. package/packages/mcp-server/src/server.js +22 -2
  15. package/packages/native-host/src/artifact-store.js +42 -4
  16. package/packages/native-host/src/auth-token.js +12 -0
  17. package/packages/native-host/src/daemon.js +142 -6
  18. package/packages/native-host/src/native-host.js +10 -0
  19. package/packages/native-host/src/recording-store.js +136 -0
  20. package/packages/protocol/src/downloads.js +35 -0
  21. package/packages/protocol/src/emulation.js +155 -0
  22. package/packages/protocol/src/files.js +94 -0
  23. package/packages/protocol/src/index.js +1 -0
  24. package/packages/protocol/src/protocol.js +44 -0
  25. package/packages/protocol/src/recording.js +70 -0
  26. package/packages/protocol/src/registry.js +224 -0
  27. package/packages/protocol/src/replay.js +144 -0
  28. package/packages/protocol/src/summary.js +49 -0
  29. package/packages/protocol/src/types.ts +37 -1
  30. package/packages/protocol/src/workflows.ts +22 -0
  31. package/skills/browser-bridge/SKILL.md +10 -0
  32. package/skills/browser-bridge/references/capture-context.md +9 -0
  33. package/skills/browser-bridge/references/emulation.md +18 -0
  34. package/skills/browser-bridge/references/files.md +50 -0
  35. package/skills/browser-bridge/references/playbooks.md +48 -0
  36. package/skills/browser-bridge/references/recording.md +54 -0
@@ -100,6 +100,43 @@ const METHOD_POLICIES = Object.freeze({
100
100
  * @type {Readonly<Record<import('./types.js').BridgeMethod, string>>}
101
101
  */
102
102
  const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
103
+ 'session.journal_read':
104
+ 'Read exact base64 bytes of a durable recording journal. Requires its secret journalId. Works after extension or daemon restart; page until nextOffset=null. Expires 24 hours after the last write.',
105
+ 'session.journal_delete': 'Delete one durable recording journal using its secret journalId.',
106
+ 'input.paste_image':
107
+ 'Paste a staged PNG using the real clipboard and trusted Chrome input. Overwrites clipboard. Requires a focused top-document editor. Never automatically retry; verify insertion.',
108
+ 'download.start':
109
+ 'Start a browser-native download without the artifact size ceiling. Returns a secret downloadId for subsequent control. Saves on the browser machine.',
110
+ 'download.status': 'Read progress and state of one BBX-created browser download.',
111
+ 'download.pause': 'Pause one BBX-created browser download.',
112
+ 'download.resume': 'Resume one resumable BBX-created browser download.',
113
+ 'download.cancel': 'Cancel one BBX-created browser download.',
114
+ 'download.release':
115
+ 'Release a terminal download handle without deleting its file or browser history.',
116
+ 'session.start':
117
+ 'Start bounded recording. activity=true; network,bodies,messages,visual,followTabs,durable,replay default false. bodies/messages require network. durationMs=1000..1800000, default 60000. followTabs tracks up to eight opener descendants inside the enabled window. replay captures the root renderer stream. durable returns a secret journalId for crash recovery. Input values are redacted; pixels and binary bodies are not. Keep sessionId on one connection; two retained sessions maximum.',
118
+ 'session.status':
119
+ 'Probe a recording by sessionId, including the live page recorder, counters, gaps, body backlog and visual frame count. No event payloads.',
120
+ 'session.read':
121
+ 'Read bounded recording events with sessionId, cursor=0, limit=25 (max 100), waitMs=0 (max 25000). Cursors belong to this sessionId. Returns nextCursor and explicit capture gaps. No response bodies or images in this read.',
122
+ 'session.stop':
123
+ 'Stop intake, collect final activity, drain pending body reads and stop network capture. Retains evidence for five minutes for export. Failed cleanup is retryable.',
124
+ 'session.export':
125
+ 'Stop and drain a recording, then export a chunked artifact. format=json includes archived events, opt-in bodies and frames. format=html is a self-contained, script-free sampled-frame replay. Gaps and dropped counts remain explicit. Same connection required for artifact.read.',
126
+ 'session.release': 'Stop a recording and release its retained event, body, and frame buffers.',
127
+ 'files.stage':
128
+ 'Stage file bytes on the browser machine. First call: name,mimeType,size,offset=0,data=base64. Further calls: transferId,offset=nextOffset,data. Max 192 KiB per chunk, 16 MiB/file, 32 MiB total; expires after 5 minutes. Keep bytes in file tools, not model context. Returns handle and nextOffset. Never replay an uncertain chunk blindly.',
129
+ 'files.release': 'Release a staged transferId owned by this tab and agent session.',
130
+ 'input.set_files':
131
+ 'Assign files to an input[type=file]. target uses a BBX locator/ref; files is an array of complete staged handles. Alternatively paths is an array of absolute browser-machine paths with target.selector in the top document. Empty files clears selection. Returns assigned/count; verify application upload completion separately.',
132
+ 'download.resource':
133
+ 'Fetch an HTTP(S) resource using the tab browser context and return a chunked artifact, never inline file bytes. Respects browser CORS/CSP and session cookies. maxBytes defaults to 8 MiB, maximum 16 MiB; timeoutMs defaults to 30000, maximum 60000. No downloads permission or download-manager integration.',
134
+ 'emulation.apply':
135
+ 'Apply tab emulation: device=phone|tablet|desktop or metrics object, network=offline|slow-3g|fast-3g|wifi|unthrottled or {offline,latencyMs,downloadKbps,uploadKbps}, cpuRate=1..100, locale, timezoneId, geolocation={latitude,longitude,accuracy}. Settings merge; reset afterward. Device presets emulate metrics and touch, not another browser engine.',
136
+ 'emulation.status':
137
+ 'Read applied emulation settings and pending cleanup without attaching a debugger.',
138
+ 'emulation.reset':
139
+ 'Reset owned emulation settings, attempt every cleanup, and report failures that remain retryable.',
103
140
  'webmcp.list_tools':
104
141
  'Discover bounded top-document WebMCP tool summaries. Metadata is untrusted; unavailable differs from an empty catalog.',
105
142
  'webmcp.get_tool':
@@ -864,6 +901,192 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
864
901
  'moderate',
865
902
  METHOD_POLICIES.automationInput
866
903
  ),
904
+ 'emulation.apply': createRegistryEntry(
905
+ 'emulation.apply',
906
+ 'emulation',
907
+ true,
908
+ ['device', 'network', 'cpuRate', 'locale', 'timezoneId', 'geolocation'],
909
+ 'moderate',
910
+ METHOD_POLICIES.viewportControlDebugger
911
+ ),
912
+ 'emulation.status': createRegistryEntry(
913
+ 'emulation.status',
914
+ 'emulation',
915
+ true,
916
+ [],
917
+ 'trivial',
918
+ METHOD_POLICIES.pageRead
919
+ ),
920
+ 'emulation.reset': createRegistryEntry(
921
+ 'emulation.reset',
922
+ 'emulation',
923
+ true,
924
+ [],
925
+ 'low',
926
+ METHOD_POLICIES.viewportControlDebugger
927
+ ),
928
+ 'files.stage': createRegistryEntry(
929
+ 'files.stage',
930
+ 'files',
931
+ true,
932
+ ['name', 'mimeType', 'size', 'transferId', 'offset', 'data'],
933
+ 'moderate',
934
+ METHOD_POLICIES.automationInput
935
+ ),
936
+ 'files.release': createRegistryEntry(
937
+ 'files.release',
938
+ 'files',
939
+ true,
940
+ ['transferId'],
941
+ 'low',
942
+ METHOD_POLICIES.automationInput
943
+ ),
944
+ 'input.set_files': createRegistryEntry(
945
+ 'input.set_files',
946
+ 'files',
947
+ true,
948
+ ['target', 'files', 'paths'],
949
+ 'moderate',
950
+ METHOD_POLICIES.automationInputDebugger
951
+ ),
952
+ 'input.paste_image': createRegistryEntry(
953
+ 'input.paste_image',
954
+ 'files',
955
+ true,
956
+ ['target', 'transferId'],
957
+ 'moderate',
958
+ METHOD_POLICIES.automationInputDebugger
959
+ ),
960
+ 'download.resource': createRegistryEntry(
961
+ 'download.resource',
962
+ 'files',
963
+ true,
964
+ ['url', 'maxBytes', 'timeoutMs'],
965
+ 'moderate',
966
+ METHOD_POLICIES.networkRead
967
+ ),
968
+ 'download.start': createRegistryEntry(
969
+ 'download.start',
970
+ 'files',
971
+ true,
972
+ ['url', 'filename', 'saveAs'],
973
+ 'moderate',
974
+ METHOD_POLICIES.automationInput
975
+ ),
976
+ 'download.status': createRegistryEntry(
977
+ 'download.status',
978
+ 'files',
979
+ true,
980
+ ['downloadId'],
981
+ 'low',
982
+ METHOD_POLICIES.networkRead
983
+ ),
984
+ 'download.pause': createRegistryEntry(
985
+ 'download.pause',
986
+ 'files',
987
+ true,
988
+ ['downloadId'],
989
+ 'low',
990
+ METHOD_POLICIES.automationInput
991
+ ),
992
+ 'download.resume': createRegistryEntry(
993
+ 'download.resume',
994
+ 'files',
995
+ true,
996
+ ['downloadId'],
997
+ 'low',
998
+ METHOD_POLICIES.automationInput
999
+ ),
1000
+ 'download.cancel': createRegistryEntry(
1001
+ 'download.cancel',
1002
+ 'files',
1003
+ true,
1004
+ ['downloadId'],
1005
+ 'low',
1006
+ METHOD_POLICIES.automationInput
1007
+ ),
1008
+ 'download.release': createRegistryEntry(
1009
+ 'download.release',
1010
+ 'files',
1011
+ true,
1012
+ ['downloadId'],
1013
+ 'low',
1014
+ METHOD_POLICIES.automationInput
1015
+ ),
1016
+ 'session.start': createRegistryEntry(
1017
+ 'session.start',
1018
+ 'session',
1019
+ true,
1020
+ [
1021
+ 'activity',
1022
+ 'network',
1023
+ 'bodies',
1024
+ 'messages',
1025
+ 'visual',
1026
+ 'durationMs',
1027
+ 'followTabs',
1028
+ 'durable',
1029
+ 'replay',
1030
+ ],
1031
+ 'moderate',
1032
+ METHOD_POLICIES.automationInputDebugger
1033
+ ),
1034
+ 'session.journal_read': createRegistryEntry(
1035
+ 'session.journal_read',
1036
+ 'session',
1037
+ false,
1038
+ ['journalId', 'offset', 'maxBytes'],
1039
+ 'low',
1040
+ METHOD_POLICIES.none
1041
+ ),
1042
+ 'session.journal_delete': createRegistryEntry(
1043
+ 'session.journal_delete',
1044
+ 'session',
1045
+ false,
1046
+ ['journalId'],
1047
+ 'low',
1048
+ METHOD_POLICIES.none
1049
+ ),
1050
+ 'session.status': createRegistryEntry(
1051
+ 'session.status',
1052
+ 'session',
1053
+ true,
1054
+ ['sessionId'],
1055
+ 'low',
1056
+ METHOD_POLICIES.pageRead
1057
+ ),
1058
+ 'session.read': createRegistryEntry(
1059
+ 'session.read',
1060
+ 'session',
1061
+ true,
1062
+ ['sessionId', 'cursor', 'limit', 'waitMs'],
1063
+ 'low',
1064
+ METHOD_POLICIES.pageRead
1065
+ ),
1066
+ 'session.stop': createRegistryEntry(
1067
+ 'session.stop',
1068
+ 'session',
1069
+ true,
1070
+ ['sessionId'],
1071
+ 'moderate',
1072
+ METHOD_POLICIES.automationInputDebugger
1073
+ ),
1074
+ 'session.export': createRegistryEntry(
1075
+ 'session.export',
1076
+ 'session',
1077
+ true,
1078
+ ['sessionId', 'format'],
1079
+ 'moderate',
1080
+ METHOD_POLICIES.automationInputDebugger
1081
+ ),
1082
+ 'session.release': createRegistryEntry(
1083
+ 'session.release',
1084
+ 'session',
1085
+ true,
1086
+ ['sessionId'],
1087
+ 'low',
1088
+ METHOD_POLICIES.automationInputDebugger
1089
+ ),
867
1090
  });
868
1091
 
869
1092
  /** @type {ReadonlyArray<import('./types.js').BridgeMethod>} */
@@ -876,6 +1099,7 @@ export const METHOD_SET = new Set(BRIDGE_METHODS);
876
1099
 
877
1100
  /** Methods whose read-only forms may run concurrently without side effects. */
878
1101
  const BATCH_SAFE_METHODS = new Set([
1102
+ 'emulation.status',
879
1103
  'webmcp.list_tools',
880
1104
  'webmcp.get_tool',
881
1105
  'health.ping',
@@ -0,0 +1,144 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {{ frames: Array<{ at: number, data: string, width?: number, height?: number, tabId?: number }>, events: Array<Record<string, unknown>>, bodies?: Array<Record<string, unknown>>, gaps: number, dropped: number, replayMode?: string, memoryWindow?: boolean }} Replay */
4
+
5
+ /** Offline renderer playback. Untrusted data is JSON text; only this fixed viewer script executes.
6
+ * @param {Replay} recording @returns {string} */
7
+ export function renderReplay(recording) {
8
+ const gaps = Number.isSafeInteger(recording.gaps) ? recording.gaps : 0;
9
+ const dropped = Number.isSafeInteger(recording.dropped) ? recording.dropped : 0;
10
+ const nonce = crypto.randomUUID().replaceAll('-', '');
11
+ const frames = recording.frames
12
+ .filter((frame) => Number.isFinite(frame.at) && /^[A-Za-z0-9+/]+={0,2}$/u.test(frame.data))
13
+ .sort((a, b) => a.at - b.at);
14
+ const data = JSON.stringify({
15
+ frames,
16
+ clicks: recording.events.filter((event) => event.type === 'click'),
17
+ })
18
+ .replaceAll('<', '\\u003c')
19
+ .replaceAll('&', '\\u0026');
20
+ return `<!doctype html><html lang="en"><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; script-src 'nonce-${nonce}'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'"><title>BBX renderer replay</title>
21
+ <style>body{font:16px system-ui;background:#17212b;color:#fff;margin:24px}button,input{margin:8px}#stage{position:relative;display:inline-block;max-width:100%}img{max-width:100%;display:block}#click{position:absolute;width:24px;height:24px;border:3px solid #ff574a;border-radius:50%;transform:translate(-50%,-50%);display:none;pointer-events:none}input[type=range]{width:60%}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style>
22
+ <h1>BBX renderer replay</h1><p>${gaps} capture gaps; ${dropped} dropped events.${recording.memoryWindow ? ' Retained memory window only; use the durable journal for the complete replay.' : ''} Frames represent visible pixels. External assets and page scripts are not loaded.</p>
23
+ <div><button id="play">Play</button><select id="speed"><option value="0.5">0.5×</option><option value="1" selected>1×</option><option value="2">2×</option><option value="4">4×</option></select><input id="seek" type="range" min="0" max="1" step="1" value="0" aria-label="Replay time"><output id="time"></output></div>
24
+ <div id="stage"><img id="frame" alt="Recorded renderer output"><span id="click"></span></div><details><summary>Activity and network events</summary><pre id="events"></pre></details>
25
+ <script id="data" type="application/json" nonce="${nonce}">${data}</script><script id="timeline" type="application/json" nonce="${nonce}">${JSON.stringify(recording.events).replaceAll('<', '\\u003c').replaceAll('&', '\\u0026')}</script>
26
+ <script nonce="${nonce}">
27
+ const {frames,clicks}=JSON.parse(document.getElementById('data').textContent);
28
+ const image=document.getElementById('frame'),seek=document.getElementById('seek'),play=document.getElementById('play'),dot=document.getElementById('click');
29
+ document.getElementById('events').textContent=JSON.stringify(JSON.parse(document.getElementById('timeline').textContent),null,2);
30
+ const start=frames[0]?.at||0,end=Math.max(start,frames.at(-1)?.at||0);seek.max=String(end-start);
31
+ let playing=false,position=0,last=0,index=-1,raf=0;
32
+ function show(){let low=0,high=frames.length-1,best=-1;while(low<=high){const mid=(low+high)>>1;if(frames[mid].at<=start+position){best=mid;low=mid+1}else high=mid-1}if(best>=0&&best!==index){index=best;image.src='data:image/jpeg;base64,'+frames[index].data}seek.value=String(position);document.getElementById('time').textContent=(position/1000).toFixed(1)+' / '+((end-start)/1000).toFixed(1)+' s';const f=frames[index];const c=clicks.findLast(c=>c.at<=start+position&&start+position-c.at<600&&(!f?.tabId||f.tabId===c.tabId));dot.style.display=c&&f?.width&&f?.height?'block':'none';if(c&&f?.width&&f?.height){dot.style.left=(100*c.x/f.width)+'%';dot.style.top=(100*c.y/f.height)+'%'}}
33
+ play.onclick=()=>{cancelAnimationFrame(raf);playing=!playing;if(position>=end-start)position=0;play.textContent=playing?'Pause':'Play';last=performance.now();if(playing)raf=requestAnimationFrame(tick)};seek.oninput=()=>{position=Number(seek.value);show()};
34
+ function tick(now){if(playing){position=Math.min(end-start,position+(now-last)*Number(document.getElementById('speed').value));show();if(position>=end-start){playing=false;play.textContent='Play'}}last=now;if(playing)raf=requestAnimationFrame(tick)}show();
35
+ </script></html>`;
36
+ }
37
+
38
+ /** Recover complete records; report a partial tail instead of silently claiming a complete recording.
39
+ * @param {string} text @returns {Replay} */
40
+ export function parseRecordingJournal(text) {
41
+ /** @type {Replay} */
42
+ const result = {
43
+ frames: [],
44
+ events: [],
45
+ bodies: [],
46
+ gaps: 0,
47
+ dropped: 0,
48
+ replayMode: 'renderer-stream',
49
+ };
50
+ const lines = text.split('\n');
51
+ /** @type {Map<number, number>} */
52
+ const eventIndexes = new Map();
53
+ let ended = false;
54
+ if (lines.pop()) result.gaps++;
55
+ for (const line of lines) {
56
+ if (!line) continue;
57
+ const entry = /** @type {Record<string, unknown>} */ (JSON.parse(line));
58
+ if (!entry || typeof entry !== 'object') throw new Error('Invalid journal record.');
59
+ if (
60
+ entry.kind === 'frame' &&
61
+ entry.frame &&
62
+ typeof entry.frame === 'object' &&
63
+ 'data' in entry.frame &&
64
+ typeof entry.frame.data === 'string' &&
65
+ 'at' in entry.frame &&
66
+ typeof entry.frame.at === 'number'
67
+ )
68
+ result.frames.push(/** @type {Replay['frames'][number]} */ (entry.frame));
69
+ else if (
70
+ (entry.kind === 'event' || entry.kind === 'event-update') &&
71
+ entry.event &&
72
+ typeof entry.event === 'object'
73
+ ) {
74
+ const event = /** @type {Record<string, unknown>} */ (entry.event);
75
+ const index =
76
+ entry.kind === 'event-update' && typeof event.seq === 'number'
77
+ ? eventIndexes.get(event.seq)
78
+ : undefined;
79
+ if (index !== undefined) result.events[index] = event;
80
+ else {
81
+ if (typeof event.seq === 'number') eventIndexes.set(event.seq, result.events.length);
82
+ result.events.push(event);
83
+ }
84
+ } else if (entry.kind === 'body' && entry.body && typeof entry.body === 'object')
85
+ result.bodies?.push(/** @type {Record<string, unknown>} */ (entry.body));
86
+ else if (entry.kind === 'end') {
87
+ ended = true;
88
+ result.gaps += Number(entry.gaps) || 0;
89
+ result.dropped = Number(entry.dropped) || 0;
90
+ }
91
+ }
92
+ if (!ended) result.gaps++;
93
+ result.events = correlateReplayEvents(result.events);
94
+ return result;
95
+ }
96
+
97
+ /** @template {Record<string, unknown>} T @param {T[]} events @returns {T[]} */
98
+ export function correlateReplayEvents(events) {
99
+ /** @type {Map<unknown, T[]>} */
100
+ const tabs = new Map();
101
+ for (const event of events) {
102
+ if (
103
+ event.origin !== 'trusted-event' ||
104
+ !['click', 'input', 'change', 'submit'].includes(String(event.type)) ||
105
+ !Number.isFinite(event.at)
106
+ )
107
+ continue;
108
+ const list = tabs.get(event.tabId) ?? [];
109
+ list.push(event);
110
+ tabs.set(event.tabId, list);
111
+ }
112
+ for (const list of tabs.values()) list.sort((a, b) => Number(a.at) - Number(b.at));
113
+ return events.map((event) => {
114
+ if (
115
+ !['request', 'page-error', 'navigation'].includes(String(event.type)) ||
116
+ !Number.isFinite(event.at)
117
+ )
118
+ return event;
119
+ const actions = tabs.get(event.tabId) ?? [];
120
+ let low = 0,
121
+ high = actions.length - 1,
122
+ best = -1;
123
+ while (low <= high) {
124
+ const mid = (low + high) >> 1;
125
+ if (Number(actions[mid].at) <= Number(event.at)) {
126
+ best = mid;
127
+ low = mid + 1;
128
+ } else high = mid - 1;
129
+ }
130
+ const action = actions[best];
131
+ const deltaMs = Number(event.at) - Number(action?.at);
132
+ return action && deltaMs <= 2000
133
+ ? {
134
+ ...event,
135
+ correlation: {
136
+ actionSeq: action.seq,
137
+ deltaMs,
138
+ basis: 'same-tab-time-window',
139
+ causality: false,
140
+ },
141
+ }
142
+ : event;
143
+ });
144
+ }
@@ -484,6 +484,55 @@ function summarizeBridgeResponseBody(response, method) {
484
484
  }),
485
485
  };
486
486
  }
487
+ if (method?.startsWith('emulation.')) {
488
+ return {
489
+ ok: true,
490
+ summary: `Emulation ${result.active ? 'active' : 'inactive'}; ${Array.isArray(result.pendingReset) ? result.pendingReset.length : 0} setting(s) retained.`,
491
+ evidence: result,
492
+ };
493
+ }
494
+ if (method === 'input.set_files') {
495
+ return {
496
+ ok: true,
497
+ summary: `File input ${result.assigned ? 'assigned' : 'not verified'}: ${result.count ?? 0} file(s).`,
498
+ evidence: result,
499
+ };
500
+ }
501
+ if (method?.startsWith('files.')) {
502
+ return {
503
+ ok: true,
504
+ summary: result.released
505
+ ? 'Staged file released.'
506
+ : `File staged: ${result.nextOffset ?? 0}/${result.size ?? 0} bytes.`,
507
+ evidence: result,
508
+ };
509
+ }
510
+ if (method === 'download.resource') {
511
+ return { ok: true, summary: 'Resource downloaded to a chunked artifact.', evidence: result };
512
+ }
513
+ if (method?.startsWith('download.')) {
514
+ return {
515
+ ok: true,
516
+ summary: result.released
517
+ ? 'Browser download handle released.'
518
+ : `Browser download ${result.state ?? 'updated'}: ${result.bytesReceived ?? 0}/${result.totalBytes ?? -1} bytes.`,
519
+ evidence: result,
520
+ };
521
+ }
522
+ if (method === 'input.paste_image') {
523
+ return {
524
+ ok: true,
525
+ summary: 'Native image paste dispatched. Verify insertion in the editor.',
526
+ evidence: result,
527
+ };
528
+ }
529
+ if (method?.startsWith('session.')) {
530
+ return {
531
+ ok: true,
532
+ summary: `Recording ${result.sessionId ?? ''}: ${result.released ? 'released' : result.delivery === 'artifact' ? 'exported' : result.active ? 'active' : 'stopped'}; ${result.gaps ?? 0} gap(s).`,
533
+ evidence: result,
534
+ };
535
+ }
487
536
  if (typeof result.daemon === 'string') {
488
537
  const access =
489
538
  result.access && typeof result.access === 'object'
@@ -96,6 +96,28 @@ export type ErrorCode =
96
96
  | 'TIMEOUT';
97
97
 
98
98
  export type BridgeMethod =
99
+ | 'session.start'
100
+ | 'session.journal_read'
101
+ | 'session.journal_delete'
102
+ | 'session.status'
103
+ | 'session.read'
104
+ | 'session.stop'
105
+ | 'session.export'
106
+ | 'session.release'
107
+ | 'files.stage'
108
+ | 'files.release'
109
+ | 'input.set_files'
110
+ | 'input.paste_image'
111
+ | 'download.resource'
112
+ | 'download.start'
113
+ | 'download.status'
114
+ | 'download.pause'
115
+ | 'download.resume'
116
+ | 'download.cancel'
117
+ | 'download.release'
118
+ | 'emulation.apply'
119
+ | 'emulation.status'
120
+ | 'emulation.reset'
99
121
  | 'webmcp.list_tools'
100
122
  | 'webmcp.get_tool'
101
123
  | 'webmcp.execute_tool'
@@ -214,6 +236,8 @@ export interface BridgeMeta {
214
236
  stale_recovery?: 'success' | 'failure';
215
237
  /** Agent session for sticky working-tab routing; unset callers share a default. */
216
238
  agent_session?: string;
239
+ /** Set by the daemon on routed requests; caller values are always overwritten. */
240
+ transport_owner?: string;
217
241
  /** Tab the request ran in. */
218
242
  tab_id?: number;
219
243
  /** How the tab was chosen: explicit tabId, the agent's working tab, or the active tab. */
@@ -1077,7 +1101,7 @@ export type ExtractContentConsistency = 'best_effort' | 'settled';
1077
1101
  export type ExtractContentSource = 'readability' | 'semantic-root' | 'body';
1078
1102
  export type ScreenshotFormat = 'png' | 'jpeg' | 'webp';
1079
1103
  export type ScreenshotDelivery = 'auto' | 'inline' | 'artifact';
1080
- export type ArtifactKind = 'screenshot' | 'har';
1104
+ export type ArtifactKind = 'screenshot' | 'har' | 'resource';
1081
1105
 
1082
1106
  export interface ExtractContentParams {
1083
1107
  format?: ExtractContentFormat;
@@ -1124,6 +1148,18 @@ export interface ArtifactDescriptor<K extends ArtifactKind = ArtifactKind> {
1124
1148
  }
1125
1149
 
1126
1150
  export interface ScreenshotMetadata {
1151
+ context?: {
1152
+ startedAt: string;
1153
+ capturedAt: string;
1154
+ tabId: number;
1155
+ tabActive: boolean | null;
1156
+ windowFocused: boolean | null;
1157
+ visibility: 'visible' | 'hidden' | null;
1158
+ documentFocused: boolean | null;
1159
+ rectSpace: 'viewport-css' | 'document-css';
1160
+ pageClip: { x: number; y: number; width: number; height: number; scale: number };
1161
+ source: 'cdp-renderer';
1162
+ };
1127
1163
  mimeType: `image/${ScreenshotFormat}`;
1128
1164
  format: ScreenshotFormat;
1129
1165
  byteLength: number;
@@ -0,0 +1,22 @@
1
+ import type { BridgeParams } from './types.js';
2
+
3
+ export interface EmulationConfig extends BridgeParams {
4
+ device?: {
5
+ width: number;
6
+ height: number;
7
+ deviceScaleFactor: number;
8
+ mobile: boolean;
9
+ touch: boolean;
10
+ };
11
+ network?: { offline: boolean; latencyMs: number; downloadKbps: number; uploadKbps: number };
12
+ cpuRate?: number;
13
+ locale?: string;
14
+ timezoneId?: string;
15
+ geolocation?: { latitude: number; longitude: number; accuracy: number };
16
+ }
17
+
18
+ export interface CdpControllerDependencies {
19
+ acquire: (tabId: number) => Promise<void>;
20
+ release: (tabId: number) => Promise<void>;
21
+ send: (tabId: number, method: string, params: Record<string, unknown>) => Promise<unknown>;
22
+ }
@@ -87,6 +87,16 @@ have taken effect or finish after dismissal. Inspect postconditions first.
87
87
 
88
88
  ### Interact & Patch
89
89
 
90
+ For device, network, CPU, locale, timezone, or geolocation reproduction, use the [emulation workflow](references/emulation.md). Discover its compact methods with `bbx protocol describe emulation`.
91
+
92
+ For uploads and authenticated downloads, use the [file workflow](references/files.md). `bbx upload` streams local file bytes without putting base64 in model context.
93
+
94
+ For repeatable tasks, record semantic locators and verified outcomes in a [task playbook](references/playbooks.md).
95
+
96
+ Screenshot coordinate spaces and loaded extension build identifiers are described in [capture context](references/capture-context.md).
97
+
98
+ For opt-in activity watch, network bodies/messages, and sampled visual replay, use the [recording workflow](references/recording.md).
99
+
90
100
  ```bash
91
101
  bbx click <ref> [button] # click element
92
102
  bbx focus <ref> # focus element
@@ -0,0 +1,9 @@
1
+ # Capture and runtime identity
2
+
3
+ Screenshot results include `context` with capture timestamps, tab ID, tab activity, window focus, document visibility/focus, and coordinate spaces. `null` means the lookup was unavailable. These flags are sampled before capture and do not prove that the user saw the image.
4
+
5
+ `rectSpace` is `viewport-css` for region requests and `document-css` for element and full-page requests. `pageClip` is the document-space CSS rectangle sent to CDP after adding the visual viewport offset. `dimensions` describes encoded image pixels. Use these fields when relating a screenshot to DOM boxes. A renderer screenshot can capture a background tab.
6
+
7
+ `health.ping` includes `loadedBuild` for the selected extension. `workerId` and `startedAt` identify its running service-worker instance. Packaged releases also include `sourceDigest`, a SHA-256 digest of the sorted staged paths and bytes before the build stamp is generated. Unpackaged checkouts report `null`. The stamp identifies the packaged sources; it is not a cryptographic attestation of the browser or machine.
8
+
9
+ After replacing an extension package, check both `extensionVersion` and `loadedBuild`. A different worker ID confirms a worker restart, while the source digest distinguishes packaged builds with the same version. Re-discover the connection ID after reload.
@@ -0,0 +1,18 @@
1
+ # Environment emulation
2
+
3
+ Use `emulation.apply` to reproduce a device layout or environmental failure. Settings merge per tab and hold a debugger session until reset or detach. `emulation.status` is a compact read and never attaches a debugger.
4
+
5
+ ```bash
6
+ bbx call --tab 123 emulation.apply '{"device":"phone","network":"slow-3g","cpuRate":4}'
7
+ bbx call --tab 123 navigation.reload '{}'
8
+ bbx call --tab 123 emulation.status '{}'
9
+ bbx call --tab 123 emulation.reset '{}'
10
+ ```
11
+
12
+ Device presets are `phone`, `tablet`, and `desktop`. Override metrics with `{"device":{"preset":"phone","width":412}}`. Presets change viewport, DPR, and touch. They do not turn Chrome into Safari.
13
+
14
+ Network presets are `offline`, `slow-3g`, `fast-3g`, `wifi`, and `unthrottled`. Explicit fields are `offline`, `latencyMs`, `downloadKbps`, and `uploadKbps`; throughput `-1` means unthrottled. Reload when testing page loading.
15
+
16
+ Other settings are `locale`, `timezoneId`, `cpuRate`, and `geolocation:{latitude,longitude,accuracy}`. Geolocation overrides do not grant website permissions. Locale changes Intl formatting; they do not rewrite request headers.
17
+
18
+ Reset after the experiment. A partial application can still have changed the tab. Reset attempts every owned setting and reports `failures` and `pendingReset`; retry cleanup if needed. Window-access cleanup also resets emulation. An external debugger detach clears ownership and cancels stale cleanup commands. While device emulation owns viewport metrics, change them through `emulation.apply.device` or reset emulation before calling `viewport.resize`.
@@ -0,0 +1,50 @@
1
+ # Files
2
+
3
+ Keep binary bytes out of model context. For agent-local files, the CLI reads and streams bounded chunks to the selected browser machine, assigns the completed handles once, then releases staging memory:
4
+
5
+ ```bash
6
+ bbx upload --tab 123 --extension CONNECTION 'input[type=file]' ./report.pdf
7
+ bbx upload --remote office --tab 123 '#attachments' ./report.pdf ./chart.png
8
+ ```
9
+
10
+ Files can be at most 16 MiB each and 32 MiB combined. The protocol equivalents are `files.stage`, `input.set_files`, and `files.release`, available through `browser_call`. `files.stage` returns only a handle, size, next offset, completion, and expiry. Handles are bound to the tab, access grant, agent session, and daemon connection. They expire after five minutes and are invalidated by worker restart. Keep multi-call transfers on one connection, such as the CLI upload helper or a persistent MCP client. After an uncertain chunk result, start a fresh transfer; never blindly replay the assignment.
11
+
12
+ For files already on the browser machine, use Chrome's native file-input operation without transferring bytes. It accepts a unique top-document selector and browser-local absolute paths, including files larger than the staging limit:
13
+
14
+ ```bash
15
+ bbx call --tab 123 input.set_files '{"target":{"selector":"input[type=file]"},"paths":["/Users/me/Downloads/report.pdf"]}'
16
+ ```
17
+
18
+ Staged assignment also accepts BBX element refs, including refs obtained inside frames. Empty `files:[]` clears the selection. `assigned:true` means the input accepted the files; wait for the application's upload-complete result before proceeding.
19
+
20
+ Authenticated downloads use the tab's fetch context and BBX's existing chunked artifacts. Browser CORS/CSP rules apply; this needs no additional extension permissions:
21
+
22
+ ```bash
23
+ bbx call --tab 123 download.resource '{"url":"https://example.com/export"}'
24
+ bbx download --tab 123 --extension CONNECTION 'https://example.com/export' ./report.csv
25
+ ```
26
+
27
+ The default size ceiling is 8 MiB and the maximum is 16 MiB. Streaming stops when decoded bytes exceed the limit, even without a Content-Length header. `timeoutMs` defaults to 30 seconds and can be at most 60 seconds. The result contains an artifact descriptor, never file bytes. Retrieve it with `artifact.read` on the same connection, or use `bbx download` to fetch, verify the SHA-256 checksum, and save it in one command. Artifacts are deleted when the owning connection closes. Remote destinations still require an encrypted tunnel, such as SSH, for transport confidentiality.
28
+
29
+ ## Browser-native downloads
30
+
31
+ Use `download.start` for large files or URLs that page fetch cannot read. Chrome streams directly to the browser machine's Downloads directory, without BBX's artifact size limit. This uses the `downloads` permission. Browser authentication, download safety checks, disk limits, and server resume support still apply.
32
+
33
+ ```bash
34
+ bbx call --tab 123 --extension CONNECTION download.start '{"url":"https://example.com/export","filename":"export.zip"}'
35
+ bbx call --tab 123 --extension CONNECTION download.status '{"downloadId":"HANDLE"}'
36
+ bbx call --tab 123 --extension CONNECTION download.pause '{"downloadId":"HANDLE"}'
37
+ bbx call --tab 123 --extension CONNECTION download.resume '{"downloadId":"HANDLE"}'
38
+ bbx call --tab 123 --extension CONNECTION download.cancel '{"downloadId":"HANDLE"}'
39
+ bbx call --tab 123 --extension CONNECTION download.release '{"downloadId":"HANDLE"}'
40
+ ```
41
+
42
+ Treat `downloadId` as a secret capability. It works across CLI invocations in the same tab and access grant, and survives an extension worker restart. BBX exposes no general browser download-history search. `status` returns bytes received/total, state, pause/resume availability, and the browser-local filename. `saveAs:true` opens Chrome's save dialog; the default does not. Filenames must be single relative names, and collisions are uniquified. Release is allowed only after completion/cancellation and never deletes the file. Access revocation or tab closure cancels retained downloads. Completed handles expire after 24 hours when a new download prunes them. At most 32 handles are retained.
43
+
44
+ ## Trusted image paste
45
+
46
+ ```bash
47
+ bbx paste-image --tab 123 --extension CONNECTION '[contenteditable=true]' ./chart.png
48
+ ```
49
+
50
+ The helper stages one PNG, focuses the editor, writes the image to the real system clipboard, dispatches Chrome's native paste command, and releases staging memory. MCP can use `files.stage`, `input.paste_image` with `target` and `transferId`, then `files.release` on one connection. The browser window and tab must already be focused, and the target must be an editable control in the top document. BBX revalidates focus before clipboard writing and before paste. No clipboard-read or extra clipboard permission is requested. This overwrites the clipboard. Inspect insertion before submitting; never automatically replay after uncertainty or focus loss.