@phnx-labs/agents-cli 1.22.116 → 1.22.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +247 -0
- package/dist/bootstrap.js +4 -11
- package/dist/commands/browser.d.ts +52 -70
- package/dist/commands/browser.js +192 -3562
- package/dist/commands/daemon.d.ts +39 -2
- package/dist/commands/daemon.js +72 -73
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +104 -12
- package/dist/commands/repo.js +13 -49
- package/dist/commands/route.js +5 -5
- package/dist/commands/routines.js +39 -12
- package/dist/commands/sessions-render.d.ts +1 -1
- package/dist/commands/sessions-render.js +0 -1
- package/dist/commands/sessions.test-fixture.d.ts +0 -1
- package/dist/commands/sessions.test-fixture.js +0 -37
- package/dist/commands/setup-browser.d.ts +17 -12
- package/dist/commands/setup-browser.js +52 -87
- package/dist/commands/setup-preferences.d.ts +0 -17
- package/dist/commands/setup-preferences.js +18 -49
- package/dist/commands/setup-term.d.ts +4 -3
- package/dist/commands/setup-term.js +5 -4
- package/dist/commands/setup.js +11 -11
- package/dist/commands/ssh.js +10 -1
- package/dist/commands/sync.js +6 -44
- package/dist/commands/teams.js +0 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +34 -6
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +47 -2
- package/dist/index.js +107 -26
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-catalog.d.ts +38 -0
- package/dist/lib/account-catalog.js +110 -26
- package/dist/lib/account-provider-registry.js +1 -1
- package/dist/lib/account-registry.d.ts +1 -1
- package/dist/lib/account-registry.js +36 -2
- package/dist/lib/accounting/rotate.js +22 -8
- package/dist/lib/accounting/usage-ingest.d.ts +6 -1
- package/dist/lib/accounting/usage-ingest.js +115 -45
- package/dist/lib/accounting/usage-sync.d.ts +134 -31
- package/dist/lib/accounting/usage-sync.js +227 -66
- package/dist/lib/accounting/usage.d.ts +16 -3
- package/dist/lib/accounting/usage.js +26 -8
- package/dist/lib/accounts/slots.d.ts +8 -0
- package/dist/lib/accounts/slots.js +17 -0
- package/dist/lib/add-dir.js +0 -1
- package/dist/lib/agent-cli-commands.js +0 -1
- package/dist/lib/agent-spec/agents.d.ts +6 -7
- package/dist/lib/agent-spec/agents.js +6 -60
- package/dist/lib/auth-health.d.ts +69 -1
- package/dist/lib/auth-health.js +133 -4
- package/dist/lib/browser/context.d.ts +82 -0
- package/dist/lib/browser/context.js +88 -0
- package/dist/lib/browser/paths.d.ts +25 -0
- package/dist/lib/browser/paths.js +51 -0
- package/dist/lib/browser/record.d.ts +38 -0
- package/dist/lib/browser/record.js +57 -0
- package/dist/lib/browser/sessions-list.js +1 -2
- package/dist/lib/browser-client.d.ts +158 -0
- package/dist/lib/browser-client.js +234 -0
- package/dist/lib/channels/owner-sink.d.ts +1 -18
- package/dist/lib/channels/owner-sink.js +29 -82
- package/dist/lib/channels/providers/rush.d.ts +7 -7
- package/dist/lib/channels/providers/rush.js +127 -57
- package/dist/lib/claude-statusline.d.ts +1 -1
- package/dist/lib/claude-statusline.js +15 -3
- package/dist/lib/commands.js +1 -1
- package/dist/lib/crabbox/lease.js +1 -1
- package/dist/lib/crabbox/runtimes.js +1 -3
- package/dist/lib/daemon/account-state-daemon-service.js +1 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
- package/dist/lib/daemon/auth-sync-service.js +37 -43
- package/dist/lib/daemon/daemon.d.ts +25 -14
- package/dist/lib/daemon/daemon.js +92 -154
- package/dist/lib/daemon/runner.d.ts +2 -2
- package/dist/lib/daemon/runner.js +32 -9
- package/dist/lib/daemon/self-update-service.d.ts +35 -34
- package/dist/lib/daemon/self-update-service.js +40 -36
- package/dist/lib/daemon/service.d.ts +12 -8
- package/dist/lib/daemon/supervisor.d.ts +65 -54
- package/dist/lib/daemon/supervisor.js +136 -171
- package/dist/lib/daemon/testdata/process-view-start.js +5 -2
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
- package/dist/lib/daemon/usage-sync-service.js +46 -78
- package/dist/lib/daemon-health.d.ts +22 -5
- package/dist/lib/daemon-health.js +47 -3
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +0 -10
- package/dist/lib/daemon-ticks.js +4 -2
- package/dist/lib/devices/doctor-findings.js +8 -11
- package/dist/lib/devices/harness-inventory.js +14 -5
- package/dist/lib/exec-account-home.js +5 -0
- package/dist/lib/exec.js +0 -11
- package/dist/lib/feed/events.js +0 -1
- package/dist/lib/feed/tool-activity.js +5 -2
- package/dist/lib/fleet-shared-state.d.ts +45 -0
- package/dist/lib/fleet-shared-state.js +78 -9
- package/dist/lib/fs-atomic.js +25 -0
- package/dist/lib/harness-auth-capabilities.js +0 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hooks/cache.d.ts +7 -0
- package/dist/lib/hooks/cache.js +27 -2
- package/dist/lib/hooks/install.js +7 -1
- package/dist/lib/hosts/ready.d.ts +38 -8
- package/dist/lib/hosts/ready.js +62 -14
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/installations/active-check.d.ts +27 -0
- package/dist/lib/installations/active-check.js +61 -0
- package/dist/lib/installations/migrate.js +6 -3
- package/dist/lib/installations/shims.js +9 -7
- package/dist/lib/installations/update.js +21 -15
- package/dist/lib/installations/versions.js +12 -1
- package/dist/lib/manifest.js +1 -1
- package/dist/lib/models.js +0 -2
- package/dist/lib/open-url.js +28 -54
- package/dist/lib/placement.d.ts +2 -0
- package/dist/lib/placement.js +5 -0
- package/dist/lib/profiles.js +0 -2
- package/dist/lib/refresh.js +1 -1
- package/dist/lib/routine-readiness.d.ts +35 -0
- package/dist/lib/routine-readiness.js +74 -16
- package/dist/lib/rules/compile.js +6 -0
- package/dist/lib/sandbox.js +0 -2
- package/dist/lib/secrets-policy.d.ts +89 -11
- package/dist/lib/secrets-policy.js +175 -28
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +65 -18
- package/dist/lib/session/discover.js +1 -283
- package/dist/lib/session/mirror.d.ts +4 -3
- package/dist/lib/session/mirror.js +4 -3
- package/dist/lib/session/parse.d.ts +0 -2
- package/dist/lib/session/parse.js +1 -138
- package/dist/lib/session/throughput.d.ts +1 -3
- package/dist/lib/session/throughput.js +0 -20
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +2 -2
- package/dist/lib/sessions-client.d.ts +60 -1
- package/dist/lib/sessions-client.js +227 -4
- package/dist/lib/setup-tool-status.js +8 -12
- package/dist/lib/signin-badge.d.ts +3 -2
- package/dist/lib/signin-badge.js +6 -3
- package/dist/lib/smart-launch.js +4 -1
- package/dist/lib/staleness/detectors/commands.d.ts +1 -1
- package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
- package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
- package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
- package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
- package/dist/lib/staleness/detectors/rules.d.ts +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +1 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
- package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.d.ts +1 -1
- package/dist/lib/staleness/writers/hooks.d.ts +1 -1
- package/dist/lib/staleness/writers/mcp.d.ts +1 -1
- package/dist/lib/staleness/writers/permissions.d.ts +1 -1
- package/dist/lib/staleness/writers/plugins.d.ts +1 -1
- package/dist/lib/staleness/writers/rules.d.ts +1 -1
- package/dist/lib/staleness/writers/skills.d.ts +1 -1
- package/dist/lib/staleness/writers/sources.js +5 -0
- package/dist/lib/staleness/writers/subagents.d.ts +1 -1
- package/dist/lib/staleness/writers/workflows.d.ts +1 -1
- package/dist/lib/state.d.ts +6 -4
- package/dist/lib/state.js +7 -17
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +1 -136
- package/dist/lib/teams/placement-probe.js +50 -15
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/types.d.ts +10 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/view-types.d.ts +32 -0
- package/dist/lib/watchdog/read.js +0 -1
- package/dist/lib/watchdog/rotate.js +1 -2
- package/dist/lib/watchdog/runner.js +2 -2
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdogTail.js +0 -31
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -8
- package/dist/browser.d.ts +0 -2
- package/dist/browser.js +0 -17
- package/dist/commands/browser-picker.d.ts +0 -18
- package/dist/commands/browser-picker.js +0 -97
- package/dist/lib/browser/arc-discovery.d.ts +0 -52
- package/dist/lib/browser/arc-discovery.js +0 -197
- package/dist/lib/browser/arc-dom.d.ts +0 -14
- package/dist/lib/browser/arc-dom.js +0 -121
- package/dist/lib/browser/caller-identity.d.ts +0 -40
- package/dist/lib/browser/caller-identity.js +0 -175
- package/dist/lib/browser/cdp.d.ts +0 -51
- package/dist/lib/browser/cdp.js +0 -254
- package/dist/lib/browser/chrome.d.ts +0 -141
- package/dist/lib/browser/chrome.js +0 -704
- package/dist/lib/browser/chromium-discovery.d.ts +0 -28
- package/dist/lib/browser/chromium-discovery.js +0 -96
- package/dist/lib/browser/devices.d.ts +0 -34
- package/dist/lib/browser/devices.js +0 -61
- package/dist/lib/browser/domain-skills.d.ts +0 -71
- package/dist/lib/browser/domain-skills.js +0 -195
- package/dist/lib/browser/drivers/arc.d.ts +0 -40
- package/dist/lib/browser/drivers/arc.js +0 -276
- package/dist/lib/browser/drivers/firefox.d.ts +0 -99
- package/dist/lib/browser/drivers/firefox.js +0 -377
- package/dist/lib/browser/drivers/local.d.ts +0 -55
- package/dist/lib/browser/drivers/local.js +0 -266
- package/dist/lib/browser/drivers/ssh.d.ts +0 -120
- package/dist/lib/browser/drivers/ssh.js +0 -467
- package/dist/lib/browser/editor.d.ts +0 -3
- package/dist/lib/browser/editor.js +0 -50
- package/dist/lib/browser/ffmpeg.d.ts +0 -12
- package/dist/lib/browser/ffmpeg.js +0 -184
- package/dist/lib/browser/firefox-discovery.d.ts +0 -69
- package/dist/lib/browser/firefox-discovery.js +0 -162
- package/dist/lib/browser/har.d.ts +0 -85
- package/dist/lib/browser/har.js +0 -77
- package/dist/lib/browser/hygiene.d.ts +0 -97
- package/dist/lib/browser/hygiene.js +0 -153
- package/dist/lib/browser/index.d.ts +0 -5
- package/dist/lib/browser/index.js +0 -5
- package/dist/lib/browser/input.d.ts +0 -7
- package/dist/lib/browser/input.js +0 -92
- package/dist/lib/browser/ipc.d.ts +0 -204
- package/dist/lib/browser/ipc.js +0 -1453
- package/dist/lib/browser/login-detection.d.ts +0 -87
- package/dist/lib/browser/login-detection.js +0 -274
- package/dist/lib/browser/profiles.d.ts +0 -422
- package/dist/lib/browser/profiles.js +0 -1157
- package/dist/lib/browser/refs.d.ts +0 -89
- package/dist/lib/browser/refs.js +0 -191
- package/dist/lib/browser/registry.d.ts +0 -70
- package/dist/lib/browser/registry.js +0 -252
- package/dist/lib/browser/remote-control.d.ts +0 -59
- package/dist/lib/browser/remote-control.js +0 -85
- package/dist/lib/browser/resolve-target.d.ts +0 -66
- package/dist/lib/browser/resolve-target.js +0 -264
- package/dist/lib/browser/runtime-state.d.ts +0 -291
- package/dist/lib/browser/runtime-state.js +0 -584
- package/dist/lib/browser/secret-ref.d.ts +0 -10
- package/dist/lib/browser/secret-ref.js +0 -14
- package/dist/lib/browser/service.d.ts +0 -812
- package/dist/lib/browser/service.js +0 -4693
- package/dist/lib/browser/stream.d.ts +0 -17
- package/dist/lib/browser/stream.js +0 -72
- package/dist/lib/browser/task-index.d.ts +0 -78
- package/dist/lib/browser/task-index.js +0 -199
- package/dist/lib/browser/types.d.ts +0 -647
- package/dist/lib/browser/types.js +0 -61
- package/dist/lib/browser/upload.d.ts +0 -29
- package/dist/lib/browser/upload.js +0 -298
- package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
- package/dist/lib/daemon/browser-ipc-service.js +0 -46
- package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
- package/dist/lib/daemon/browser-task-reap-service.js +0 -26
- package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
- package/dist/lib/fleet-shared-repo-sync.js +0 -490
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import * as net from 'net';
|
|
2
|
-
import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
|
|
3
|
-
import { launchBrowser, getPortOccupant, getProcessUserDataDir, storeOccupant, storeRelaunchCommand } from '../chrome.js';
|
|
4
|
-
import { parseEndpointUrl, isAttachOnlyProfile, resolveProfileDataDir, normalizeDataDir } from '../profiles.js';
|
|
5
|
-
/**
|
|
6
|
-
* Cheap TCP-level "is something bound here?" probe. Used as a fallback when
|
|
7
|
-
* `getPortOccupant()` (lsof-based) misses the process — Comet and other
|
|
8
|
-
* Electron apps sometimes hold a TCP socket that lsof's `-sTCP:LISTEN` filter
|
|
9
|
-
* doesn't report. If anything ACKs the connection, we treat the port as taken
|
|
10
|
-
* and surface a friendly error instead of silently auto-launching a fresh
|
|
11
|
-
* browser that would then conflict.
|
|
12
|
-
*/
|
|
13
|
-
async function probeTcpBound(port, host = '127.0.0.1', timeoutMs = 500) {
|
|
14
|
-
return new Promise((resolve) => {
|
|
15
|
-
const sock = net.createConnection({ port, host });
|
|
16
|
-
const cleanup = () => {
|
|
17
|
-
sock.removeAllListeners();
|
|
18
|
-
sock.destroy();
|
|
19
|
-
};
|
|
20
|
-
const timer = setTimeout(() => { cleanup(); resolve(false); }, timeoutMs);
|
|
21
|
-
sock.once('connect', () => { clearTimeout(timer); cleanup(); resolve(true); });
|
|
22
|
-
sock.once('error', () => { clearTimeout(timer); cleanup(); resolve(false); });
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Local-port listeners we refuse to attach through. These forward CDP traffic
|
|
27
|
-
* to a remote host — silently using them would let a `cdp://127.0.0.1:N`
|
|
28
|
-
* profile drive a browser on a different machine without the caller realizing.
|
|
29
|
-
*/
|
|
30
|
-
const TUNNEL_PROCESS_NAMES = new Set(['ssh', 'autossh', 'mosh-client', 'socat']);
|
|
31
|
-
function isTunnelProcess(command) {
|
|
32
|
-
return TUNNEL_PROCESS_NAMES.has(command.toLowerCase());
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Arc is single-instance: relaunching the Arc binary with a fresh
|
|
36
|
-
* `--user-data-dir` does not create a second debuggable process — macOS routes
|
|
37
|
-
* the launch to the running Arc, which was started with no debug port, so the
|
|
38
|
-
* spawn produces a stray window and no CDP endpoint (issue #2779). agents
|
|
39
|
-
* browser therefore ATTACHES to the user's running Arc and never launches its
|
|
40
|
-
* own. When the running Arc exposes no CDP endpoint on the profile's port, we
|
|
41
|
-
* fail loud with the one relaunch that fixes it rather than silently spawning a
|
|
42
|
-
* duplicate. Exported so the contract is unit-testable without a real Arc.
|
|
43
|
-
*/
|
|
44
|
-
export function arcAttachRequiredError(profileName, port) {
|
|
45
|
-
return new Error(`Arc is not exposing a CDP endpoint on cdp://127.0.0.1:${port} for profile ` +
|
|
46
|
-
`"${profileName}". Arc is single-instance — agents browser attaches to your ` +
|
|
47
|
-
`RUNNING Arc and never launches a second one, so it will not start an isolated ` +
|
|
48
|
-
`Arc for you. Quit Arc, then relaunch it with remote debugging on this port:\n` +
|
|
49
|
-
` open -a Arc --args --remote-debugging-port=${port}\n` +
|
|
50
|
-
`and retry. The port must match the profile's endpoint (\`agents browser profiles list\`).`);
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* The loud error an ATTACH-ONLY profile raises when nothing debuggable is on its
|
|
54
|
-
* port (PHNX-3967). Same contract as {@link arcAttachRequiredError}, generalized
|
|
55
|
-
* to any browser: agents attach to a browser the user already started and NEVER
|
|
56
|
-
* spawn a rival window, so instead of falling through to `launchBrowser` (which
|
|
57
|
-
* would produce the second, logged-out dock tile this ticket set out to end) we
|
|
58
|
-
* fail loud with the exact relaunch that makes the canonical instance
|
|
59
|
-
* attachable — pinned to the profile's DURABLE `--user-data-dir` so the one-time
|
|
60
|
-
* sign-in persists. Exported so the contract is unit-testable without a browser.
|
|
61
|
-
*/
|
|
62
|
-
export function attachOnlyRequiredError(profile, port) {
|
|
63
|
-
if (profile.browser === 'arc')
|
|
64
|
-
return arcAttachRequiredError(profile.name, port);
|
|
65
|
-
const app = profile.browser === 'comet' ? 'Comet' : profile.browser;
|
|
66
|
-
const dataDir = resolveProfileDataDir(profile);
|
|
67
|
-
return new Error(`Profile "${profile.name}" is attach-only and nothing is serving the Chrome ` +
|
|
68
|
-
`DevTools Protocol on cdp://127.0.0.1:${port}. agents browser attaches to the ` +
|
|
69
|
-
`${app} you already started and never launches a second one, so it will not spawn ` +
|
|
70
|
-
`a rival window (that is the duplicate, logged-out tile this profile exists to ` +
|
|
71
|
-
`prevent). Launch the canonical ${app} with remote debugging on its durable data dir:\n` +
|
|
72
|
-
` open -a ${app} --args --remote-debugging-port=${port} --user-data-dir=${dataDir}\n` +
|
|
73
|
-
`and retry. Keep signing in once in that window — the data dir is durable, so the ` +
|
|
74
|
-
`login survives quit+relaunch.`);
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* The loud error an attach-only profile raises when a browser IS serving CDP on
|
|
78
|
-
* its port but is a FOREIGN instance — its `--user-data-dir` is not the profile's
|
|
79
|
-
* durable dir (PHNX-3967). This closes the port-squat: a logged-out `/tmp/...`
|
|
80
|
-
* Comet answering CDP on the canonical port passes the browser-FAMILY identity
|
|
81
|
-
* check, so without an ownership check agents would drive it as if it were the
|
|
82
|
-
* credentialed browser. Rather than adopt the squatter we reject it and name the
|
|
83
|
-
* fix. Exported for unit testing.
|
|
84
|
-
*/
|
|
85
|
-
export function foreignInstanceError(profile, port, runningDataDir, pid) {
|
|
86
|
-
const expected = resolveProfileDataDir(profile);
|
|
87
|
-
const app = profile.browser === 'comet' ? 'Comet' : profile.browser;
|
|
88
|
-
return new Error(`Attach-only ownership check failed for profile "${profile.name}" on ` +
|
|
89
|
-
`cdp://127.0.0.1:${port}: a ${app} is serving CDP there (pid ${pid}) but it is ` +
|
|
90
|
-
`running a FOREIGN user-data-dir\n` +
|
|
91
|
-
` running: ${runningDataDir}\n` +
|
|
92
|
-
` expected: ${expected}\n` +
|
|
93
|
-
`so it is not this profile's credentialed browser. agents will not drive it. Close ` +
|
|
94
|
-
`that instance (\`kill ${pid}\`), then relaunch the canonical ${app}:\n` +
|
|
95
|
-
` open -a ${app} --args --remote-debugging-port=${port} --user-data-dir=${expected}`);
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* The loud error a profile pinned to the owner's own store (PHNX-4042) raises
|
|
99
|
-
* when a browser already holds that store without a debug port. Launching on it
|
|
100
|
-
* would not open a second browser: Chromium hands the arguments to the running
|
|
101
|
-
* instance and exits, the port never binds, and the agent would wait out a
|
|
102
|
-
* timeout while the owner sees a stray window. Exported for unit testing.
|
|
103
|
-
*/
|
|
104
|
-
export function storeInUseError(profile, port, pid) {
|
|
105
|
-
const app = profile.browser === 'comet' ? 'Comet' : profile.browser;
|
|
106
|
-
const dataDir = resolveProfileDataDir(profile);
|
|
107
|
-
return new Error(`Profile "${profile.name}" is pinned to ${dataDir}, and a ${app} (pid ${pid}) already has that ` +
|
|
108
|
-
`store open without remote debugging, so nothing serves the Chrome DevTools Protocol on ` +
|
|
109
|
-
`cdp://127.0.0.1:${port}. agents browser will not launch a second ${app} on the same store. ` +
|
|
110
|
-
`Quit that ${app}, then relaunch it with remote debugging:\n` +
|
|
111
|
-
` ${storeRelaunchCommand(profile.browser, port, dataDir, profile.profileDirectory)}\n` +
|
|
112
|
-
`and retry. Your logins stay: the store is the one you already use.`);
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Prefix every ownership-rejection message carries, so `connectLocal`'s catch can
|
|
116
|
-
* re-throw it verbatim instead of misreading it as "attach failed, launch fresh".
|
|
117
|
-
*/
|
|
118
|
-
const OWNERSHIP_REJECTION_PREFIX = 'Attach-only ownership check failed';
|
|
119
|
-
/**
|
|
120
|
-
* Verify the browser serving CDP on `port` belongs to this attach-only profile,
|
|
121
|
-
* by comparing its live `--user-data-dir` to the profile's durable dir
|
|
122
|
-
* (PHNX-3967). No-op for a `launch`-policy profile. When an occupant IS present
|
|
123
|
-
* but its data dir can't be read, fail LOUD rather than open: a canonical Comet
|
|
124
|
-
* this profile is meant to attach to was launched by the user with the durable
|
|
125
|
-
* `--user-data-dir` and is readable via `ps`, so an unreadable occupant is the
|
|
126
|
-
* suspicious case, not a legitimate one — driving it would be the exact
|
|
127
|
-
* port-squat this guard exists to stop.
|
|
128
|
-
*/
|
|
129
|
-
function verifyEndpointOwnership(profile, port) {
|
|
130
|
-
// Attach-only profiles and profiles pinned to the owner's own store
|
|
131
|
-
// (PHNX-4042) both name the exact user-data dir a running instance must have.
|
|
132
|
-
if (!isAttachOnlyProfile(profile) && !profile.userDataDir)
|
|
133
|
-
return;
|
|
134
|
-
// Arc is the user's single running instance under its own default data dir; it
|
|
135
|
-
// has no managed durable dir to compare against and no /tmp-squat vector.
|
|
136
|
-
if (profile.browser === 'arc')
|
|
137
|
-
return;
|
|
138
|
-
const occupant = getPortOccupant(port);
|
|
139
|
-
if (!occupant)
|
|
140
|
-
return;
|
|
141
|
-
const expected = resolveProfileDataDir(profile);
|
|
142
|
-
const runningDataDir = getProcessUserDataDir(occupant.pid);
|
|
143
|
-
if (!runningDataDir) {
|
|
144
|
-
throw new Error(`${OWNERSHIP_REJECTION_PREFIX} for profile "${profile.name}" on cdp://127.0.0.1:${port}: ` +
|
|
145
|
-
`a process (pid ${occupant.pid}) is serving CDP there but its --user-data-dir could not be ` +
|
|
146
|
-
`read, so ownership can't be confirmed. Refusing to attach to an unverified instance. If ` +
|
|
147
|
-
`this is your canonical browser, relaunch it with --user-data-dir=${expected} so the ` +
|
|
148
|
-
`attach-only guard can verify it.`);
|
|
149
|
-
}
|
|
150
|
-
if (normalizeDataDir(runningDataDir) !== normalizeDataDir(expected)) {
|
|
151
|
-
throw foreignInstanceError(profile, port, runningDataDir, occupant.pid);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
export async function connectLocal(endpoint, profile,
|
|
155
|
-
/**
|
|
156
|
-
* Runtime key (`<profile>@<endpoint>`) the launched browser's user-data-dir,
|
|
157
|
-
* pid, and port files are stored under. Separate from `profile.name`, which
|
|
158
|
-
* stays the bare user-facing name and appears only in messages (RUSH-2709).
|
|
159
|
-
*/
|
|
160
|
-
key) {
|
|
161
|
-
const url = new URL(endpoint);
|
|
162
|
-
if (url.protocol !== 'cdp:') {
|
|
163
|
-
throw new Error(`Invalid local endpoint: ${endpoint}`);
|
|
164
|
-
}
|
|
165
|
-
// Share the parser with the SSH driver and the collision-detection code
|
|
166
|
-
// path so `cdp://host:N` and `cdp://host?port=N` behave identically.
|
|
167
|
-
const parsed = parseEndpointUrl(endpoint);
|
|
168
|
-
const port = parsed?.port ?? 9222;
|
|
169
|
-
// Refuse to attach through an SSH tunnel before we even try to speak CDP.
|
|
170
|
-
// `verifyBrowserIdentity` only inspects what comes back over the wire — it
|
|
171
|
-
// can't tell whether the browser actually lives on this machine or on the
|
|
172
|
-
// far end of an `ssh -L` tunnel. A stale tunnel from a prior session
|
|
173
|
-
// (common when an SSH-driven profile is deleted before the daemon exits)
|
|
174
|
-
// will silently hijack any "local" profile bound to the same port.
|
|
175
|
-
const preOccupant = getPortOccupant(port);
|
|
176
|
-
if (preOccupant && isTunnelProcess(preOccupant.command)) {
|
|
177
|
-
throw new Error(`Port ${port} is held by ${preOccupant.command} (pid ${preOccupant.pid}), an SSH ` +
|
|
178
|
-
`tunnel forwarding to a remote host. Profile "${profile.name}" is configured as ` +
|
|
179
|
-
`local (${endpoint}) but traffic would round-trip to another machine. Either kill ` +
|
|
180
|
-
`the tunnel (\`kill ${preOccupant.pid}\`) and retry, or switch the profile to an ` +
|
|
181
|
-
`ssh:// endpoint to drive the remote browser explicitly.`);
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
const { wsUrl, browser } = await discoverBrowserWsUrl(port, 'localhost', profile.name);
|
|
185
|
-
verifyBrowserIdentity(browser, profile.browser, port);
|
|
186
|
-
// Ownership beyond browser FAMILY (PHNX-3967): a foreign /tmp Comet answers
|
|
187
|
-
// CDP on the canonical port and passes verifyBrowserIdentity, so before we
|
|
188
|
-
// adopt the endpoint confirm its --user-data-dir is this profile's durable
|
|
189
|
-
// dir. Rejects the port-squatter loudly instead of driving it.
|
|
190
|
-
verifyEndpointOwnership(profile, port);
|
|
191
|
-
const cdp = new CDPClient();
|
|
192
|
-
await cdp.connect(wsUrl);
|
|
193
|
-
return { cdp, port, pid: 0 };
|
|
194
|
-
}
|
|
195
|
-
catch (err) {
|
|
196
|
-
if (err instanceof Error && err.message.startsWith('Browser identity mismatch')) {
|
|
197
|
-
throw err;
|
|
198
|
-
}
|
|
199
|
-
// An ownership rejection is a definitive answer — never fall through to a
|
|
200
|
-
// fresh launch or a generic port message. Re-throw it verbatim.
|
|
201
|
-
if (err instanceof Error && err.message.startsWith(OWNERSHIP_REJECTION_PREFIX)) {
|
|
202
|
-
throw err;
|
|
203
|
-
}
|
|
204
|
-
// An attach-only profile reached the catch, so the attach above failed:
|
|
205
|
-
// nothing debuggable is on this port. Never fall through to launchBrowser —
|
|
206
|
-
// that would spawn the duplicate/stray-window this ticket set out to end
|
|
207
|
-
// (Arc: #2779, PHNX-2399; Comet: PHNX-3967). Fail loud with the relaunch that
|
|
208
|
-
// makes the canonical instance attachable.
|
|
209
|
-
if (isAttachOnlyProfile(profile)) {
|
|
210
|
-
throw attachOnlyRequiredError(profile, port);
|
|
211
|
-
}
|
|
212
|
-
// Distinguish "nothing listening on this port" (fine to launch fresh) from
|
|
213
|
-
// "something is listening but it's not a debuggable browser" (bail loudly —
|
|
214
|
-
// silently launching on a different port leads to confusing `pid 0` and
|
|
215
|
-
// `CDP connection not open` errors downstream).
|
|
216
|
-
const occupant = getPortOccupant(port);
|
|
217
|
-
if (occupant) {
|
|
218
|
-
throw new Error(`Port ${port} is occupied by ${occupant.command} (pid ${occupant.pid}) but is ` +
|
|
219
|
-
`not serving the Chrome DevTools Protocol. Either stop that process ` +
|
|
220
|
-
`(\`kill ${occupant.pid}\`) or restart it with \`--remote-debugging-port=${port}\` ` +
|
|
221
|
-
`so profile "${profile.name}" can attach.`);
|
|
222
|
-
}
|
|
223
|
-
// lsof-based detection misses some Electron-family processes (Comet, custom
|
|
224
|
-
// chrome wrappers). Cheap TCP probe as a safety net: if something ACKs a
|
|
225
|
-
// connect, the port is bound — bail loudly with the profile name + endpoint
|
|
226
|
-
// rather than silently launching a duplicate browser.
|
|
227
|
-
if (await probeTcpBound(port)) {
|
|
228
|
-
throw new Error(`Profile "${profile.name}" is configured for cdp://127.0.0.1:${port}, ` +
|
|
229
|
-
`but something is already bound to that port without serving the Chrome ` +
|
|
230
|
-
`DevTools Protocol. If that's your browser running without remote debugging, ` +
|
|
231
|
-
`quit it and relaunch with \`--remote-debugging-port=${port}\`. Otherwise, ` +
|
|
232
|
-
`update the profile to a free port (\`agents browser profiles list\`).`);
|
|
233
|
-
}
|
|
234
|
-
// A store-pinned profile launches on the owner's store only while nothing
|
|
235
|
-
// holds it; a browser already open there without a port must be relaunched
|
|
236
|
-
// by the owner, never doubled (PHNX-4042).
|
|
237
|
-
if (profile.userDataDir) {
|
|
238
|
-
const holder = storeOccupant(profile.userDataDir);
|
|
239
|
-
if (holder)
|
|
240
|
-
throw storeInUseError(profile, port, holder.pid);
|
|
241
|
-
}
|
|
242
|
-
const newPort = port;
|
|
243
|
-
const chromeOpts = {
|
|
244
|
-
...profile.chrome,
|
|
245
|
-
viewport: profile.viewport,
|
|
246
|
-
// A discovered native profile launches the owner's browser on its own
|
|
247
|
-
// store: one window for the owner and agents, logins intact (PHNX-4042).
|
|
248
|
-
...(profile.userDataDir
|
|
249
|
-
? { userDataDir: profile.userDataDir, profileDirectory: profile.profileDirectory }
|
|
250
|
-
: {}),
|
|
251
|
-
};
|
|
252
|
-
let launched;
|
|
253
|
-
try {
|
|
254
|
-
launched = await launchBrowser(key, profile.browser, newPort, chromeOpts, profile.secrets, profile.binary, profile.electron === true);
|
|
255
|
-
}
|
|
256
|
-
catch (launchErr) {
|
|
257
|
-
const reason = launchErr instanceof Error ? launchErr.message : String(launchErr);
|
|
258
|
-
throw new Error(`Could not start ${profile.browser} for profile "${profile.name}" on port ${newPort}: ${reason}. ` +
|
|
259
|
-
`Check that the browser binary is installed (\`agents browser profiles list\`) and ` +
|
|
260
|
-
`that no other process is holding the port.`);
|
|
261
|
-
}
|
|
262
|
-
const cdp = new CDPClient();
|
|
263
|
-
await cdp.connect(launched.wsUrl);
|
|
264
|
-
return { cdp, port: newPort, pid: launched.pid };
|
|
265
|
-
}
|
|
266
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { CDPClient } from '../cdp.js';
|
|
2
|
-
import type { BrowserProfile, ConnectionKey } from '../types.js';
|
|
3
|
-
import { shellQuote } from '../../ssh-exec.js';
|
|
4
|
-
export { shellQuote };
|
|
5
|
-
export interface SSHConnection {
|
|
6
|
-
cdp: CDPClient;
|
|
7
|
-
port: number;
|
|
8
|
-
pid: number;
|
|
9
|
-
cleanup: () => void;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Which shell dialect the *remote* host speaks. Selected per-endpoint via the
|
|
13
|
-
* `&os=windows` query param on an `ssh://` target. POSIX is the default — the
|
|
14
|
-
* historical behavior for macOS/Linux remotes. Windows remotes run OpenSSH
|
|
15
|
-
* Server with cmd.exe as the default shell, so the launch/teardown command
|
|
16
|
-
* strings differ (no `&` backgrounding, no `lsof`, `.exe` instead of `.app`).
|
|
17
|
-
*/
|
|
18
|
-
type RemoteOs = 'windows' | 'posix';
|
|
19
|
-
interface SSHConnectOptions {
|
|
20
|
-
/**
|
|
21
|
-
* Attach to a browser another device already declared (identity-bearing or
|
|
22
|
-
* a fungible name we don't declare here). Do not launch a fresh remote
|
|
23
|
-
* chrome-data, and do not kill that browser when the tunnel closes — it
|
|
24
|
-
* holds the logins the caller is trying to reach.
|
|
25
|
-
*/
|
|
26
|
-
persistRemote?: boolean;
|
|
27
|
-
}
|
|
28
|
-
/** Lifecycle for a registry-driven tunnel vs a profile the daemon launched. */
|
|
29
|
-
export declare function persistRemoteLifecycle(persistRemote: boolean): {
|
|
30
|
-
launchRemote: boolean;
|
|
31
|
-
killOnCleanup: boolean;
|
|
32
|
-
};
|
|
33
|
-
export declare function connectSSH(endpoint: string, profile: BrowserProfile,
|
|
34
|
-
/**
|
|
35
|
-
* Runtime key (`<profile>@<device>`) this connection is stored under. It
|
|
36
|
-
* keys the tunnel's runtime record; `profile.name` is the bare, user-facing
|
|
37
|
-
* name and appears only in messages (RUSH-2709).
|
|
38
|
-
*/
|
|
39
|
-
key: ConnectionKey, opts?: SSHConnectOptions): Promise<SSHConnection>;
|
|
40
|
-
/**
|
|
41
|
-
* Wrap a PowerShell script as a `-EncodedCommand` invocation. Base64 of the
|
|
42
|
-
* UTF-16LE bytes is a single quote-free token, so it rides through Node spawn
|
|
43
|
-
* → Windows sshd → cmd.exe with zero escaping hazards (hand-quoted
|
|
44
|
-
* `powershell -Command "…"` is fragile the moment a path or URL is involved).
|
|
45
|
-
*/
|
|
46
|
-
export declare function encodePowerShell(script: string): string;
|
|
47
|
-
/**
|
|
48
|
-
* The PowerShell that launches the browser on a Windows remote. Three hard
|
|
49
|
-
* requirements shaped this:
|
|
50
|
-
* 1. The browser must OUTLIVE the ssh session. Windows OpenSSH terminates
|
|
51
|
-
* the session's job tree on disconnect, which reaps both `start /B` and
|
|
52
|
-
* `Start-Process` children (verified against a real box). A scheduled
|
|
53
|
-
* task runs under the Task Scheduler service, so the process survives
|
|
54
|
-
* after we drop the ssh connection and reconnect over the CDP tunnel.
|
|
55
|
-
* 2. The browser must run in the user's INTERACTIVE session. The previous
|
|
56
|
-
* WMI `Win32_Process.Create` launch also survived disconnect, but it
|
|
57
|
-
* spawned Edge in session 0 (the services session): the CDP socket bound
|
|
58
|
-
* and accepted, yet the DevTools server never initialized — every
|
|
59
|
-
* /json/version probe hung forever and DevToolsActivePort was never
|
|
60
|
-
* written (verified against a real box, Edg/150). A scheduled task
|
|
61
|
-
* registered and started by the logged-on user runs in that user's
|
|
62
|
-
* interactive session, where DevTools comes up normally.
|
|
63
|
-
* 3. A distinct `--user-data-dir` so a fresh instance bound to the debugging
|
|
64
|
-
* port comes up even when the user already has Edge open.
|
|
65
|
-
* CreateProcess ignores App Paths, so we resolve the real `.exe` from the
|
|
66
|
-
* registry at runtime rather than relying on a bare `msedge` name. The task is
|
|
67
|
-
* unregistered immediately after start — the browser keeps running; nothing
|
|
68
|
-
* lingers in the scheduler.
|
|
69
|
-
*/
|
|
70
|
-
export declare function buildWindowsLaunchScript(browserType: string, port: number, customBinary?: string): string;
|
|
71
|
-
/**
|
|
72
|
-
* The PowerShell that kills whatever holds the CDP port on a Windows remote.
|
|
73
|
-
* Tree-kill (`taskkill /T`), not `Stop-Process`: Chromium is multi-process, and
|
|
74
|
-
* killing only the port-owning main process orphans its children. The orphans
|
|
75
|
-
* keep the profile's SingletonLock, so every later launch against the same
|
|
76
|
-
* `--user-data-dir` delegates to the zombie tree, exits, and never binds the
|
|
77
|
-
* CDP port — `browser start --device` then fails until someone hand-cleans the
|
|
78
|
-
* box (found live on win-mini by the #561 e2e suite).
|
|
79
|
-
*/
|
|
80
|
-
export declare function buildWindowsKillScript(port: number): string;
|
|
81
|
-
/**
|
|
82
|
-
* Build the remote command that launches the browser detached with a CDP port.
|
|
83
|
-
* POSIX backgrounds the `.app` binary with `… &`; Windows resolves the exe and
|
|
84
|
-
* starts it via an interactive one-shot scheduled task (encoded PowerShell)
|
|
85
|
-
* so it survives the ssh session AND serves CDP (session 0 never does).
|
|
86
|
-
*/
|
|
87
|
-
export declare function buildLaunchCmd(remoteOs: RemoteOs, browserType: string, port: number, customBinary?: string): string;
|
|
88
|
-
/**
|
|
89
|
-
* Build the remote command that kills whatever holds the CDP port.
|
|
90
|
-
* POSIX uses `lsof`+`kill`; Windows uses encoded PowerShell
|
|
91
|
-
* (Get-NetTCPConnection → Stop-Process).
|
|
92
|
-
*/
|
|
93
|
-
export declare function buildKillCmd(remoteOs: RemoteOs, port: number): string;
|
|
94
|
-
export declare function ensureRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
|
|
95
|
-
/**
|
|
96
|
-
* Kill the remote browser holding the CDP port. Invoked on stop/cleanup so the
|
|
97
|
-
* task-launched (Windows) / detached (posix) browser process is not orphaned when
|
|
98
|
-
* the ssh tunnel is torn down — killing only the tunnel left it running on
|
|
99
|
-
* win-mini after every `browser stop`. Best-effort: never rejects.
|
|
100
|
-
*/
|
|
101
|
-
export declare function killRemoteBrowser(user: string, host: string, remoteOs: RemoteOs, port: number): Promise<void>;
|
|
102
|
-
/**
|
|
103
|
-
* Identify whether a pid listening on our target local port is an SSH
|
|
104
|
-
* tunnel WE would have spawned for `host:remotePort`. Used so that two
|
|
105
|
-
* agents-browser invocations of the same SSH profile share a tunnel
|
|
106
|
-
* rather than failing the second one with "port in use".
|
|
107
|
-
*
|
|
108
|
-
* Best-effort match against the ssh -L command line via `ps`. If we
|
|
109
|
-
* can't read the cmd or the args don't look like ours, treat as not-ours.
|
|
110
|
-
*
|
|
111
|
-
* Windows has no `ps`, so the POSIX branch below always throws there and every
|
|
112
|
-
* reuse check returned false — the second invocation to the same host:port then
|
|
113
|
-
* failed "port in use". On win32 we instead reuse the netstat -ano + tasklist
|
|
114
|
-
* occupant lookup (getPortOccupant): the tunnel binds `remotePort` locally
|
|
115
|
-
* (localPort === remotePort in the caller), so the pid holding that port is our
|
|
116
|
-
* ssh.exe. tasklist only exposes the image name, not the full command line, so
|
|
117
|
-
* we can't match host/remotePort as tightly as the POSIX branch — an ssh
|
|
118
|
-
* process on our exact local port is our tunnel in practice.
|
|
119
|
-
*/
|
|
120
|
-
export declare function isOwnTunnel(pid: number, host: string, remotePort: number): boolean;
|