@phnx-labs/agents-cli 1.22.92 → 1.22.94

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 (55) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -0
  3. package/dist/commands/browser.d.ts +1 -0
  4. package/dist/commands/browser.js +145 -11
  5. package/dist/lib/browser/arc-discovery.d.ts +60 -0
  6. package/dist/lib/browser/arc-discovery.js +187 -0
  7. package/dist/lib/browser/arc-dom.d.ts +14 -0
  8. package/dist/lib/browser/arc-dom.js +121 -0
  9. package/dist/lib/browser/drivers/arc.d.ts +57 -0
  10. package/dist/lib/browser/drivers/arc.js +281 -0
  11. package/dist/lib/browser/profiles.d.ts +5 -1
  12. package/dist/lib/browser/profiles.js +94 -9
  13. package/dist/lib/browser/refs.d.ts +2 -0
  14. package/dist/lib/browser/refs.js +2 -1
  15. package/dist/lib/browser/resolve-target.d.ts +2 -0
  16. package/dist/lib/browser/resolve-target.js +8 -0
  17. package/dist/lib/browser/runtime-state.js +9 -1
  18. package/dist/lib/browser/service.d.ts +37 -3
  19. package/dist/lib/browser/service.js +571 -41
  20. package/dist/lib/browser/types.d.ts +55 -1
  21. package/dist/lib/claude-account-token.d.ts +13 -0
  22. package/dist/lib/claude-account-token.js +29 -0
  23. package/dist/lib/daemon/attention-notify-service.d.ts +5 -2
  24. package/dist/lib/daemon/attention-notify-service.js +9 -2
  25. package/dist/lib/exec.js +11 -0
  26. package/dist/lib/feed/answer.js +9 -0
  27. package/dist/lib/feed/attention.d.ts +18 -2
  28. package/dist/lib/feed/attention.js +0 -0
  29. package/dist/lib/feed/feed.d.ts +6 -2
  30. package/dist/lib/feed/feed.js +10 -4
  31. package/dist/lib/harness/adapter.d.ts +23 -0
  32. package/dist/lib/harness/adapter.js +29 -0
  33. package/dist/lib/harness/adapters/claude.js +5 -3
  34. package/dist/lib/harness/adapters/copilot.js +4 -2
  35. package/dist/lib/harness/adapters/grok.js +4 -2
  36. package/dist/lib/harness/adapters/kimi.js +4 -2
  37. package/dist/lib/harness/adapters/muse.js +4 -3
  38. package/dist/lib/harness/adapters/opencode.js +4 -2
  39. package/dist/lib/helper-download.js +1 -1
  40. package/dist/lib/helper-versions.js +1 -1
  41. package/dist/lib/installations/shims.d.ts +2 -2
  42. package/dist/lib/installations/shims.js +16 -8
  43. package/dist/lib/menubar/download-menubar.js +4 -1
  44. package/dist/lib/menubar/install-menubar.d.ts +10 -9
  45. package/dist/lib/menubar/install-menubar.js +17 -15
  46. package/dist/lib/secrets-policy.d.ts +5 -2
  47. package/dist/lib/secrets-policy.js +18 -4
  48. package/dist/lib/session/active.d.ts +18 -11
  49. package/dist/lib/session/active.js +52 -44
  50. package/dist/lib/session/session-cache.d.ts +1 -1
  51. package/dist/lib/session/session-cache.js +1 -0
  52. package/dist/lib/session/state.d.ts +26 -0
  53. package/dist/lib/session/state.js +43 -27
  54. package/dist/lib/types.d.ts +7 -0
  55. package/package.json +1 -1
@@ -16,12 +16,14 @@ import { taskMatchesCaller } from './caller-identity.js';
16
16
  import { getRefs, resolveRefToCoords, describeRefs, healRef } from './refs.js';
17
17
  import { clickAtCoords, hoverAtCoords, scrollAtCoords, typeText, pressKey, focusNode } from './input.js';
18
18
  import { typeEditorText } from './editor.js';
19
+ import { arcClickExpression, arcFillExpression, arcRefsExpression, arcScrollExpression, parseArcRefsResult, } from './arc-dom.js';
19
20
  import { detectUploadPattern, stageUploadFile, uploadToDropTarget, uploadToFileInput, uploadViaFileChooser, } from './upload.js';
20
21
  import { emit } from '../feed/events.js';
21
22
  import { resolveActor } from '../actor.js';
22
23
  import { recordBrowserSession } from '../session/db.js';
23
24
  import { sshExecAsync } from '../ssh-exec.js';
24
25
  import { resolveFfmpeg } from './ffmpeg.js';
26
+ import { ArcNativeCapabilityError, executeJavaScript, navigateArcTab, closeArcTab, createArcTab, enumerateArcSpaces, isArcRunning, resolveArcTab, restoreArcSelection, selectWindowTab, selectArcTab, } from './drivers/arc.js';
25
27
  /**
26
28
  * Canonical form for comparing a requested URL against what CDP reports for a
27
29
  * live target. `new URL('https://example.com').href` is `https://example.com/`,
@@ -229,6 +231,10 @@ function expandHome(p) {
229
231
  * can make; if it doesn't round-trip within 1s the connection is dead.
230
232
  */
231
233
  async function isConnHealthy(conn, timeoutMs = 1000) {
234
+ // Native Arc connections: healthy if Arc is still running (no CDP socket to probe).
235
+ if (conn.backend === 'arc-native') {
236
+ return isArcRunning();
237
+ }
232
238
  if (!conn.cdp.isOpen)
233
239
  return false;
234
240
  try {
@@ -242,6 +248,10 @@ async function isConnHealthy(conn, timeoutMs = 1000) {
242
248
  return false;
243
249
  }
244
250
  }
251
+ function requireCdp(conn, capability) {
252
+ if (conn.backend === 'arc-native')
253
+ throw new ArcNativeCapabilityError(capability);
254
+ }
245
255
  /** Join error lines so callers get a next command, not a dead-end message. */
246
256
  export function actionable(...lines) {
247
257
  return lines.filter((l) => l != null && l !== '').join('\n');
@@ -294,6 +304,20 @@ export function resolveTaskIdentity(forwarded, resolveLocalActor) {
294
304
  sessionId: forwarded.sessionId,
295
305
  };
296
306
  }
307
+ /**
308
+ * The unique URL a native Arc tab is created at before it is navigated to its
309
+ * real target (PHNX-2399). Arc's `make new tab` accepts only http(s) URLs (it
310
+ * rejects `data:` and `about:` outright), so the marker is an https URL on the
311
+ * reserved `.invalid` TLD: DNS fails locally, no request leaves the machine,
312
+ * and the tab keeps the exact URL for the enumerate-and-match that follows.
313
+ */
314
+ const ARC_CREATE_MARKER_PREFIX = 'https://agents-browser.invalid/';
315
+ export function arcCreateMarker() {
316
+ return `${ARC_CREATE_MARKER_PREFIX}${crypto.randomUUID()}`;
317
+ }
318
+ export function isArcCreateMarker(url) {
319
+ return url.startsWith(ARC_CREATE_MARKER_PREFIX);
320
+ }
297
321
  export class BrowserService {
298
322
  static SOURCE_PREFIX = {
299
323
  'rush-app': 'rush-app-',
@@ -435,6 +459,9 @@ export class BrowserService {
435
459
  endpointName: opts.endpointName,
436
460
  probe: opts.probe,
437
461
  });
462
+ if (!routed.local && routed.commandDispatch) {
463
+ throw new Error(`Native Arc profile "${profileName}" is owned by ${routed.device}; dispatch the complete browser command there.`);
464
+ }
438
465
  const composite = routed.key;
439
466
  const effectiveProfile = routed.profile;
440
467
  const taskId = generateTaskId();
@@ -509,7 +536,7 @@ export class BrowserService {
509
536
  // pile-up in RUSH-2622. Tabs the daemon did NOT open are still left alone —
510
537
  // the branch only fires when the profile has no page target at all.
511
538
  let startupBlankTargetId;
512
- if (!opts.url && !conn.electron) {
539
+ if (!opts.url && !conn.electron && conn.backend !== 'arc-native') {
513
540
  const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
514
541
  if (!targetInfos.some((t) => t.type === 'page')) {
515
542
  const created = await this.createPageTarget(conn, {
@@ -538,13 +565,23 @@ export class BrowserService {
538
565
  // actor (the RUSH-2020 bug).
539
566
  ...resolveTaskIdentity({ actor: opts.actor, launchId: opts.launchId, sessionId: opts.sessionId }, () => resolveActor().id),
540
567
  };
568
+ if (conn.backend === 'arc-native') {
569
+ const selected = await this.resolveArcSpace(conn.arcProfile);
570
+ task.arcNative = {
571
+ profileId: conn.arcProfile.profileId,
572
+ windowId: selected.windowId,
573
+ spaceId: selected.spaceId,
574
+ spaceTitle: selected.spaceTitle,
575
+ tabs: {},
576
+ };
577
+ }
541
578
  if (startupBlankTargetId) {
542
579
  const shortId = generateShortId();
543
580
  task.tabs[shortId] = startupBlankTargetId;
544
581
  task.currentTabId = shortId;
545
582
  }
546
583
  // For Electron, get the existing window as the tab
547
- if (conn.electron) {
584
+ if (conn.electron && conn.backend !== 'arc-native') {
548
585
  const windowId = await this.getOrCreateWindow(conn);
549
586
  if (windowId) {
550
587
  const shortId = generateShortId();
@@ -596,7 +633,7 @@ export class BrowserService {
596
633
  // opening a fresh target is created:true.
597
634
  let tabId;
598
635
  let firstOpen;
599
- if (opts.url && !conn.electron && conn.browserType !== 'arc') {
636
+ if (opts.url && conn.backend !== 'arc-native' && !conn.electron && conn.browserType !== 'arc') {
600
637
  const adopted = opts.fresh ? undefined : await this.adoptTabShowing(conn, opts.url);
601
638
  const targetId = adopted ?? (await this.createPageTarget(conn, { url: opts.url })).targetId;
602
639
  const shortId = generateShortId();
@@ -607,7 +644,7 @@ export class BrowserService {
607
644
  tabId = shortId;
608
645
  firstOpen = { tabId: shortId, created: !adopted, refreshed: false };
609
646
  }
610
- else if (opts.url && (conn.electron || conn.browserType === 'arc')) {
647
+ else if (opts.url && (conn.backend === 'arc-native' || conn.electron || conn.browserType === 'arc')) {
611
648
  // Electron and Arc share the reuse-in-place path: neither may open a fresh
612
649
  // tab here (Electron drives its one window; Arc crashes on Target.createTarget),
613
650
  // so the implicit first navigate attaches to an existing tab — honoring the
@@ -830,36 +867,55 @@ export class BrowserService {
830
867
  const mapKey = this.taskMapKey(conn, task) ?? taskName;
831
868
  // Get domains from tabs before closing (for history)
832
869
  const domains = new Set();
833
- try {
834
- const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
835
- for (const cdpId of Object.values(task.tabs)) {
836
- const target = targetInfos.find((t) => t.targetId === cdpId);
837
- if (target?.url) {
870
+ if (conn.backend === 'arc-native') {
871
+ // Resolve exact durable refs for history. Missing/moved tabs are not
872
+ // adopted and never become candidates for a best-effort close.
873
+ for (const ref of Object.values(this.requireArcTask(task).tabs)) {
874
+ const live = await resolveArcTab(ref);
875
+ if (live?.url) {
838
876
  try {
839
- const domain = new URL(target.url).hostname.replace(/^www\./, '');
877
+ const domain = new URL(live.url).hostname.replace(/^www\./, '');
840
878
  if (domain && domain !== 'blank')
841
879
  domains.add(domain);
842
880
  }
843
- catch {
844
- // invalid URL
845
- }
881
+ catch { /* invalid URL */ }
846
882
  }
847
883
  }
884
+ await this.saveToHistory(task, Array.from(domains));
885
+ await this.closeArcNativeTabs(conn, task);
848
886
  }
849
- catch {
850
- // CDP not responding
851
- }
852
- // Save to history before closing
853
- await this.saveToHistory(task, Array.from(domains));
854
- // Close task's tabs (not the window - it's shared, and not a tab the
855
- // task merely borrowed - that one was open before the task existed).
856
- await Promise.all(this.closeableTargetIds(task).map((cdpId) => conn.cdp.send('Target.closeTarget', { targetId: cdpId }).catch(() => {
857
- // Tab already closed
858
- })));
859
- for (const cdpId of Object.values(task.tabs)) {
860
- conn.sessionCache.delete(cdpId);
887
+ else {
888
+ try {
889
+ const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
890
+ for (const cdpId of Object.values(task.tabs)) {
891
+ const target = targetInfos.find((t) => t.targetId === cdpId);
892
+ if (target?.url) {
893
+ try {
894
+ const domain = new URL(target.url).hostname.replace(/^www\./, '');
895
+ if (domain && domain !== 'blank')
896
+ domains.add(domain);
897
+ }
898
+ catch {
899
+ // invalid URL
900
+ }
901
+ }
902
+ }
903
+ }
904
+ catch {
905
+ // CDP not responding
906
+ }
907
+ // Save to history before closing
908
+ await this.saveToHistory(task, Array.from(domains));
909
+ // Close task's tabs (not the window - it's shared, and not a tab the
910
+ // task merely borrowed - that one was open before the task existed).
911
+ await Promise.all(this.closeableTargetIds(task).map((cdpId) => conn.cdp.send('Target.closeTarget', { targetId: cdpId }).catch(() => {
912
+ // Tab already closed
913
+ })));
914
+ for (const cdpId of Object.values(task.tabs)) {
915
+ conn.sessionCache.delete(cdpId);
916
+ }
917
+ this.invalidateTargetCache(conn);
861
918
  }
862
- this.invalidateTargetCache(conn);
863
919
  conn.tasks.delete(mapKey);
864
920
  await this.saveTaskState(key, conn.tasks);
865
921
  // Feed + usage rows are read by humans: bare profile name (RUSH-2709).
@@ -875,8 +931,10 @@ export class BrowserService {
875
931
  });
876
932
  }).catch(() => { });
877
933
  if (conn.forkedFrom && conn.tasks.size === 0) {
878
- conn.cdp.close();
879
- killChrome(conn.pid);
934
+ if (conn.backend !== 'arc-native') {
935
+ conn.cdp.close();
936
+ killChrome(conn.pid);
937
+ }
880
938
  conn.cleanup?.();
881
939
  this.connections.delete(key);
882
940
  clearProfileRuntime(key);
@@ -908,6 +966,16 @@ export class BrowserService {
908
966
  const conn = this.connections.get(key);
909
967
  if (!conn)
910
968
  continue;
969
+ // Native Arc connections: close owned tabs but NEVER kill the Arc process.
970
+ // The CLI does not own the Arc process — the user started it.
971
+ if (conn.backend === 'arc-native') {
972
+ for (const task of conn.tasks.values()) {
973
+ await this.closeArcNativeTabs(conn, task);
974
+ }
975
+ this.connections.delete(key);
976
+ clearProfileRuntime(key);
977
+ continue;
978
+ }
911
979
  conn.cdp.close();
912
980
  killChrome(conn.pid);
913
981
  conn.cleanup?.();
@@ -934,6 +1002,11 @@ export class BrowserService {
934
1002
  }
935
1003
  async navigateLocked(conn, task, url) {
936
1004
  this.maybeUpdateLabelFromUrl(conn, task, url);
1005
+ // Native Arc backend (PHNX-2399): route through the native driver instead of CDP.
1006
+ if (conn.backend === 'arc-native') {
1007
+ return this.navigateArcNative(conn, task, url);
1008
+ }
1009
+ requireCdp(conn, 'navigate');
937
1010
  // Same-task reopen (PHNX-2399): the URL is already live in a tab this task
938
1011
  // owns → reload THAT tab in place and keep its id, before the create/reuse
939
1012
  // paths below. Chrome and Arc alike; Electron drives one window and never
@@ -1035,6 +1108,10 @@ export class BrowserService {
1035
1108
  return this.runExclusive(`task:${task.profile}:${task.name}`, () => this.tabAddLocked(conn, task, url));
1036
1109
  }
1037
1110
  async tabAddLocked(conn, task, url) {
1111
+ if (conn.backend === 'arc-native') {
1112
+ return this.navigateArcNative(conn, task, url, true);
1113
+ }
1114
+ requireCdp(conn, 'createTab');
1038
1115
  if (conn.electron) {
1039
1116
  throw new Error('Electron apps do not support opening additional tabs');
1040
1117
  }
@@ -1057,12 +1134,27 @@ export class BrowserService {
1057
1134
  async tabFocus(taskId, tabHint) {
1058
1135
  const { conn, task } = await this.findTask(taskId);
1059
1136
  const resolvedTabId = await this.resolveTabHint(conn, task, tabHint);
1137
+ if (conn.backend === 'arc-native') {
1138
+ await this.runExclusive('arc-native:host-app', async () => {
1139
+ const ref = this.requireArcTask(task).tabs[resolvedTabId];
1140
+ if (!ref)
1141
+ throw new Error(`Tab ${resolvedTabId} not found`);
1142
+ await selectArcTab(ref);
1143
+ task.currentTabId = resolvedTabId;
1144
+ await this.saveTaskState(task.profile, conn.tasks);
1145
+ });
1146
+ return { tabId: resolvedTabId };
1147
+ }
1060
1148
  task.currentTabId = resolvedTabId;
1061
1149
  await this.saveTaskState(task.profile, conn.tasks);
1062
1150
  return { tabId: resolvedTabId };
1063
1151
  }
1064
1152
  async tabList(taskId) {
1065
1153
  const { conn, task } = await this.findTask(taskId);
1154
+ if (conn.backend === 'arc-native') {
1155
+ return (await this.listArcTaskTabs(task)).map((tab) => ({ ...tab, current: tab.current === true }));
1156
+ }
1157
+ requireCdp(conn, 'enumerate');
1066
1158
  const targets = (await conn.cdp.send('Target.getTargets'));
1067
1159
  const tabs = [];
1068
1160
  for (const [shortId, cdpId] of Object.entries(task.tabs)) {
@@ -1089,6 +1181,10 @@ export class BrowserService {
1089
1181
  if (byPrefix.length > 1) {
1090
1182
  throw new Error(`Ambiguous tab hint "${hint}" — matches ${byPrefix.length} tabs`);
1091
1183
  }
1184
+ // Native Arc addresses owned tabs only by the task's stable short id. URL
1185
+ // and title are display data and must never become fallback identities.
1186
+ if (conn.backend === 'arc-native')
1187
+ throw new Error(`Tab "${hint}" not found`);
1092
1188
  // URL substring match
1093
1189
  const targets = (await conn.cdp.send('Target.getTargets'));
1094
1190
  const matches = [];
@@ -1121,12 +1217,16 @@ export class BrowserService {
1121
1217
  async tabs(taskId, profileRef) {
1122
1218
  if (taskId) {
1123
1219
  const { conn, task } = await this.findTask(taskId, profileRef);
1220
+ if (conn.backend === 'arc-native')
1221
+ return this.listArcTaskTabs(task);
1124
1222
  return this.getTabsForTask(conn.cdp, task);
1125
1223
  }
1126
1224
  const allTabs = [];
1127
1225
  for (const [, conn] of this.connections) {
1128
1226
  for (const [, task] of conn.tasks) {
1129
- const tabs = await this.getTabsForTask(conn.cdp, task);
1227
+ const tabs = conn.backend === 'arc-native'
1228
+ ? await this.listArcTaskTabs(task)
1229
+ : await this.getTabsForTask(conn.cdp, task);
1130
1230
  allTabs.push(...tabs);
1131
1231
  }
1132
1232
  }
@@ -1134,6 +1234,30 @@ export class BrowserService {
1134
1234
  }
1135
1235
  async tabClose(taskId, tabHint) {
1136
1236
  const { conn, task } = await this.findTask(taskId);
1237
+ if (conn.backend === 'arc-native') {
1238
+ await this.runExclusive('arc-native:host-app', async () => {
1239
+ await this.reconcileArcCreateIntents(conn, task);
1240
+ const native = this.requireArcTask(task);
1241
+ const ids = tabHint === undefined
1242
+ ? Object.keys(native.tabs)
1243
+ : [await this.resolveTabHint(conn, task, tabHint)];
1244
+ for (const shortId of ids) {
1245
+ const ref = native.tabs[shortId];
1246
+ if (ref)
1247
+ await closeArcTab(ref);
1248
+ delete native.tabs[shortId];
1249
+ delete task.tabs[shortId];
1250
+ delete task.refDescriptors?.[shortId];
1251
+ }
1252
+ if (!task.currentTabId || ids.includes(task.currentTabId)) {
1253
+ const remaining = Object.keys(native.tabs);
1254
+ task.currentTabId = remaining.at(-1);
1255
+ }
1256
+ await this.saveTaskState(task.profile, conn.tasks);
1257
+ });
1258
+ return;
1259
+ }
1260
+ requireCdp(conn, 'closeTab');
1137
1261
  if (tabHint !== undefined) {
1138
1262
  const shortId = await this.resolveTabHint(conn, task, tabHint);
1139
1263
  const cdpId = task.tabs[shortId];
@@ -1170,6 +1294,10 @@ export class BrowserService {
1170
1294
  async evaluate(taskId, tabHint, expression) {
1171
1295
  const { conn, task } = await this.findTask(taskId);
1172
1296
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1297
+ // Native Arc backend (PHNX-2399): synchronous JS only, isolated world.
1298
+ if (conn.backend === 'arc-native') {
1299
+ return this.evaluateArcNative(conn, task, shortId, expression);
1300
+ }
1173
1301
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1174
1302
  const target = await this.getTarget(conn, cdpTargetId);
1175
1303
  if (!target) {
@@ -1199,6 +1327,13 @@ export class BrowserService {
1199
1327
  }
1200
1328
  async screenshot(taskId, tabHint, outputPath, quality = 'compressed') {
1201
1329
  const { conn, task, key: runtimeKey } = await this.findTask(taskId);
1330
+ // Native Arc backend: screenshots cannot select/activate the user's tab.
1331
+ if (conn.backend === 'arc-native') {
1332
+ throw new ArcNativeCapabilityError('screenshot', 'Screenshot is unavailable for native Arc automation. ' +
1333
+ 'Screenshots require activating the tab, which would disrupt user selection. ' +
1334
+ 'This task still belongs to the Arc profile. ' +
1335
+ 'Use a Chromium-family browser for screenshot capability.');
1336
+ }
1202
1337
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1203
1338
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1204
1339
  const target = await this.getTarget(conn, cdpTargetId);
@@ -1259,6 +1394,7 @@ export class BrowserService {
1259
1394
  */
1260
1395
  async printToPdf(taskId, tabHint, outputPath) {
1261
1396
  const { conn, task, key: runtimeKey } = await this.findTask(taskId);
1397
+ requireCdp(conn, 'pdf');
1262
1398
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1263
1399
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1264
1400
  const target = await this.getTarget(conn, cdpTargetId);
@@ -1287,6 +1423,7 @@ export class BrowserService {
1287
1423
  throw new Error(`Task "${taskId}" is already recording. Call record stop first.`);
1288
1424
  }
1289
1425
  const { conn, task, key: runtimeKey } = await this.findTask(taskId);
1426
+ requireCdp(conn, 'screenshot');
1290
1427
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1291
1428
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1292
1429
  const target = await this.getTarget(conn, cdpTargetId);
@@ -1488,6 +1625,9 @@ export class BrowserService {
1488
1625
  return { path: outputPath, fps, durationCapSec: durationSec, maxMb };
1489
1626
  }
1490
1627
  async recordStop(taskId, reason = 'manual') {
1628
+ const activeTask = await this.findTask(taskId).catch(() => undefined);
1629
+ if (activeTask)
1630
+ requireCdp(activeTask.conn, 'screenshot');
1491
1631
  const rec = this.recordings.get(taskId);
1492
1632
  if (!rec) {
1493
1633
  throw new Error(`Task "${taskId}" is not currently recording`);
@@ -1570,6 +1710,13 @@ export class BrowserService {
1570
1710
  return { path: rec.outputPath, bytes, durationMs, reason };
1571
1711
  }
1572
1712
  async recordStatus(taskId) {
1713
+ // The idle reaper calls this while deciding whether a task is stale. Do
1714
+ // not route an already-live task through findTask(), which stamps it as
1715
+ // active and defeats idle detection. A cold daemon may still rehydrate for
1716
+ // an explicit user query.
1717
+ const live = this.findTaskByHandle(taskId) ?? await this.findTask(taskId).catch(() => undefined);
1718
+ if (live)
1719
+ requireCdp(live.conn, 'screenshot');
1573
1720
  const rec = this.recordings.get(taskId);
1574
1721
  if (!rec)
1575
1722
  return { recording: false };
@@ -1578,6 +1725,13 @@ export class BrowserService {
1578
1725
  async refs(taskId, tabHint, opts = {}) {
1579
1726
  const { conn, task } = await this.findTask(taskId);
1580
1727
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1728
+ if (conn.backend === 'arc-native') {
1729
+ const result = parseArcRefsResult(await this.evaluateArcNative(conn, task, shortId, arcRefsExpression(opts)), opts);
1730
+ this.cacheRefDescriptors(task, shortId, result.nodeMap, result.opts);
1731
+ await this.saveTaskState(task.profile, conn.tasks);
1732
+ return result;
1733
+ }
1734
+ requireCdp(conn, 'enumerate');
1581
1735
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1582
1736
  const target = await this.getTarget(conn, cdpTargetId);
1583
1737
  if (!target)
@@ -1614,7 +1768,7 @@ export class BrowserService {
1614
1768
  const stillMatches = fresh !== undefined &&
1615
1769
  fresh.role === cached.role &&
1616
1770
  fresh.name === cached.name &&
1617
- fresh.backendNodeId !== undefined;
1771
+ (fresh.backendNodeId !== undefined || fresh.selector !== undefined);
1618
1772
  if (stillMatches)
1619
1773
  return { targetRef: ref };
1620
1774
  const newRef = healRef(cached, nodeMap);
@@ -1632,6 +1786,18 @@ export class BrowserService {
1632
1786
  async click(taskId, ref, tabHint) {
1633
1787
  const { conn, task } = await this.findTask(taskId);
1634
1788
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1789
+ if (conn.backend === 'arc-native') {
1790
+ const snapshot = task.refDescriptors?.[shortId];
1791
+ const buildOpts = snapshot?.opts ?? { interactive: true, limit: 500 };
1792
+ const { nodeMap } = parseArcRefsResult(await this.evaluateArcNative(conn, task, shortId, arcRefsExpression(buildOpts)), buildOpts);
1793
+ const { targetRef, healed } = this.resolveHealedRef(snapshot, nodeMap, ref);
1794
+ const selector = nodeMap.get(targetRef)?.selector;
1795
+ if (!selector)
1796
+ throw new Error(`Ref ${ref} has no DOM selector`);
1797
+ await this.evaluateArcNative(conn, task, shortId, arcClickExpression(selector));
1798
+ return healed ? { healed } : {};
1799
+ }
1800
+ requireCdp(conn, 'click');
1635
1801
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1636
1802
  const target = await this.getTarget(conn, cdpTargetId);
1637
1803
  if (!target)
@@ -1663,6 +1829,7 @@ export class BrowserService {
1663
1829
  */
1664
1830
  async clickAt(taskId, x, y, tabHint) {
1665
1831
  const { conn, task } = await this.findTask(taskId);
1832
+ requireCdp(conn, 'trustedInput');
1666
1833
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1667
1834
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1668
1835
  const target = await this.getTarget(conn, cdpTargetId);
@@ -1674,6 +1841,18 @@ export class BrowserService {
1674
1841
  async type(taskId, ref, text, tabHint, clear) {
1675
1842
  const { conn, task } = await this.findTask(taskId);
1676
1843
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1844
+ if (conn.backend === 'arc-native') {
1845
+ const snapshot = task.refDescriptors?.[shortId];
1846
+ const buildOpts = snapshot?.opts ?? { interactive: true, limit: 500 };
1847
+ const { nodeMap } = parseArcRefsResult(await this.evaluateArcNative(conn, task, shortId, arcRefsExpression(buildOpts)), buildOpts);
1848
+ const { targetRef } = this.resolveHealedRef(snapshot, nodeMap, ref);
1849
+ const selector = nodeMap.get(targetRef)?.selector;
1850
+ if (!selector)
1851
+ throw new Error(`Ref ${ref} has no DOM selector`);
1852
+ await this.evaluateArcNative(conn, task, shortId, arcFillExpression(selector, text, clear ?? false));
1853
+ return;
1854
+ }
1855
+ requireCdp(conn, 'type');
1677
1856
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1678
1857
  const target = await this.getTarget(conn, cdpTargetId);
1679
1858
  if (!target)
@@ -1701,6 +1880,7 @@ export class BrowserService {
1701
1880
  }
1702
1881
  async press(taskId, key, tabHint) {
1703
1882
  const { conn, task } = await this.findTask(taskId);
1883
+ requireCdp(conn, 'trustedInput');
1704
1884
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1705
1885
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1706
1886
  const target = await this.getTarget(conn, cdpTargetId);
@@ -1711,6 +1891,7 @@ export class BrowserService {
1711
1891
  }
1712
1892
  async hover(taskId, ref, tabHint) {
1713
1893
  const { conn, task } = await this.findTask(taskId);
1894
+ requireCdp(conn, 'trustedInput');
1714
1895
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1715
1896
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1716
1897
  const target = await this.getTarget(conn, cdpTargetId);
@@ -1724,6 +1905,13 @@ export class BrowserService {
1724
1905
  async scroll(taskId, deltaX, deltaY, atX, atY, tabHint) {
1725
1906
  const { conn, task } = await this.findTask(taskId);
1726
1907
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
1908
+ if (conn.backend === 'arc-native') {
1909
+ if (atX !== undefined || atY !== undefined)
1910
+ throw new ArcNativeCapabilityError('trustedInput');
1911
+ await this.evaluateArcNative(conn, task, shortId, arcScrollExpression(deltaX, deltaY));
1912
+ return;
1913
+ }
1914
+ requireCdp(conn, 'scroll');
1727
1915
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1728
1916
  const target = await this.getTarget(conn, cdpTargetId);
1729
1917
  if (!target)
@@ -1733,6 +1921,7 @@ export class BrowserService {
1733
1921
  }
1734
1922
  async upload(taskId, files, options) {
1735
1923
  const { conn, task } = await this.findTask(taskId);
1924
+ requireCdp(conn, 'upload');
1736
1925
  const shortId = options.tabHint
1737
1926
  ? await this.resolveTabHint(conn, task, options.tabHint)
1738
1927
  : this.resolveCurrentTab(task);
@@ -1883,6 +2072,7 @@ export class BrowserService {
1883
2072
  // ─── Viewport & Device Emulation ──────────────────────────────────────────────
1884
2073
  async setViewport(taskId, width, height, options = {}) {
1885
2074
  const { conn, task } = await this.findTask(taskId);
2075
+ requireCdp(conn, 'viewport');
1886
2076
  const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
1887
2077
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1888
2078
  const target = await this.getTarget(conn, cdpTargetId);
@@ -1963,6 +2153,7 @@ export class BrowserService {
1963
2153
  }
1964
2154
  async getConsoleLogs(taskId, options = {}) {
1965
2155
  const { conn, task } = await this.findTask(taskId);
2156
+ requireCdp(conn, 'consoleCapture');
1966
2157
  const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
1967
2158
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1968
2159
  const target = await this.getTarget(conn, cdpTargetId);
@@ -1981,6 +2172,7 @@ export class BrowserService {
1981
2172
  }
1982
2173
  async getErrors(taskId, options = {}) {
1983
2174
  const { conn, task } = await this.findTask(taskId);
2175
+ requireCdp(conn, 'consoleCapture');
1984
2176
  const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
1985
2177
  const cdpTargetId = this.getCdpTargetId(task, shortId);
1986
2178
  const target = await this.getTarget(conn, cdpTargetId);
@@ -2033,6 +2225,7 @@ export class BrowserService {
2033
2225
  }
2034
2226
  async getNetworkRequests(taskId, options = {}) {
2035
2227
  const { conn, task } = await this.findTask(taskId);
2228
+ requireCdp(conn, 'networkCapture');
2036
2229
  const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
2037
2230
  const cdpTargetId = this.getCdpTargetId(task, shortId);
2038
2231
  const target = await this.getTarget(conn, cdpTargetId);
@@ -2052,6 +2245,7 @@ export class BrowserService {
2052
2245
  }
2053
2246
  async getResponseBody(taskId, urlPattern, options = {}) {
2054
2247
  const { conn, task } = await this.findTask(taskId);
2248
+ requireCdp(conn, 'networkCapture');
2055
2249
  const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
2056
2250
  const cdpTargetId = this.getCdpTargetId(task, shortId);
2057
2251
  const target = await this.getTarget(conn, cdpTargetId);
@@ -2125,11 +2319,12 @@ export class BrowserService {
2125
2319
  // ─── Wait Conditions ─────────────────────────────────────────────────────────
2126
2320
  async wait(taskId, type, value, options = {}) {
2127
2321
  const timeout = options.timeout ?? 30000;
2322
+ const { conn, task } = await this.findTask(taskId);
2323
+ requireCdp(conn, 'asyncEvaluate');
2128
2324
  if (type === 'time') {
2129
2325
  await new Promise((r) => setTimeout(r, typeof value === 'number' ? value : parseInt(value, 10)));
2130
2326
  return;
2131
2327
  }
2132
- const { conn, task } = await this.findTask(taskId);
2133
2328
  const shortId = options.tabHint ? await this.resolveTabHint(conn, task, options.tabHint) : this.resolveCurrentTab(task);
2134
2329
  const cdpTargetId = this.getCdpTargetId(task, shortId);
2135
2330
  const target = await this.getTarget(conn, cdpTargetId);
@@ -2202,6 +2397,7 @@ export class BrowserService {
2202
2397
  }
2203
2398
  async setDownloadPath(taskId, downloadPath, tabHint) {
2204
2399
  const { conn, task, key: runtimeKey } = await this.findTask(taskId);
2400
+ requireCdp(conn, 'download');
2205
2401
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
2206
2402
  const cdpTargetId = this.getCdpTargetId(task, shortId);
2207
2403
  const target = await this.getTarget(conn, cdpTargetId);
@@ -2230,6 +2426,8 @@ export class BrowserService {
2230
2426
  return resolvedPath;
2231
2427
  }
2232
2428
  async waitForDownload(taskId, timeout = 60000) {
2429
+ const { conn } = await this.findTask(taskId);
2430
+ requireCdp(conn, 'download');
2233
2431
  const start = Date.now();
2234
2432
  while (Date.now() - start < timeout) {
2235
2433
  const dl = this.pendingDownloads.get(taskId);
@@ -2284,7 +2482,8 @@ export class BrowserService {
2284
2482
  this.recordings.delete(taskId);
2285
2483
  }
2286
2484
  for (const [, conn] of this.connections) {
2287
- conn.cdp.close();
2485
+ if (conn.backend !== 'arc-native')
2486
+ conn.cdp.close();
2288
2487
  conn.cleanup?.();
2289
2488
  }
2290
2489
  this.connections.clear();
@@ -2310,6 +2509,7 @@ export class BrowserService {
2310
2509
  await cdp.connect(wsUrl);
2311
2510
  await this.enableDomains(cdp);
2312
2511
  const connection = {
2512
+ backend: 'cdp',
2313
2513
  cdp,
2314
2514
  port,
2315
2515
  pid,
@@ -2357,6 +2557,7 @@ export class BrowserService {
2357
2557
  await this.enableDomains(cdp);
2358
2558
  const tasks = this.loadTaskState(key);
2359
2559
  return {
2560
+ backend: 'cdp',
2360
2561
  cdp,
2361
2562
  port: existingInfo.port,
2362
2563
  pid: existingInfo.pid,
@@ -2389,6 +2590,7 @@ export class BrowserService {
2389
2590
  const conn = await connectLocal(endpoint, profile, key);
2390
2591
  await this.enableDomains(conn.cdp);
2391
2592
  return {
2593
+ backend: 'cdp',
2392
2594
  cdp: conn.cdp,
2393
2595
  port: conn.port,
2394
2596
  pid: conn.pid,
@@ -2405,6 +2607,7 @@ export class BrowserService {
2405
2607
  });
2406
2608
  await this.enableDomains(conn.cdp);
2407
2609
  return {
2610
+ backend: 'cdp',
2408
2611
  cdp: conn.cdp,
2409
2612
  port: conn.port,
2410
2613
  pid: conn.pid,
@@ -2427,6 +2630,7 @@ export class BrowserService {
2427
2630
  }
2428
2631
  await this.enableDomains(cdp);
2429
2632
  return {
2633
+ backend: 'cdp',
2430
2634
  cdp,
2431
2635
  port: 0,
2432
2636
  pid: 0,
@@ -2445,6 +2649,7 @@ export class BrowserService {
2445
2649
  await cdp.connect(wsUrl);
2446
2650
  await this.enableDomains(cdp);
2447
2651
  return {
2652
+ backend: 'cdp',
2448
2653
  cdp,
2449
2654
  port,
2450
2655
  pid: 0,
@@ -2455,6 +2660,32 @@ export class BrowserService {
2455
2660
  sessionCache: new Map(),
2456
2661
  };
2457
2662
  }
2663
+ // Native Arc backend (PHNX-2399): `arc-native:` protocol connects through
2664
+ // Apple Events instead of CDP. No debugging port, no browser launch.
2665
+ if (url.protocol === 'arc-native:') {
2666
+ if (process.platform !== 'darwin') {
2667
+ throw new Error('Native Arc automation is only available on macOS');
2668
+ }
2669
+ if (!(await isArcRunning())) {
2670
+ throw new Error('Arc is not running. Start Arc normally, then retry.\n' +
2671
+ 'Native Arc automation drives your existing browser — it does not launch one.');
2672
+ }
2673
+ if (!profile.arc) {
2674
+ throw new Error(`Native Arc profile "${profile.name}" has no stable native profile metadata. ` +
2675
+ `Re-select it with: agents browser use arc-<profile-id>`);
2676
+ }
2677
+ return {
2678
+ port: 0,
2679
+ pid: 0,
2680
+ electron: false,
2681
+ browserType: 'arc',
2682
+ targetFilter: profile.targetFilter,
2683
+ tasks: this.loadTaskState(key),
2684
+ sessionCache: new Map(),
2685
+ backend: 'arc-native',
2686
+ arcProfile: profile.arc,
2687
+ };
2688
+ }
2458
2689
  return null;
2459
2690
  }
2460
2691
  async enableDomains(cdp) {
@@ -2660,6 +2891,7 @@ export class BrowserService {
2660
2891
  const matches = this.listTasksForCaller(caller);
2661
2892
  if (matches.length === 1) {
2662
2893
  const m = matches[0];
2894
+ await this.prepareTask(m.conn, m.task);
2663
2895
  await this.touchTask(m.conn, m.task);
2664
2896
  return { conn: m.conn, task: m.task, key: m.key, created: false };
2665
2897
  }
@@ -2790,6 +3022,18 @@ export class BrowserService {
2790
3022
  // `recordBrowserSession` call in `start` uses for the same reason.
2791
3023
  }
2792
3024
  }
3025
+ async prepareTask(conn, task) {
3026
+ if (conn.backend !== 'arc-native')
3027
+ return;
3028
+ await this.runExclusive('arc-native:host-app', async () => {
3029
+ await this.reconcileArcCreateIntents(conn, task);
3030
+ const native = this.requireArcTask(task);
3031
+ if (native.profileId !== conn.arcProfile.profileId) {
3032
+ throw new Error(`Arc task profile ${JSON.stringify(native.profileId)} does not match connected profile ` +
3033
+ `${JSON.stringify(conn.arcProfile.profileId)}.`);
3034
+ }
3035
+ });
3036
+ }
2793
3037
  async findTask(taskId, profileRef) {
2794
3038
  if (profileRef) {
2795
3039
  // Accept bare profile names against composite connection keys.
@@ -2801,6 +3045,7 @@ export class BrowserService {
2801
3045
  const conn = this.connections.get(key);
2802
3046
  const task = this.lookupTaskOnConn(conn, taskId);
2803
3047
  if (task) {
3048
+ await this.prepareTask(conn, task);
2804
3049
  await this.touchTask(conn, task);
2805
3050
  return { conn, task, key: conn.key ?? key };
2806
3051
  }
@@ -2808,6 +3053,7 @@ export class BrowserService {
2808
3053
  // RAM miss → rehydrate from disk for this profile.
2809
3054
  const rehydrated = await this.rehydrateTaskFromDisk(taskId, profileRef);
2810
3055
  if (rehydrated) {
3056
+ await this.prepareTask(rehydrated.conn, rehydrated.task);
2811
3057
  await this.touchTask(rehydrated.conn, rehydrated.task);
2812
3058
  return rehydrated;
2813
3059
  }
@@ -2816,6 +3062,7 @@ export class BrowserService {
2816
3062
  for (const [key, conn] of this.connections) {
2817
3063
  const task = this.lookupTaskOnConn(conn, taskId);
2818
3064
  if (task) {
3065
+ await this.prepareTask(conn, task);
2819
3066
  await this.touchTask(conn, task);
2820
3067
  return { conn, task, key: conn.key ?? key };
2821
3068
  }
@@ -2824,6 +3071,7 @@ export class BrowserService {
2824
3071
  // meta.json still has the browser pid/port. Reconnect and adopt.
2825
3072
  const rehydrated = await this.rehydrateTaskFromDisk(taskId);
2826
3073
  if (rehydrated) {
3074
+ await this.prepareTask(rehydrated.conn, rehydrated.task);
2827
3075
  await this.touchTask(rehydrated.conn, rehydrated.task);
2828
3076
  return rehydrated;
2829
3077
  }
@@ -2861,6 +3109,33 @@ export class BrowserService {
2861
3109
  catch {
2862
3110
  return null;
2863
3111
  }
3112
+ // Native Arc endpoints (PHNX-2399): reconnect via the native driver if Arc
3113
+ // is still running. No port, no CDP — just verify Arc is alive and restore
3114
+ // the task state.
3115
+ if (resolved.target.startsWith('arc-native:')) {
3116
+ if (process.platform !== 'darwin' || !(await isArcRunning()))
3117
+ return null;
3118
+ const tasks = this.loadTaskState(key);
3119
+ for (const [k, t] of diskTasks) {
3120
+ if (!tasks.has(k))
3121
+ tasks.set(k, t);
3122
+ }
3123
+ if (!profile.arc)
3124
+ return null;
3125
+ return {
3126
+ port: 0,
3127
+ pid: 0,
3128
+ electron: false,
3129
+ browserType: 'arc',
3130
+ targetFilter: profile.targetFilter,
3131
+ key,
3132
+ profile: bare,
3133
+ tasks,
3134
+ sessionCache: new Map(),
3135
+ backend: 'arc-native',
3136
+ arcProfile: profile.arc,
3137
+ };
3138
+ }
2864
3139
  const existingInfo = getRunningChromeInfo(key);
2865
3140
  const parsed = parseEndpointUrl(resolved.target);
2866
3141
  const port = existingInfo?.port ?? parsed?.port;
@@ -2886,6 +3161,7 @@ export class BrowserService {
2886
3161
  tasks.set(k, t);
2887
3162
  }
2888
3163
  return {
3164
+ backend: 'cdp',
2889
3165
  cdp: conn.cdp,
2890
3166
  port: conn.port,
2891
3167
  pid: conn.pid,
@@ -2918,6 +3194,7 @@ export class BrowserService {
2918
3194
  tasks.set(k, t);
2919
3195
  }
2920
3196
  const conn = {
3197
+ backend: 'cdp',
2921
3198
  cdp,
2922
3199
  port,
2923
3200
  pid: existingInfo?.pid ?? 0,
@@ -2975,10 +3252,12 @@ export class BrowserService {
2975
3252
  else {
2976
3253
  // Shared/borrowed tunnel (or a local connection with no tunnel): close
2977
3254
  // only our own CDP client; killing the tunnel would break the winner.
2978
- try {
2979
- conn.cdp.close();
3255
+ if (conn.backend !== 'arc-native') {
3256
+ try {
3257
+ conn.cdp.close();
3258
+ }
3259
+ catch { /* best effort */ }
2980
3260
  }
2981
- catch { /* best effort */ }
2982
3261
  }
2983
3262
  return existing;
2984
3263
  }
@@ -3019,7 +3298,8 @@ export class BrowserService {
3019
3298
  if (registered !== conn)
3020
3299
  continue;
3021
3300
  try {
3022
- await this.applyDefaultDownloadBehavior(conn, key);
3301
+ if (conn.backend !== 'arc-native')
3302
+ await this.applyDefaultDownloadBehavior(conn, key);
3023
3303
  }
3024
3304
  catch {
3025
3305
  // Non-fatal for rehydrate.
@@ -3080,7 +3360,8 @@ export class BrowserService {
3080
3360
  conn = this.registerRehydratedConnection(key, attached);
3081
3361
  if (conn === attached) {
3082
3362
  try {
3083
- await this.applyDefaultDownloadBehavior(conn, key);
3363
+ if (conn.backend !== 'arc-native')
3364
+ await this.applyDefaultDownloadBehavior(conn, key);
3084
3365
  }
3085
3366
  catch {
3086
3367
  // Non-fatal.
@@ -3117,6 +3398,41 @@ export class BrowserService {
3117
3398
  const conn = this.connections.get(key);
3118
3399
  if (!conn)
3119
3400
  return null;
3401
+ if (conn.backend === 'arc-native') {
3402
+ const tasks = [];
3403
+ for (const task of conn.tasks.values()) {
3404
+ const tabs = await this.listArcTaskTabs(task);
3405
+ const domains = tabs.flatMap((tab) => {
3406
+ try {
3407
+ const domain = new URL(tab.url).hostname.replace(/^www\./, '');
3408
+ return domain && domain !== 'blank' ? [domain] : [];
3409
+ }
3410
+ catch {
3411
+ return [];
3412
+ }
3413
+ });
3414
+ tasks.push({
3415
+ id: task.id,
3416
+ name: task.name,
3417
+ label: task.label ?? task.name,
3418
+ tabCount: Object.keys(task.tabs).length,
3419
+ currentTabId: task.currentTabId,
3420
+ createdAt: task.createdAt,
3421
+ tabs: tabs.length ? tabs : undefined,
3422
+ domains: [...new Set(domains)],
3423
+ });
3424
+ }
3425
+ const parsed = parseConnectionKey(key);
3426
+ return {
3427
+ name: conn.profile ?? parsed.profile,
3428
+ endpoint: parsed.endpoint,
3429
+ key,
3430
+ running: await isArcRunning(),
3431
+ port: 0,
3432
+ pid: 0,
3433
+ tasks,
3434
+ };
3435
+ }
3120
3436
  // Fetch all targets once for efficiency
3121
3437
  let targets = [];
3122
3438
  try {
@@ -3339,10 +3655,12 @@ export class BrowserService {
3339
3655
  return undefined;
3340
3656
  if (await isConnHealthy(conn))
3341
3657
  return conn;
3342
- try {
3343
- conn.cdp.close();
3658
+ if (conn.backend !== 'arc-native') {
3659
+ try {
3660
+ conn.cdp.close();
3661
+ }
3662
+ catch { /* already closed */ }
3344
3663
  }
3345
- catch { /* already closed */ }
3346
3664
  conn.cleanup?.();
3347
3665
  this.connections.delete(key);
3348
3666
  return undefined;
@@ -3373,13 +3691,219 @@ export class BrowserService {
3373
3691
  }
3374
3692
  return undefined;
3375
3693
  }
3694
+ // ---------------------------------------------------------------------------
3695
+ // Native Arc backend helpers (PHNX-2399)
3696
+ // ---------------------------------------------------------------------------
3697
+ /**
3698
+ * Bind a task to the profile's Space in the visible Arc window that holds it.
3699
+ * The Space is the profile (PHNX-2399); only its live window id is looked up.
3700
+ */
3701
+ async resolveArcSpace(profile) {
3702
+ const live = (await enumerateArcSpaces()).filter((space) => space.spaceId === profile.spaceId);
3703
+ if (live.length !== 1) {
3704
+ throw new Error(live.length === 0
3705
+ ? `Arc Space ${JSON.stringify(profile.spaceTitle)} (${profile.spaceId}) is not open in a visible Arc window.`
3706
+ : `Arc Space ${JSON.stringify(profile.spaceTitle)} is open in more than one window; refusing an ambiguous native address.`);
3707
+ }
3708
+ return {
3709
+ windowId: live[0].windowId,
3710
+ spaceId: live[0].spaceId,
3711
+ spaceTitle: profile.spaceTitle,
3712
+ };
3713
+ }
3714
+ requireArcTask(task) {
3715
+ const native = task.arcNative;
3716
+ if (!native || typeof native.profileId !== 'string' || typeof native.windowId !== 'string' ||
3717
+ typeof native.spaceId !== 'string' || !native.tabs || typeof native.tabs !== 'object') {
3718
+ throw new Error(`Task ${JSON.stringify(task.name)} has no valid durable Arc identity.`);
3719
+ }
3720
+ for (const [shortId, ref] of Object.entries(native.tabs)) {
3721
+ if (!ref || typeof ref.windowId !== 'string' || typeof ref.spaceId !== 'string' ||
3722
+ typeof ref.tabId !== 'string' || ref.windowId !== native.windowId || ref.spaceId !== native.spaceId) {
3723
+ throw new Error(`Owned Arc tab ${JSON.stringify(shortId)} has no stable id in its original window/Space.`);
3724
+ }
3725
+ }
3726
+ for (const [shortId, intent] of Object.entries(native.createIntents ?? {})) {
3727
+ if (!intent || intent.tabId !== shortId || typeof intent.markerUrl !== 'string' ||
3728
+ !isArcCreateMarker(intent.markerUrl) ||
3729
+ typeof intent.targetUrl !== 'string' || typeof intent.createdAt !== 'number') {
3730
+ throw new Error(`Arc creation intent ${JSON.stringify(shortId)} is invalid; refusing recovery.`);
3731
+ }
3732
+ if (intent.ref && (intent.ref.windowId !== native.windowId || intent.ref.spaceId !== native.spaceId ||
3733
+ typeof intent.ref.tabId !== 'string')) {
3734
+ throw new Error(`Arc creation intent ${JSON.stringify(shortId)} left its original window/Space.`);
3735
+ }
3736
+ }
3737
+ return native;
3738
+ }
3739
+ async reconcileArcCreateIntents(conn, task) {
3740
+ const native = this.requireArcTask(task);
3741
+ for (const intent of Object.values(native.createIntents ?? {})) {
3742
+ let ref = intent.ref;
3743
+ if (ref) {
3744
+ const live = await resolveArcTab(ref);
3745
+ if (!live) {
3746
+ throw new Error(`Owned Arc tab ${intent.tabId} left its original window/Space while creation was incomplete; refusing to adopt it elsewhere.`);
3747
+ }
3748
+ if (live.url === intent.targetUrl) {
3749
+ native.tabs[intent.tabId] = ref;
3750
+ task.tabs[intent.tabId] = ref.tabId;
3751
+ if (intent.previousTabId) {
3752
+ await restoreArcSelection(ref, intent.previousTabId);
3753
+ }
3754
+ delete native.createIntents?.[intent.tabId];
3755
+ await this.saveTaskState(task.profile, conn.tasks);
3756
+ continue;
3757
+ }
3758
+ if (live.url !== intent.markerUrl) {
3759
+ throw new Error(`Owned Arc tab ${intent.tabId} changed outside this task during creation; refusing to navigate it.`);
3760
+ }
3761
+ }
3762
+ else {
3763
+ const originalSpace = (await enumerateArcSpaces()).find((space) => space.windowId === native.windowId && space.spaceId === native.spaceId);
3764
+ if (!originalSpace) {
3765
+ throw new Error('The original Arc window/Space for this task is no longer present.');
3766
+ }
3767
+ const markerMatches = originalSpace.tabs.filter((tab) => tab.url === intent.markerUrl);
3768
+ if (markerMatches.length > 1) {
3769
+ throw new Error(`Arc creation marker ${JSON.stringify(intent.markerUrl)} is not unique in the original Space.`);
3770
+ }
3771
+ if (markerMatches[0]) {
3772
+ ref = markerMatches[0];
3773
+ }
3774
+ else {
3775
+ try {
3776
+ ref = await createArcTab({ windowId: native.windowId, spaceId: native.spaceId }, intent.markerUrl);
3777
+ }
3778
+ catch (error) {
3779
+ // Arc selects the Space it was asked to create in even when the
3780
+ // creation itself fails. Put the owner back on their tab before
3781
+ // surfacing the error, so a failed agent verb never leaves them
3782
+ // staring at the wrong Space.
3783
+ if (intent.previousTabId) {
3784
+ await selectWindowTab(native.windowId, intent.previousTabId).catch(() => undefined);
3785
+ }
3786
+ throw error;
3787
+ }
3788
+ }
3789
+ intent.ref = ref;
3790
+ native.tabs[intent.tabId] = ref;
3791
+ task.tabs[intent.tabId] = ref.tabId;
3792
+ await this.saveTaskState(task.profile, conn.tasks);
3793
+ }
3794
+ await navigateArcTab(ref, intent.targetUrl);
3795
+ if (intent.previousTabId) {
3796
+ await restoreArcSelection(ref, intent.previousTabId);
3797
+ }
3798
+ delete native.createIntents?.[intent.tabId];
3799
+ task.currentTabId = intent.tabId;
3800
+ await this.saveTaskState(task.profile, conn.tasks);
3801
+ }
3802
+ }
3803
+ async createArcOwnedTab(conn, task, url) {
3804
+ const native = this.requireArcTask(task);
3805
+ const tabId = generateShortId();
3806
+ const markerUrl = arcCreateMarker();
3807
+ const originalSpace = (await enumerateArcSpaces()).find((space) => space.windowId === native.windowId && space.spaceId === native.spaceId);
3808
+ if (!originalSpace)
3809
+ throw new Error('The original Arc window/Space for this task is no longer present.');
3810
+ native.createIntents ??= {};
3811
+ native.createIntents[tabId] = {
3812
+ tabId,
3813
+ markerUrl,
3814
+ targetUrl: url,
3815
+ createdAt: Date.now(),
3816
+ previousTabId: originalSpace.activeTabId,
3817
+ };
3818
+ await this.saveTaskState(task.profile, conn.tasks);
3819
+ await this.reconcileArcCreateIntents(conn, task);
3820
+ return tabId;
3821
+ }
3822
+ async navigateArcNative(conn, task, url, addTab = false) {
3823
+ return this.runExclusive('arc-native:host-app', async () => {
3824
+ await this.reconcileArcCreateIntents(conn, task);
3825
+ const native = this.requireArcTask(task);
3826
+ if (!addTab) {
3827
+ for (const [shortId, ref] of Object.entries(native.tabs)) {
3828
+ const live = await resolveArcTab(ref);
3829
+ if (live && canonicalTabUrl(live.url) === canonicalTabUrl(url)) {
3830
+ await navigateArcTab(ref, url);
3831
+ task.currentTabId = shortId;
3832
+ await this.saveTaskState(task.profile, conn.tasks);
3833
+ return { tabId: shortId, url, created: false, refreshed: true, message: 'Tab already open—refreshed' };
3834
+ }
3835
+ }
3836
+ }
3837
+ const currentId = task.currentTabId;
3838
+ if (!addTab && currentId) {
3839
+ const ref = native.tabs[currentId];
3840
+ if (!ref || !(await resolveArcTab(ref))) {
3841
+ throw new Error(`Owned Arc tab ${currentId} is missing from its original window/Space; refusing to adopt a moved tab.`);
3842
+ }
3843
+ await navigateArcTab(ref, url);
3844
+ return { tabId: currentId, url, created: false, refreshed: false };
3845
+ }
3846
+ const tabId = await this.createArcOwnedTab(conn, task, url);
3847
+ return { tabId, url, created: true, refreshed: false };
3848
+ });
3849
+ }
3850
+ /**
3851
+ * Execute JavaScript in a native Arc tab. Sync JS only, isolated world —
3852
+ * async/promise evaluation fails honestly with an AppleScript error.
3853
+ */
3854
+ async evaluateArcNative(conn, task, shortId, expression) {
3855
+ const ref = this.requireArcTask(task).tabs[shortId];
3856
+ if (!ref || !(await resolveArcTab(ref))) {
3857
+ throw new Error(`Owned Arc tab ${shortId} is missing from its original window/Space.`);
3858
+ }
3859
+ return executeJavaScript(ref, expression);
3860
+ }
3861
+ async listArcTaskTabs(task) {
3862
+ const native = this.requireArcTask(task);
3863
+ const tabs = [];
3864
+ for (const [shortId, ref] of Object.entries(native.tabs)) {
3865
+ const live = await resolveArcTab(ref);
3866
+ if (!live) {
3867
+ throw new Error(`Owned Arc tab ${shortId} is missing from its original window/Space; refusing to adopt a moved tab.`);
3868
+ }
3869
+ tabs.push({
3870
+ id: shortId,
3871
+ url: live.url,
3872
+ title: live.title,
3873
+ task: task.name,
3874
+ current: shortId === task.currentTabId,
3875
+ });
3876
+ }
3877
+ return tabs;
3878
+ }
3879
+ /**
3880
+ * Close native Arc tabs owned by a task. Only closes tabs the task created
3881
+ * (not borrowed ones). Never kills Arc. Identifies tabs only by stable ids in
3882
+ * the original window and Space.
3883
+ */
3884
+ async closeArcNativeTabs(conn, task) {
3885
+ await this.runExclusive('arc-native:host-app', async () => {
3886
+ await this.reconcileArcCreateIntents(conn, task);
3887
+ const native = this.requireArcTask(task);
3888
+ for (const [shortId, ref] of Object.entries(native.tabs)) {
3889
+ await closeArcTab(ref);
3890
+ delete native.tabs[shortId];
3891
+ delete task.tabs[shortId];
3892
+ delete task.refDescriptors?.[shortId];
3893
+ }
3894
+ task.currentTabId = undefined;
3895
+ });
3896
+ }
3376
3897
  /** Connect a profile at `target`, register it under `key`, and arm downloads. */
3377
3898
  async openConnection(profile, target, key, profileName, opts = {}) {
3378
3899
  const conn = await this.connectProfile(profile, target, key, opts);
3379
3900
  conn.key = key;
3380
3901
  conn.profile = profileName;
3381
3902
  this.connections.set(key, conn);
3382
- await this.applyDefaultDownloadBehavior(conn, key);
3903
+ // Download behavior configuration is CDP-specific; skip for native Arc.
3904
+ if (conn.backend !== 'arc-native') {
3905
+ await this.applyDefaultDownloadBehavior(conn, key);
3906
+ }
3383
3907
  return conn;
3384
3908
  }
3385
3909
  /**
@@ -3402,6 +3926,9 @@ export class BrowserService {
3402
3926
  endpointName: opts.endpointName,
3403
3927
  probe: opts.probe,
3404
3928
  });
3929
+ if (!routed.local && routed.commandDispatch) {
3930
+ throw new Error(`Native Arc profile "${profileName}" is owned by ${routed.device}; dispatch the complete browser command there.`);
3931
+ }
3405
3932
  const key = routed.key;
3406
3933
  const effectiveProfile = routed.profile;
3407
3934
  const reused = await this.reuseProfileConnection(profileName, key, routed.local);
@@ -3411,6 +3938,9 @@ export class BrowserService {
3411
3938
  adoptLegacyRuntimeIfLocal(routed.local, profileName, key, getBrowserRuntimeDir());
3412
3939
  conn = await this.openConnection(effectiveProfile, routed.target, key, profileName, { persistRemote: !routed.local });
3413
3940
  }
3941
+ if (conn.backend === 'arc-native') {
3942
+ throw new ArcNativeCapabilityError('show', 'Task-less viewer tabs are unavailable for native Arc because creation intent cannot be bound to an owning task.');
3943
+ }
3414
3944
  // createPageTarget refuses Arc with an actionable error rather than crashing it.
3415
3945
  const created = await this.createPageTarget(conn, { url });
3416
3946
  this.invalidateTargetCache(conn);