@phnx-labs/agents-cli 1.22.40 → 1.22.41

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 (58) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +5 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.js +0 -1
  5. package/dist/cli/command-registry.js +1 -2
  6. package/dist/commands/browser.d.ts +15 -0
  7. package/dist/commands/browser.js +115 -40
  8. package/dist/commands/feed.js +3 -11
  9. package/dist/commands/secrets.js +87 -97
  10. package/dist/commands/sessions-picker.d.ts +1 -0
  11. package/dist/commands/sessions-picker.js +27 -5
  12. package/dist/commands/sessions-share.d.ts +25 -0
  13. package/dist/commands/sessions-share.js +166 -0
  14. package/dist/commands/sessions.js +2 -0
  15. package/dist/commands/setup-browser.js +1 -1
  16. package/dist/commands/setup-preferences.js +2 -2
  17. package/dist/commands/webhook.js +14 -9
  18. package/dist/lib/browser/cdp.js +4 -0
  19. package/dist/lib/browser/chrome.js +12 -1
  20. package/dist/lib/browser/drivers/ssh.js +1 -0
  21. package/dist/lib/browser/profiles.d.ts +3 -3
  22. package/dist/lib/browser/profiles.js +7 -7
  23. package/dist/lib/browser/service.d.ts +24 -1
  24. package/dist/lib/browser/service.js +38 -14
  25. package/dist/lib/browser/types.d.ts +1 -1
  26. package/dist/lib/daemon-webhooks.d.ts +3 -1
  27. package/dist/lib/daemon-webhooks.js +12 -7
  28. package/dist/lib/device-config.js +1 -1
  29. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  30. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  31. package/dist/lib/observe-aliases.d.ts +2 -2
  32. package/dist/lib/observe-aliases.js +2 -11
  33. package/dist/lib/project-key.js +7 -0
  34. package/dist/lib/runner.js +21 -6
  35. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  36. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  37. package/dist/lib/secrets/bundles.d.ts +1 -1
  38. package/dist/lib/secrets/bundles.js +1 -1
  39. package/dist/lib/secrets/headless.d.ts +16 -0
  40. package/dist/lib/secrets/headless.js +21 -0
  41. package/dist/lib/secrets/remote.d.ts +9 -7
  42. package/dist/lib/secrets/remote.js +18 -9
  43. package/dist/lib/session/share-html.d.ts +55 -0
  44. package/dist/lib/session/share-html.js +319 -0
  45. package/dist/lib/settings-manifest.d.ts +2 -0
  46. package/dist/lib/settings-manifest.js +81 -3
  47. package/dist/lib/share/publish.d.ts +38 -0
  48. package/dist/lib/share/publish.js +81 -8
  49. package/dist/lib/startup/command-registry.d.ts +2 -1
  50. package/dist/lib/startup/command-registry.js +4 -2
  51. package/dist/lib/triggers/handlers.d.ts +37 -2
  52. package/dist/lib/triggers/handlers.js +56 -5
  53. package/dist/lib/triggers/webhook.d.ts +80 -6
  54. package/dist/lib/triggers/webhook.js +127 -3
  55. package/dist/lib/types.d.ts +1 -1
  56. package/dist/lib/wrap.d.ts +33 -0
  57. package/dist/lib/wrap.js +70 -0
  58. package/package.json +1 -1
@@ -25,8 +25,10 @@ function readWebhookSecrets(bundleName) {
25
25
  secrets.github = env.GITHUB_WEBHOOK_SECRET;
26
26
  if (env.LINEAR_WEBHOOK_SECRET)
27
27
  secrets.linear = env.LINEAR_WEBHOOK_SECRET;
28
- if (!secrets.github && !secrets.linear) {
29
- throw new Error(`Bundle '${bundleName}' must contain GITHUB_WEBHOOK_SECRET or LINEAR_WEBHOOK_SECRET.`);
28
+ if (env.SLACK_SIGNING_SECRET)
29
+ secrets.slack = env.SLACK_SIGNING_SECRET;
30
+ if (!secrets.github && !secrets.linear && !secrets.slack) {
31
+ throw new Error(`Bundle '${bundleName}' must contain GITHUB_WEBHOOK_SECRET, LINEAR_WEBHOOK_SECRET, or SLACK_SIGNING_SECRET.`);
30
32
  }
31
33
  return secrets;
32
34
  }
@@ -36,8 +38,8 @@ export function registerWebhooksCommand(program) {
36
38
  .description('Run a localhost signed webhook receiver for routine triggers.');
37
39
  webhooks
38
40
  .command('serve')
39
- .description('Receive signed GitHub/Linear webhooks on /hooks/<source> and fire matching routines.')
40
- .requiredOption('--secrets-bundle <name>', 'agents secrets bundle containing GITHUB_WEBHOOK_SECRET and/or LINEAR_WEBHOOK_SECRET')
41
+ .description('Receive signed GitHub/Linear/Slack webhooks on /hooks/<source> and fire matching routines and handlers.')
42
+ .requiredOption('--secrets-bundle <name>', 'agents secrets bundle containing GITHUB_WEBHOOK_SECRET, LINEAR_WEBHOOK_SECRET, and/or SLACK_SIGNING_SECRET')
41
43
  .option('--bind <addr>', `Bind address (default ${DEFAULT_HOST})`, DEFAULT_HOST)
42
44
  .option('-p, --port <n>', `Local port (default ${DEFAULT_PORT})`, String(DEFAULT_PORT))
43
45
  .option('--rate-limit <n>', 'Accepted deliveries per source per minute', '60')
@@ -61,12 +63,15 @@ export function registerWebhooksCommand(program) {
61
63
  // Durable delivery dedup: replays survive a receiver restart (an
62
64
  // in-memory store would forget every seen delivery on restart).
63
65
  deliveryStore: createFileDeliveryStore(path.join(getRuntimeStateDir(), 'webhook', 'deliveries.json')),
64
- onDelivery: (webhook, fired, handlers) => {
66
+ // Logged at MATCH time, before dispatch — a `run.command` handler can
67
+ // block on a shelled-out agent run for minutes, and that must never
68
+ // delay the log that says a delivery fired (RUSH-2722).
69
+ onMatch: (webhook, matchedJobNames, matchedHandlerNames) => {
65
70
  const parts = [];
66
- if (fired.length)
67
- parts.push(`routines ${fired.map((f) => f.jobName).join(', ')}`);
68
- if (handlers.length)
69
- parts.push(`handlers ${handlers.map((h) => h.handlerName).join(', ')}`);
71
+ if (matchedJobNames.length)
72
+ parts.push(`routines ${matchedJobNames.join(', ')}`);
73
+ if (matchedHandlerNames.length)
74
+ parts.push(`handlers ${matchedHandlerNames.join(', ')}`);
70
75
  console.log(`${new Date().toISOString()} ${webhook.source}:${webhook.event} ` +
71
76
  (parts.length ? `fired ${parts.join('; ')}` : 'no match'));
72
77
  },
@@ -227,6 +227,10 @@ export function verifyBrowserIdentity(reported, expected, port, host = 'localhos
227
227
  // doesn't override the Chromium branding. Accept chrome here so attaching
228
228
  // to a Comet instance doesn't trip a false "identity mismatch".
229
229
  comet: ['comet', 'chrome'],
230
+ // Arc reports itself as plain "Chrome/<version>" in /json/version (verified
231
+ // live: Arc 1.15x → "Chrome/151"), so accept chrome to avoid a false
232
+ // identity mismatch when attaching to a real Arc.
233
+ arc: ['arc', 'chrome'],
230
234
  brave: ['brave', 'brave-browser', 'chrome'],
231
235
  // Windows/Chromium Edge reports its /json/version "Browser" field as
232
236
  // "Edg/<version>" (the `Edg` token, not `Edge`), which normalizeBrowserName
@@ -23,6 +23,7 @@ const BROWSER_PATHS = {
23
23
  chromium: ['/Applications/Chromium.app/Contents/MacOS/Chromium'],
24
24
  brave: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'],
25
25
  edge: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],
26
+ arc: ['/Applications/Arc.app/Contents/MacOS/Arc'],
26
27
  custom: [],
27
28
  },
28
29
  linux: {
@@ -31,6 +32,8 @@ const BROWSER_PATHS = {
31
32
  chromium: ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium'],
32
33
  brave: ['/usr/bin/brave-browser', '/usr/bin/brave'],
33
34
  edge: ['/usr/bin/microsoft-edge'],
35
+ // Arc has no Linux build (macOS + Windows only).
36
+ arc: [],
34
37
  custom: [],
35
38
  },
36
39
  win32: {
@@ -57,6 +60,9 @@ const BROWSER_PATHS = {
57
60
  `${WIN_PROGRAMFILES_X86}\\Microsoft\\Edge\\Application\\msedge.exe`,
58
61
  `${WIN_LOCALAPPDATA}\\Microsoft\\Edge\\Application\\msedge.exe`,
59
62
  ],
63
+ // Arc ships a Windows build, but its install path is not yet verified here;
64
+ // leave unlisted (undetected) rather than guess a path that false-positives.
65
+ arc: [],
60
66
  custom: [],
61
67
  },
62
68
  };
@@ -162,6 +168,9 @@ export function findBrowserPath(browserType, customBinary) {
162
168
  if (browserType === 'comet' && platform === 'linux') {
163
169
  throw new Error('Browser "comet" is not available on Linux (Comet ships macOS and Windows builds only). Use chrome, chromium, brave, or edge on this platform.');
164
170
  }
171
+ if (browserType === 'arc' && platform === 'linux') {
172
+ throw new Error('Browser "arc" is not available on Linux (Arc ships macOS and Windows builds only). Use chrome, chromium, brave, or edge on this platform.');
173
+ }
165
174
  throw new Error(`Browser "${browserType}" not found. Install it first.`);
166
175
  }
167
176
  // Per-platform Chromium-family priority list for "no --profile" auto-pick.
@@ -170,7 +179,9 @@ export function findBrowserPath(browserType, customBinary) {
170
179
  // way cdp.ts expects, so they'd need separate drivers.
171
180
  const DEFAULT_BROWSER_PRIORITY = {
172
181
  // macOS: Chrome leads (>70% of dev machines), then the rest of the family.
173
- darwin: ['chrome', 'brave', 'edge', 'chromium', 'comet'],
182
+ // Arc is last: it's in maintenance mode and needs a blank tab to drive, so it
183
+ // shouldn't win auto-pick over a mainstream browser.
184
+ darwin: ['chrome', 'brave', 'edge', 'chromium', 'comet', 'arc'],
174
185
  // Linux: Chrome/Chromium first (apt/snap), then Brave/Edge if present.
175
186
  linux: ['chrome', 'chromium', 'brave', 'edge'],
176
187
  // Windows: Edge is preinstalled on every supported build, so it's the
@@ -142,6 +142,7 @@ const POSIX_BROWSER_PATHS = {
142
142
  chromium: '/Applications/Chromium.app/Contents/MacOS/Chromium',
143
143
  brave: '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
144
144
  edge: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
145
+ arc: '/Applications/Arc.app/Contents/MacOS/Arc',
145
146
  };
146
147
  // Windows App Paths registry keys per browser. CreateProcess (used by WMI
147
148
  // Win32_Process.Create) does not honor App Paths the way ShellExecute/`start`
@@ -14,7 +14,7 @@ export interface ScopedBrowserProfile {
14
14
  }
15
15
  /**
16
16
  * The device-local configured default profile name (set via
17
- * `agents browser profiles set-default`), or undefined when unset. When set, it
17
+ * `agents browser use`), or undefined when unset. When set, it
18
18
  * is the profile `agents browser start` resolves to for BOTH the no-`--profile`
19
19
  * path and an explicit `--profile default`. Stored as this machine's
20
20
  * `browser.profile` device-config key (the per-device doc's `config:` block —
@@ -62,7 +62,7 @@ export declare function isProfileLaunchableHere(profile: BrowserProfile): boolea
62
62
  * set-default <name>`) when it names an existing profile that can launch here;
63
63
  * (2) an existing `default` profile that can launch here; (3) auto-pick the
64
64
  * first installed Chromium-family browser per the platform priority list in
65
- * chrome.ts (macOS: chrome > brave > edge > chromium > comet; Linux: chrome >
65
+ * chrome.ts (macOS: chrome > brave > edge > chromium > comet > arc; Linux: chrome >
66
66
  * chromium > brave > edge; Windows: edge > chrome > brave > comet) and pin a
67
67
  * `default` profile to it. Throws an actionable error if none of those binaries
68
68
  * are installed.
@@ -133,7 +133,7 @@ export declare function padColumn(text: string, width: number): string;
133
133
  * Two things used to be called "default" in this listing with no way to tell
134
134
  * them apart (RUSH-2710): the profile literally NAMED `default` (the
135
135
  * auto-detected one), and whichever profile this machine resolves a bare
136
- * `agents browser start` to (`agents config set browser.profile <name>`). They
136
+ * `agents browser start` to (`agents browser use <name>`). They
137
137
  * are frequently different profiles. Now only the second is a marker — a `*` in
138
138
  * a leading column, explained by a legend line — so the name column carries just
139
139
  * the name and `default` in it always means the profile of that name.
@@ -25,7 +25,7 @@ function isMachineLocalProfile(name) {
25
25
  }
26
26
  /**
27
27
  * The device-local configured default profile name (set via
28
- * `agents browser profiles set-default`), or undefined when unset. When set, it
28
+ * `agents browser use`), or undefined when unset. When set, it
29
29
  * is the profile `agents browser start` resolves to for BOTH the no-`--profile`
30
30
  * path and an explicit `--profile default`. Stored as this machine's
31
31
  * `browser.profile` device-config key (the per-device doc's `config:` block —
@@ -165,7 +165,7 @@ export function isProfileLaunchableHere(profile) {
165
165
  * set-default <name>`) when it names an existing profile that can launch here;
166
166
  * (2) an existing `default` profile that can launch here; (3) auto-pick the
167
167
  * first installed Chromium-family browser per the platform priority list in
168
- * chrome.ts (macOS: chrome > brave > edge > chromium > comet; Linux: chrome >
168
+ * chrome.ts (macOS: chrome > brave > edge > chromium > comet > arc; Linux: chrome >
169
169
  * chromium > brave > edge; Windows: edge > chrome > brave > comet) and pin a
170
170
  * `default` profile to it. Throws an actionable error if none of those binaries
171
171
  * are installed.
@@ -187,18 +187,18 @@ export async function ensureDefaultBrowserProfile() {
187
187
  console.warn(chosen
188
188
  ? `warning: configured default browser profile "${configured}" can't launch on this ` +
189
189
  `machine (its browser/binary isn't installed here); falling back to auto-detect. ` +
190
- `Fix with: agents browser profiles set-default <name> (or --unset)`
190
+ `Fix with: agents browser use <name> (or --unset)`
191
191
  : `warning: configured default browser profile "${configured}" no longer exists; ` +
192
- `falling back to auto-detect. Fix with: agents browser profiles set-default <name> (or --unset)`);
192
+ `falling back to auto-detect. Fix with: agents browser use <name> (or --unset)`);
193
193
  }
194
194
  const existing = await getProfile(DEFAULT_BROWSER_PROFILE_NAME);
195
195
  if (existing && isProfileLaunchableHere(existing))
196
196
  return existing;
197
197
  const detected = findFirstInstalledBrowser();
198
198
  if (!detected) {
199
- throw new Error('No supported browser found. Install one of: Chrome, Brave, Edge, Chromium, or Comet, ' +
199
+ throw new Error('No supported browser found. Install one of: Chrome, Brave, Edge, Chromium, Comet, or Arc, ' +
200
200
  'then re-run `agents browser start`. Or create a profile explicitly with ' +
201
- '`agents browser profiles create <name> --browser <chrome|comet|chromium|brave|edge|custom>`. ' +
201
+ '`agents browser profiles create <name> --browser <chrome|comet|chromium|brave|edge|arc|custom>`. ' +
202
202
  'Note: Safari and Firefox are not supported — agents browser drives over the ' +
203
203
  'Chrome DevTools Protocol, which they don\'t implement.');
204
204
  }
@@ -431,7 +431,7 @@ export function padColumn(text, width) {
431
431
  * Two things used to be called "default" in this listing with no way to tell
432
432
  * them apart (RUSH-2710): the profile literally NAMED `default` (the
433
433
  * auto-detected one), and whichever profile this machine resolves a bare
434
- * `agents browser start` to (`agents config set browser.profile <name>`). They
434
+ * `agents browser start` to (`agents browser use <name>`). They
435
435
  * are frequently different profiles. Now only the second is a marker — a `*` in
436
436
  * a leading column, explained by a legend line — so the name column carries just
437
437
  * the name and `default` in it always means the profile of that name.
@@ -1,6 +1,6 @@
1
1
  import { CDPClient } from './cdp.js';
2
2
  import { type ResolvedDomainSkill } from './domain-skills.js';
3
- import { type Task, type TabInfo, type ProfileStatus, type HistoricalTask, type ReapResult } from './types.js';
3
+ import { type Task, type TabInfo, type ProfileStatus, type BrowserType, type HistoricalTask, type ReapResult } from './types.js';
4
4
  import { type ReapOptions } from './hygiene.js';
5
5
  import { type RefOpts, type RefNode } from './refs.js';
6
6
  import type { TargetFilter } from './types.js';
@@ -50,6 +50,13 @@ interface ProfileConnection {
50
50
  port: number;
51
51
  pid: number;
52
52
  electron?: boolean;
53
+ /**
54
+ * The profile's declared browser family. Load-bearing for Arc: Arc answers
55
+ * `Browser.getVersion` but exposes zero CDP page targets and CRASHES on
56
+ * `Target.createTarget` (verified live, PR #2778), so any tab-creating path
57
+ * must refuse rather than crash the user's Arc. See `createPageTarget`.
58
+ */
59
+ browserType?: BrowserType;
53
60
  /** Raw `url:<v>` / `title:<v>` filter copied from the profile config. */
54
61
  targetFilter?: string;
55
62
  /**
@@ -83,6 +90,14 @@ interface ProfileConnection {
83
90
  }
84
91
  /** Join error lines so callers get a next command, not a dead-end message. */
85
92
  export declare function actionable(...lines: string[]): string;
93
+ /**
94
+ * Arc answers `Browser.getVersion` (so a connection succeeds) but exposes zero
95
+ * CDP page targets via every discovery method and CRASHES when a new tab is
96
+ * requested via `Target.createTarget` (verified live, PR #2778). It is therefore
97
+ * not drivable. This is the single clear, actionable error every tab-creating
98
+ * path throws instead of crashing the user's Arc window.
99
+ */
100
+ export declare function arcNotDrivableError(profileName?: string): Error;
86
101
  /** Derive a human label from an explicit title or a navigated URL. */
87
102
  export declare function deriveTaskLabel(opts: {
88
103
  title?: string;
@@ -392,6 +407,14 @@ export declare class BrowserService {
392
407
  private connectProfile;
393
408
  private connectEndpoint;
394
409
  private enableDomains;
410
+ /**
411
+ * The single path that opens a new CDP page/window target. It exists so the
412
+ * Arc guard lives in exactly one place: `Target.createTarget` crashes Arc
413
+ * (verified, PR #2778), so an Arc connection throws a clear, actionable error
414
+ * here instead of every call site re-checking. Every drivable Chromium-family
415
+ * browser goes straight through.
416
+ */
417
+ private createPageTarget;
395
418
  private getOrCreateWindow;
396
419
  private hasTaskNamed;
397
420
  /** Map key for a task on a connection (tasks are keyed by `name`). */
@@ -243,6 +243,17 @@ async function isConnHealthy(conn, timeoutMs = 1000) {
243
243
  export function actionable(...lines) {
244
244
  return lines.filter((l) => l != null && l !== '').join('\n');
245
245
  }
246
+ /**
247
+ * Arc answers `Browser.getVersion` (so a connection succeeds) but exposes zero
248
+ * CDP page targets via every discovery method and CRASHES when a new tab is
249
+ * requested via `Target.createTarget` (verified live, PR #2778). It is therefore
250
+ * not drivable. This is the single clear, actionable error every tab-creating
251
+ * path throws instead of crashing the user's Arc window.
252
+ */
253
+ export function arcNotDrivableError(profileName) {
254
+ const scope = profileName ? ` (profile "${profileName}")` : '';
255
+ return new Error(actionable(`Browser "arc"${scope} is not drivable: Arc exposes no CDP page targets and`, 'crashes when a new tab is requested, so `agents browser` cannot control it.', 'Use a Chromium-family browser instead — Comet, Chrome, Chromium, or Brave:', ' agents browser profiles create <name> --browser comet'));
256
+ }
246
257
  /** Derive a human label from an explicit title or a navigated URL. */
247
258
  export function deriveTaskLabel(opts) {
248
259
  if (opts.title?.trim())
@@ -404,9 +415,9 @@ export class BrowserService {
404
415
  if (!opts.url && !conn.electron) {
405
416
  const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
406
417
  if (!targetInfos.some((t) => t.type === 'page')) {
407
- const created = (await conn.cdp.send('Target.createTarget', {
418
+ const created = await this.createPageTarget(conn, {
408
419
  url: 'about:blank',
409
- }));
420
+ });
410
421
  startupBlankTargetId = created.targetId;
411
422
  this.invalidateTargetCache(conn);
412
423
  }
@@ -485,10 +496,7 @@ export class BrowserService {
485
496
  let tabId;
486
497
  if (opts.url && !conn.electron) {
487
498
  const adopted = opts.fresh ? undefined : await this.adoptTabShowing(conn, opts.url);
488
- const targetId = adopted ??
489
- (await conn.cdp.send('Target.createTarget', {
490
- url: opts.url,
491
- })).targetId;
499
+ const targetId = adopted ?? (await this.createPageTarget(conn, { url: opts.url })).targetId;
492
500
  const shortId = generateShortId();
493
501
  task.tabs[shortId] = targetId;
494
502
  task.currentTabId = shortId;
@@ -747,9 +755,7 @@ export class BrowserService {
747
755
  return { tabId: shortId, url, created: true };
748
756
  }
749
757
  // Chrome: create new tab
750
- const result = (await conn.cdp.send('Target.createTarget', {
751
- url,
752
- }));
758
+ const result = await this.createPageTarget(conn, { url });
753
759
  const shortId = generateShortId();
754
760
  task.tabs[shortId] = result.targetId;
755
761
  task.currentTabId = shortId;
@@ -763,9 +769,7 @@ export class BrowserService {
763
769
  if (conn.electron) {
764
770
  throw new Error('Electron apps do not support opening additional tabs');
765
771
  }
766
- const result = (await conn.cdp.send('Target.createTarget', {
767
- url,
768
- }));
772
+ const result = await this.createPageTarget(conn, { url });
769
773
  const shortId = generateShortId();
770
774
  task.tabs[shortId] = result.targetId;
771
775
  task.currentTabId = shortId; // new tab becomes current
@@ -1929,6 +1933,7 @@ export class BrowserService {
1929
1933
  port,
1930
1934
  pid,
1931
1935
  electron: true,
1936
+ browserType: profile.browser,
1932
1937
  targetFilter: profile.targetFilter,
1933
1938
  profileName: forkName,
1934
1939
  forkedFrom: profile.name,
@@ -1963,6 +1968,7 @@ export class BrowserService {
1963
1968
  port: existingInfo.port,
1964
1969
  pid: existingInfo.pid,
1965
1970
  electron: effectiveProfile.electron,
1971
+ browserType: effectiveProfile.browser,
1966
1972
  targetFilter: effectiveProfile.targetFilter,
1967
1973
  tasks,
1968
1974
  sessionCache: new Map(),
@@ -1994,6 +2000,7 @@ export class BrowserService {
1994
2000
  port: conn.port,
1995
2001
  pid: conn.pid,
1996
2002
  electron: profile.electron,
2003
+ browserType: profile.browser,
1997
2004
  targetFilter: profile.targetFilter,
1998
2005
  tasks: conn.pid === 0 ? this.loadTaskState(profile.name) : new Map(),
1999
2006
  sessionCache: new Map(),
@@ -2007,6 +2014,7 @@ export class BrowserService {
2007
2014
  port: conn.port,
2008
2015
  pid: conn.pid,
2009
2016
  electron: profile.electron,
2017
+ browserType: profile.browser,
2010
2018
  targetFilter: profile.targetFilter,
2011
2019
  tasks: new Map(),
2012
2020
  sessionCache: new Map(),
@@ -2028,6 +2036,7 @@ export class BrowserService {
2028
2036
  port: 0,
2029
2037
  pid: 0,
2030
2038
  electron: profile.electron,
2039
+ browserType: profile.browser,
2031
2040
  targetFilter: profile.targetFilter,
2032
2041
  tasks: this.loadTaskState(profile.name),
2033
2042
  sessionCache: new Map(),
@@ -2045,6 +2054,7 @@ export class BrowserService {
2045
2054
  port,
2046
2055
  pid: 0,
2047
2056
  electron: profile.electron,
2057
+ browserType: profile.browser,
2048
2058
  targetFilter: profile.targetFilter,
2049
2059
  tasks: this.loadTaskState(profile.name),
2050
2060
  sessionCache: new Map(),
@@ -2055,6 +2065,19 @@ export class BrowserService {
2055
2065
  async enableDomains(cdp) {
2056
2066
  await cdp.send('Target.setDiscoverTargets', { discover: true });
2057
2067
  }
2068
+ /**
2069
+ * The single path that opens a new CDP page/window target. It exists so the
2070
+ * Arc guard lives in exactly one place: `Target.createTarget` crashes Arc
2071
+ * (verified, PR #2778), so an Arc connection throws a clear, actionable error
2072
+ * here instead of every call site re-checking. Every drivable Chromium-family
2073
+ * browser goes straight through.
2074
+ */
2075
+ async createPageTarget(conn, params) {
2076
+ if (conn.browserType === 'arc') {
2077
+ throw arcNotDrivableError(conn.bareName ?? conn.profileName);
2078
+ }
2079
+ return (await conn.cdp.send('Target.createTarget', params));
2080
+ }
2058
2081
  async getOrCreateWindow(conn) {
2059
2082
  // Already have a window for this profile?
2060
2083
  if (conn.windowId) {
@@ -2086,10 +2109,10 @@ export class BrowserService {
2086
2109
  `Available page targets:\n${candidates || ' (none)'}`);
2087
2110
  }
2088
2111
  // First ever use - create window
2089
- const result = (await conn.cdp.send('Target.createTarget', {
2112
+ const result = await this.createPageTarget(conn, {
2090
2113
  url: 'about:blank',
2091
2114
  newWindow: true,
2092
- }));
2115
+ });
2093
2116
  conn.windowId = result.targetId;
2094
2117
  return result.targetId;
2095
2118
  }
@@ -2358,6 +2381,7 @@ export class BrowserService {
2358
2381
  port,
2359
2382
  pid: existingInfo?.pid ?? 0,
2360
2383
  electron: profile.electron,
2384
+ browserType: profile.browser,
2361
2385
  targetFilter: resolved.targetFilter ?? profile.targetFilter,
2362
2386
  profileName: dirName,
2363
2387
  bareName: bare,
@@ -1,4 +1,4 @@
1
- export type BrowserType = 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'custom';
1
+ export type BrowserType = 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'arc' | 'custom';
2
2
  /**
3
3
  * A single named endpoint preset within a profile. Lets one profile cover
4
4
  * the local + remote variants of the same app (e.g. an Electron app on this
@@ -23,7 +23,9 @@ export interface HostedReceiverFunnel {
23
23
  }
24
24
  /** One receiver this box hosts. */
25
25
  export interface HostedReceiverConfig {
26
- /** Secrets bundle holding GITHUB_WEBHOOK_SECRET and/or LINEAR_WEBHOOK_SECRET. */
26
+ /** Secrets bundle holding GITHUB_WEBHOOK_SECRET, LINEAR_WEBHOOK_SECRET, and/or
27
+ * SLACK_SIGNING_SECRET (a Slack receiver also carries SLACK_BOT_TOKEN when the
28
+ * agent replies with the Slack Web API rather than `agents send`). */
27
29
  bundle: string;
28
30
  /** Local bind port (default 8787). */
29
31
  port?: number;
@@ -112,8 +112,10 @@ export function resolveReceiverSecrets(bundle) {
112
112
  secrets.github = env.GITHUB_WEBHOOK_SECRET;
113
113
  if (env.LINEAR_WEBHOOK_SECRET)
114
114
  secrets.linear = env.LINEAR_WEBHOOK_SECRET;
115
- if (!secrets.github && !secrets.linear) {
116
- throw new Error(`bundle '${bundle}' has neither GITHUB_WEBHOOK_SECRET nor LINEAR_WEBHOOK_SECRET`);
115
+ if (env.SLACK_SIGNING_SECRET)
116
+ secrets.slack = env.SLACK_SIGNING_SECRET;
117
+ if (!secrets.github && !secrets.linear && !secrets.slack) {
118
+ throw new Error(`bundle '${bundle}' has none of GITHUB_WEBHOOK_SECRET, LINEAR_WEBHOOK_SECRET, or SLACK_SIGNING_SECRET`);
117
119
  }
118
120
  return secrets;
119
121
  }
@@ -176,12 +178,15 @@ export async function startHostedWebhookReceivers(opts) {
176
178
  // Per-port durable delivery dedup so replays survive a daemon restart and
177
179
  // two receivers on distinct ports never share a dedup ledger.
178
180
  deliveryStore: createFileDeliveryStore(path.join(getRuntimeStateDir(), 'webhook', `deliveries-${port}.json`)),
179
- onDelivery: (webhook, fired, handlers) => {
181
+ // Logged at MATCH time, before dispatch — a `run.command` handler can
182
+ // block on a shelled-out agent run for minutes, and that must never
183
+ // delay the log that says a delivery fired (RUSH-2722).
184
+ onMatch: (webhook, matchedJobNames, matchedHandlerNames) => {
180
185
  const parts = [];
181
- if (fired.length)
182
- parts.push(`routines ${fired.map((f) => f.jobName).join(', ')}`);
183
- if (handlers.length)
184
- parts.push(`handlers ${handlers.map((h) => h.handlerName).join(', ')}`);
186
+ if (matchedJobNames.length)
187
+ parts.push(`routines ${matchedJobNames.join(', ')}`);
188
+ if (matchedHandlerNames.length)
189
+ parts.push(`handlers ${matchedHandlerNames.join(', ')}`);
185
190
  log('INFO', `webhook ${webhook.source}:${webhook.event} ${parts.length ? `fired ${parts.join('; ')}` : 'no match'}`);
186
191
  },
187
192
  // The 202 ack means no HTTP status carries a settle failure — the daemon
@@ -96,7 +96,7 @@ export const CONFIG_KEYS = [
96
96
  scope: 'device',
97
97
  visibility: 'machine',
98
98
  type: 'string',
99
- description: 'Browser profile `agents browser start` resolves to without --profile (set via `agents browser profiles set-default`).',
99
+ description: 'Browser profile `agents browser start` resolves to without --profile (set via `agents browser use`).',
100
100
  },
101
101
  {
102
102
  name: 'agents.max-concurrent',
@@ -5,13 +5,13 @@
5
5
  * remain the stores; these are doors that point at the right reader.
6
6
  *
7
7
  * inbox → feed (needs-you default)
8
- * timeline → feed --filter updates (agent progress stream)
9
8
  *
10
9
  * `roster` was removed — use `agents sessions --active`.
10
+ * `timeline` was removed — use `agents feed --filter updates` (RUSH-2692).
11
11
  * `audit` is NOT an alias here — `agents audit` is already the tamper-evident
12
12
  * run-dispatch log. Ops trail = `agents events` (optionally `--audit`).
13
13
  */
14
- export type ObserveAlias = 'inbox' | 'timeline';
14
+ export type ObserveAlias = 'inbox';
15
15
  export declare const OBSERVE_ALIASES: readonly ObserveAlias[];
16
16
  export interface ObserveExpandResult {
17
17
  /** argv for the real command (no program name): e.g. ['feed', '--filter', 'updates'] */
@@ -5,13 +5,13 @@
5
5
  * remain the stores; these are doors that point at the right reader.
6
6
  *
7
7
  * inbox → feed (needs-you default)
8
- * timeline → feed --filter updates (agent progress stream)
9
8
  *
10
9
  * `roster` was removed — use `agents sessions --active`.
10
+ * `timeline` was removed — use `agents feed --filter updates` (RUSH-2692).
11
11
  * `audit` is NOT an alias here — `agents audit` is already the tamper-evident
12
12
  * run-dispatch log. Ops trail = `agents events` (optionally `--audit`).
13
13
  */
14
- export const OBSERVE_ALIASES = ['inbox', 'timeline'];
14
+ export const OBSERVE_ALIASES = ['inbox'];
15
15
  /** True when `rest` already carries a `--filter` / `--filter=…` flag. */
16
16
  export function hasFilterFlag(rest) {
17
17
  return rest.some((a) => a === '--filter' || a.startsWith('--filter='));
@@ -32,15 +32,6 @@ export function expandObserveAlias(alias, rest = []) {
32
32
  argv: ['feed', ...tail],
33
33
  note: 'agents inbox → agents feed (needs-you inbox)',
34
34
  };
35
- case 'timeline': {
36
- const argv = hasFilterFlag(tail)
37
- ? ['feed', ...tail]
38
- : ['feed', '--filter', 'updates', ...tail];
39
- return {
40
- argv,
41
- note: 'agents timeline → agents feed --filter updates',
42
- };
43
- }
44
35
  default:
45
36
  return null;
46
37
  }
@@ -55,6 +55,13 @@ export function repoRootForCwd(dir, home = os.homedir()) {
55
55
  for (;;) {
56
56
  if (fs.existsSync(path.join(current, '.git')))
57
57
  return current === stop ? undefined : current;
58
+ // Never climb past $HOME: an ancestor of home (/tmp, /, ...) is not part of
59
+ // any project this cwd belongs to, and treating one as the repo root would
60
+ // let unrelated host state (e.g. a stray /tmp/.git) swallow every loose
61
+ // directory under home. Mirrors the shim's own home boundary — see
62
+ // shims.ts shimExecTail.
63
+ if (current === stop)
64
+ return undefined;
58
65
  const parent = path.dirname(current);
59
66
  if (parent === current)
60
67
  return undefined;
@@ -26,7 +26,7 @@ import { resolveModel, buildReasoningFlags } from './models.js';
26
26
  import { createTimer, redactPrompt, emitRoutineEnd } from './feed/events.js';
27
27
  import { codexEditWritableRoots, codexPolicyArgs } from './codex-policy.js';
28
28
  import { applyAddDirs } from './add-dir.js';
29
- import { normalizeMode, resolveHeadlessMode, buildExecEnv, detectRateLimit, detectAuthFailure, isAuthFailureFromLog, authFailureReason, } from './exec.js';
29
+ import { normalizeMode, resolveHeadlessMode, buildExecEnv, detectRateLimit, detectAuthFailure, isAuthFailureFromLog, authFailureReason, AGENT_COMMANDS, } from './exec.js';
30
30
  import { resolveActor } from './actor.js';
31
31
  import { loadTask as loadHostTask } from './hosts/tasks.js';
32
32
  import { reconcileTask as reconcileHostTask } from './hosts/reconcile.js';
@@ -39,7 +39,7 @@ import { resolveClaudeSetupToken } from './claude-account-token.js';
39
39
  import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from './accounting/rotate.js';
40
40
  import { readAuthHealth, isDeadVerdict } from './auth-health.js';
41
41
  import { machineId } from './machine-id.js';
42
- import { isSelfUpdatingAgent, ROUTINE_AGENT_COMMANDS as AGENT_COMMANDS, isAgentHardDeprecated, hardDeprecationError } from './agents.js';
42
+ import { isSelfUpdatingAgent, ROUTINE_AGENT_COMMANDS, isAgentHardDeprecated, hardDeprecationError } from './agents.js';
43
43
  export class RoutineAlreadyRunningError extends Error {
44
44
  constructor(jobName, runId) {
45
45
  super(`Routine '${jobName}' already has a running execution (${runId})`);
@@ -416,6 +416,17 @@ async function runWithAttempt(config, trigger, run, wrapTerminal) {
416
416
  function terminateRoutineTree(pid) {
417
417
  if (!pid)
418
418
  return;
419
+ // Never take THIS process down. Both kills below are unconditional SIGKILLs,
420
+ // so a RunMeta naming the reaper's own pid -- a reused pid, a record written
421
+ // by the process now doing the reaping, or a hand-written fixture -- makes the
422
+ // reaper SIGKILL itself, and via `-pid` its whole process group with it. There
423
+ // is no recovery from that: the run is never finalized, and on the daemon it
424
+ // takes the scheduler down mid-sweep. daemon.ts:457 already refuses to evict an
425
+ // incumbent whose pid is `process.pid` for exactly this reason; the reap path
426
+ // needs the same guard. Skipping the kill still lets the caller finalize the
427
+ // run record, which is the part that matters.
428
+ if (pid === process.pid)
429
+ return;
419
430
  if (process.platform === 'win32') {
420
431
  killTree(pid);
421
432
  return;
@@ -503,7 +514,7 @@ export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
503
514
  cmd.push('--account', config.account);
504
515
  return cmd;
505
516
  }
506
- const template = AGENT_COMMANDS[agent];
517
+ const template = ROUTINE_AGENT_COMMANDS[agent];
507
518
  if (!template) {
508
519
  throw new Error(`Unsupported agent for daemon jobs: ${agent}`);
509
520
  }
@@ -610,7 +621,7 @@ export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
610
621
  return cmd;
611
622
  }
612
623
  /**
613
- * Append --model and reasoning flags to a command being assembled.
624
+ * Append the agent's canonical model flag and reasoning flags to a command.
614
625
  *
615
626
  * Pass-through model resolution: validates against the installed (agent, version)
616
627
  * catalog when possible and writes a warning to stderr on miss, but never blocks.
@@ -621,15 +632,19 @@ function appendModelAndReasoning(cmd, config) {
621
632
  const agent = config.agent;
622
633
  const model = config.config?.model;
623
634
  if (model) {
635
+ const modelFlag = AGENT_COMMANDS[agent].modelFlag;
636
+ if (!modelFlag) {
637
+ throw new Error(`Agent ${agent} does not support routine model selection`);
638
+ }
624
639
  if (config.version) {
625
640
  const resolved = resolveModel(agent, config.version, model);
626
641
  if (resolved.warning) {
627
642
  process.stderr.write(`[agents] ${resolved.warning}\n`);
628
643
  }
629
- cmd.push('--model', resolved.forwarded);
644
+ cmd.push(modelFlag, resolved.forwarded);
630
645
  }
631
646
  else {
632
- cmd.push('--model', model);
647
+ cmd.push(modelFlag, model);
633
648
  }
634
649
  }
635
650
  const reasoning = config.config?.reasoning;
@@ -305,7 +305,7 @@ export declare function resolveBundleEnv(bundle: SecretsBundle, _opts?: ResolveB
305
305
  * path in index.ts can share it without a bundles↔index import cycle. See that
306
306
  * module for the full contract.
307
307
  */
308
- export { isHeadlessSecretsContext } from './headless.js';
308
+ export { isHeadlessSecretsContext, isAgentInvocationContext } from './headless.js';
309
309
  /**
310
310
  * Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
311
311
  *
@@ -1211,7 +1211,7 @@ export function resolveBundleEnv(bundle, _opts = {}) {
1211
1211
  * path in index.ts can share it without a bundles↔index import cycle. See that
1212
1212
  * module for the full contract.
1213
1213
  */
1214
- export { isHeadlessSecretsContext } from './headless.js';
1214
+ export { isHeadlessSecretsContext, isAgentInvocationContext } from './headless.js';
1215
1215
  /**
1216
1216
  * Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
1217
1217
  *