@phnx-labs/agents-cli 1.22.46 → 1.22.49
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 +663 -0
- package/LICENSE +102 -182
- package/README.md +96 -49
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +243 -25
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +711 -62
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +26 -24
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +107 -53
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/focus.d.ts +14 -0
- package/dist/commands/focus.js +48 -5
- package/dist/commands/go.d.ts +3 -0
- package/dist/commands/go.js +16 -3
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +7 -6
- package/dist/commands/mailboxes.js +4 -3
- package/dist/commands/menubar.js +11 -4
- package/dist/commands/projects.d.ts +16 -1
- package/dist/commands/projects.js +58 -64
- package/dist/commands/resume.js +1 -1
- package/dist/commands/route.js +4 -4
- package/dist/commands/routines.js +4 -3
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +5 -0
- package/dist/commands/setup-secrets.js +23 -0
- package/dist/commands/share.d.ts +20 -5
- package/dist/commands/share.js +152 -52
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +243 -18
- package/dist/commands/teams.js +34 -2
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +32 -62
- package/dist/commands/watchdog.js +9 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +84 -6
- package/dist/lib/accounting/usage.js +293 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +25 -15
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +40 -9
- package/dist/lib/browser/profiles.d.ts +104 -65
- package/dist/lib/browser/profiles.js +277 -165
- package/dist/lib/browser/registry.d.ts +39 -0
- package/dist/lib/browser/registry.js +149 -0
- package/dist/lib/browser/remote-control.d.ts +28 -2
- package/dist/lib/browser/remote-control.js +35 -3
- package/dist/lib/browser/resolve-target.d.ts +68 -0
- package/dist/lib/browser/resolve-target.js +250 -0
- package/dist/lib/browser/runtime-state.d.ts +44 -11
- package/dist/lib/browser/runtime-state.js +98 -15
- package/dist/lib/browser/service.d.ts +65 -9
- package/dist/lib/browser/service.js +196 -61
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/byok-usage.js +2 -2
- package/dist/lib/claude-statusline.d.ts +29 -0
- package/dist/lib/claude-statusline.js +139 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +1 -1
- package/dist/lib/cloud/rush.js +1 -1
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/computer/computer-rpc.js +2 -2
- package/dist/lib/computer/download.d.ts +15 -15
- package/dist/lib/computer/download.js +35 -98
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
- package/dist/lib/computer/ssh-tunnel.js +12 -22
- package/dist/lib/config-keys.d.ts +6 -11
- package/dist/lib/config-keys.js +18 -19
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +2 -2
- package/dist/lib/crabbox/lease.d.ts +9 -0
- package/dist/lib/crabbox/lease.js +70 -15
- package/dist/lib/crabbox/runtimes.js +1 -1
- package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
- package/dist/lib/daemon/account-state-daemon-service.js +26 -0
- package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
- package/dist/lib/daemon/browser-ipc-service.js +46 -0
- package/dist/lib/daemon/daemon.d.ts +3 -0
- package/dist/lib/daemon/daemon.js +212 -444
- package/dist/lib/daemon/device-probe-service.d.ts +26 -0
- package/dist/lib/daemon/device-probe-service.js +47 -0
- package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
- package/dist/lib/daemon/keychain-reap-service.js +32 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
- package/dist/lib/daemon/monitor-engine-service.js +29 -0
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
- package/dist/lib/daemon/secrets-broker-service.js +66 -0
- package/dist/lib/daemon/self-heal-service.d.ts +27 -0
- package/dist/lib/daemon/self-heal-service.js +48 -0
- package/dist/lib/daemon/service.d.ts +102 -0
- package/dist/lib/daemon/service.js +98 -0
- package/dist/lib/daemon/session-index-service.d.ts +22 -0
- package/dist/lib/daemon/session-index-service.js +40 -0
- package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
- package/dist/lib/daemon/state-dir-check-service.js +63 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +298 -0
- package/dist/lib/daemon/watchdog-service.d.ts +19 -0
- package/dist/lib/daemon/watchdog-service.js +40 -0
- package/dist/lib/daemon-health.d.ts +15 -0
- package/dist/lib/daemon-health.js +29 -5
- package/dist/lib/daemon-services.d.ts +8 -1
- package/dist/lib/daemon-services.js +55 -0
- package/dist/lib/daemon-ticks.d.ts +13 -2
- package/dist/lib/daemon-ticks.js +22 -20
- package/dist/lib/device-config.d.ts +25 -10
- package/dist/lib/device-config.js +90 -39
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +26 -0
- package/dist/lib/devices/stats-cache.js +59 -7
- package/dist/lib/devices/worker-pick.d.ts +48 -0
- package/dist/lib/devices/worker-pick.js +113 -0
- package/dist/lib/exec.d.ts +70 -14
- package/dist/lib/exec.js +110 -38
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/harness/adapters/cursor.js +8 -11
- package/dist/lib/helper-download.d.ts +108 -0
- package/dist/lib/helper-download.js +194 -0
- package/dist/lib/helper-versions.d.ts +52 -0
- package/dist/lib/helper-versions.js +58 -0
- package/dist/lib/hooks/cache.js +19 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/credentials.d.ts +1 -1
- package/dist/lib/hosts/credentials.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +7 -1
- package/dist/lib/hosts/dispatch.js +24 -5
- package/dist/lib/hosts/passthrough.d.ts +8 -15
- package/dist/lib/hosts/passthrough.js +35 -16
- package/dist/lib/hosts/providers/devices.d.ts +1 -1
- package/dist/lib/hosts/providers/devices.js +1 -1
- package/dist/lib/hosts/reconnect.d.ts +179 -21
- package/dist/lib/hosts/reconnect.js +266 -45
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/installations/migrate.d.ts +4 -5
- package/dist/lib/installations/migrate.js +6 -7
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +23 -13
- package/dist/lib/installations/versions.js +25 -2
- package/dist/lib/linear-autoclose.d.ts +1 -1
- package/dist/lib/linear-autoclose.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/menubar/download-menubar.d.ts +47 -0
- package/dist/lib/menubar/download-menubar.js +60 -0
- package/dist/lib/menubar/install-menubar.d.ts +44 -7
- package/dist/lib/menubar/install-menubar.js +105 -23
- package/dist/lib/menubar/notify-desktop.d.ts +3 -3
- package/dist/lib/menubar/notify-desktop.js +6 -6
- package/dist/lib/model-tiers.d.ts +1 -1
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/project-pull.d.ts +1 -1
- package/dist/lib/project-pull.js +2 -2
- package/dist/lib/projects.d.ts +1 -1
- package/dist/lib/projects.js +1 -1
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/sandbox.js +6 -7
- package/dist/lib/secrets/download-keychain.d.ts +47 -0
- package/dist/lib/secrets/download-keychain.js +70 -0
- package/dist/lib/secrets/install-helper.d.ts +20 -1
- package/dist/lib/secrets/install-helper.js +44 -3
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +86 -2
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +142 -8
- package/dist/lib/session/discover.d.ts +23 -1
- package/dist/lib/session/discover.js +44 -10
- package/dist/lib/session/highlights.d.ts +49 -1
- package/dist/lib/session/highlights.js +95 -0
- package/dist/lib/session/host-link.d.ts +16 -2
- package/dist/lib/session/host-link.js +7 -1
- package/dist/lib/session/parse.js +33 -1
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/remote-list.d.ts +9 -0
- package/dist/lib/session/remote/remote-list.js +19 -2
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +17 -3
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +93 -1
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/tool-calls.d.ts +2 -0
- package/dist/lib/session/tool-calls.js +1 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +19 -0
- package/dist/lib/session/trajectory-html.js +770 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +284 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +384 -0
- package/dist/lib/session/types.d.ts +15 -0
- package/dist/lib/sha256-asset.d.ts +26 -0
- package/dist/lib/sha256-asset.js +41 -0
- package/dist/lib/share/backend.d.ts +95 -0
- package/dist/lib/share/backend.js +139 -0
- package/dist/lib/share/delete.d.ts +6 -2
- package/dist/lib/share/delete.js +8 -9
- package/dist/lib/share/html.d.ts +22 -0
- package/dist/lib/share/html.js +88 -0
- package/dist/lib/share/provision.d.ts +18 -4
- package/dist/lib/share/provision.js +38 -4
- package/dist/lib/share/publish.d.ts +39 -15
- package/dist/lib/share/publish.js +55 -26
- package/dist/lib/share/worker-template.js +171 -11
- package/dist/lib/sqlite.js +16 -3
- package/dist/lib/ssh-exec.d.ts +65 -0
- package/dist/lib/ssh-exec.js +120 -4
- package/dist/lib/startup/command-registry.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +6 -3
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +2 -2
- package/dist/lib/state.js +11 -5
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/terminal/types.js +1 -1
- package/dist/lib/tmux/orphan-reap.js +1 -1
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +204 -27
- package/dist/lib/traces/backend.d.ts +29 -0
- package/dist/lib/traces/backend.js +46 -0
- package/dist/lib/traces/classify.d.ts +52 -0
- package/dist/lib/traces/classify.js +72 -0
- package/dist/lib/traces/config.d.ts +10 -0
- package/dist/lib/traces/config.js +2 -0
- package/dist/lib/traces/provision.d.ts +19 -0
- package/dist/lib/traces/provision.js +30 -0
- package/dist/lib/traces/sync.d.ts +150 -0
- package/dist/lib/traces/sync.js +447 -0
- package/dist/lib/traces/worker-template.d.ts +4 -0
- package/dist/lib/traces/worker-template.js +159 -0
- package/dist/lib/types.d.ts +34 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +18 -4
- package/dist/lib/usage-refresh.js +68 -19
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +5 -7
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/usage-fleet.d.ts +0 -32
- package/dist/lib/usage-fleet.js +0 -125
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import * as yaml from 'yaml';
|
|
5
|
+
import { getUserAgentsDir, readMeta, updateMeta } from '../state.js';
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
/** Leftover central `browser:` map from before per-device declarations. Empty when none remain. */
|
|
10
|
+
export function centralBrowserProfiles() {
|
|
11
|
+
const central = readMeta().browser;
|
|
12
|
+
if (!central || Object.keys(central).length === 0)
|
|
13
|
+
return {};
|
|
14
|
+
return { ...central };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Fold leftover central `browser:` entries into THIS device's declaration file.
|
|
18
|
+
*
|
|
19
|
+
* Never called implicitly, and that is the point. Every device can read the
|
|
20
|
+
* central map, so an implicit claim races: whichever box happens to read first
|
|
21
|
+
* claims the name, {@link profileKind} then reports it `identity`, and the
|
|
22
|
+
* daemon tunnels to that box — which for a fleet-wide `cdp://localhost:*`
|
|
23
|
+
* profile is a logged-out headless browser wearing a credentialed browser's
|
|
24
|
+
* name. That is the exact bug this module exists to remove, and an implicit
|
|
25
|
+
* migration would write it to disk as a stored fact.
|
|
26
|
+
*
|
|
27
|
+
* The claim is an explicit operator action (`agents browser profiles claim`),
|
|
28
|
+
* run on the machine that actually owns the browser. `canHostHere` is supplied
|
|
29
|
+
* by the command layer so this module stays a leaf — it must not import
|
|
30
|
+
* `isProfileLaunchableHere` (that would cycle through chrome.ts → profiles.ts).
|
|
31
|
+
* Only profiles this machine can host are claimed; the rest stay central,
|
|
32
|
+
* undeclared, and fail loudly on resolve.
|
|
33
|
+
*/
|
|
34
|
+
export function migrateCentralBrowserProfiles(canHostHere, name) {
|
|
35
|
+
const meta = readMeta();
|
|
36
|
+
const central = meta.browser;
|
|
37
|
+
if (!central || Object.keys(central).length === 0) {
|
|
38
|
+
if (name) {
|
|
39
|
+
throw new Error(`No leftover central browser profile named "${name}".`);
|
|
40
|
+
}
|
|
41
|
+
return { claimed: [], skipped: [] };
|
|
42
|
+
}
|
|
43
|
+
const local = meta.deviceBrowser ?? {};
|
|
44
|
+
if (name) {
|
|
45
|
+
const config = central[name];
|
|
46
|
+
if (!config) {
|
|
47
|
+
throw new Error(local[name]
|
|
48
|
+
? `Browser profile "${name}" is already declared on this device.`
|
|
49
|
+
: `No leftover central browser profile named "${name}".`);
|
|
50
|
+
}
|
|
51
|
+
if (!canHostHere(config)) {
|
|
52
|
+
throw new Error(`Cannot claim browser profile "${name}": this machine cannot host it ` +
|
|
53
|
+
`(its browser/binary isn't installed here). Run this command on the machine that has that browser.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const toClaim = {};
|
|
57
|
+
const skipped = [];
|
|
58
|
+
for (const [profileName, config] of Object.entries(central)) {
|
|
59
|
+
if (name && profileName !== name)
|
|
60
|
+
continue;
|
|
61
|
+
if (!canHostHere(config)) {
|
|
62
|
+
skipped.push(profileName);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const existing = local[profileName];
|
|
66
|
+
if (existing && !isDeepStrictEqual(existing, config)) {
|
|
67
|
+
throw new Error(`Cannot migrate browser profile "${profileName}": central agents.yaml and this device's ` +
|
|
68
|
+
`agents.yaml declare different configurations. Resolve the duplicate before retrying.`);
|
|
69
|
+
}
|
|
70
|
+
toClaim[profileName] = config;
|
|
71
|
+
}
|
|
72
|
+
const claimed = Object.keys(toClaim).sort();
|
|
73
|
+
skipped.sort();
|
|
74
|
+
if (claimed.length === 0)
|
|
75
|
+
return { claimed, skipped };
|
|
76
|
+
updateMeta((current) => {
|
|
77
|
+
const legacy = current;
|
|
78
|
+
const remaining = {};
|
|
79
|
+
for (const [profileName, config] of Object.entries(legacy.browser ?? {})) {
|
|
80
|
+
if (!(profileName in toClaim))
|
|
81
|
+
remaining[profileName] = config;
|
|
82
|
+
}
|
|
83
|
+
const { browser: _removed, ...withoutCentralBrowser } = legacy;
|
|
84
|
+
return {
|
|
85
|
+
...withoutCentralBrowser,
|
|
86
|
+
...(Object.keys(remaining).length > 0 ? { browser: remaining } : {}),
|
|
87
|
+
deviceBrowser: { ...current.deviceBrowser, ...toClaim },
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
return { claimed, skipped };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Every profile any device declares, keyed by name to all declaring devices.
|
|
94
|
+
* Reads every `devices/<name>/agents.yaml`; declarations never overwrite.
|
|
95
|
+
*/
|
|
96
|
+
export function profileRegistry() {
|
|
97
|
+
const registry = new Map();
|
|
98
|
+
const devicesDir = path.join(getUserAgentsDir(), 'devices');
|
|
99
|
+
if (!fs.existsSync(devicesDir))
|
|
100
|
+
return registry;
|
|
101
|
+
const devices = fs
|
|
102
|
+
.readdirSync(devicesDir, { withFileTypes: true })
|
|
103
|
+
.filter((entry) => entry.isDirectory())
|
|
104
|
+
.map((entry) => entry.name)
|
|
105
|
+
.sort();
|
|
106
|
+
for (const device of devices) {
|
|
107
|
+
const file = path.join(devicesDir, device, 'agents.yaml');
|
|
108
|
+
if (!fs.existsSync(file))
|
|
109
|
+
continue;
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = yaml.parse(fs.readFileSync(file, 'utf8'));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw new Error(`Cannot read browser declarations from ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
116
|
+
}
|
|
117
|
+
if (parsed == null)
|
|
118
|
+
continue;
|
|
119
|
+
if (!isRecord(parsed)) {
|
|
120
|
+
throw new Error(`Cannot read browser declarations from ${file}: document root must be a map.`);
|
|
121
|
+
}
|
|
122
|
+
const browser = parsed.browser;
|
|
123
|
+
if (browser == null)
|
|
124
|
+
continue;
|
|
125
|
+
if (!isRecord(browser)) {
|
|
126
|
+
throw new Error(`Cannot read browser declarations from ${file}: browser must be a map.`);
|
|
127
|
+
}
|
|
128
|
+
for (const [name, config] of Object.entries(browser)) {
|
|
129
|
+
if (!isRecord(config)) {
|
|
130
|
+
throw new Error(`Cannot read browser declaration "${name}" from ${file}: profile must be a map.`);
|
|
131
|
+
}
|
|
132
|
+
const declarations = registry.get(name) ?? [];
|
|
133
|
+
declarations.push({ device, config: config });
|
|
134
|
+
registry.set(name, declarations);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return registry;
|
|
138
|
+
}
|
|
139
|
+
/** Devices declaring `name`, empty when nobody does. */
|
|
140
|
+
export function declaringDevices(name) {
|
|
141
|
+
return (profileRegistry().get(name) ?? []).map((declaration) => declaration.device);
|
|
142
|
+
}
|
|
143
|
+
/** Exactly one declaring device is identity-bearing; several are fungible. */
|
|
144
|
+
export function profileKind(name) {
|
|
145
|
+
const count = profileRegistry().get(name)?.length ?? 0;
|
|
146
|
+
if (count === 0)
|
|
147
|
+
return null;
|
|
148
|
+
return count === 1 ? 'identity' : 'fungible';
|
|
149
|
+
}
|
|
@@ -9,8 +9,17 @@
|
|
|
9
9
|
* `browser.remote-control` config key, never synced).
|
|
10
10
|
*
|
|
11
11
|
* Local invocations (no marker) are never gated — this only governs cross-machine
|
|
12
|
-
* drives. Read-only queries are not gated
|
|
13
|
-
*
|
|
12
|
+
* drives. Read-only queries are not gated.
|
|
13
|
+
*
|
|
14
|
+
* The authoritative gate is {@link assertRemoteControlAllowedForRequest}, called
|
|
15
|
+
* inside the browser daemon at the top of `resolveOrCreateTask` — the one
|
|
16
|
+
* chokepoint every task-scoped verb resolves through — plus `BrowserService.start`
|
|
17
|
+
* for the task-less `browser start` command. It has to live there because ~18
|
|
18
|
+
* page verbs (`navigate`, `click`, `screenshot`, `tab-add`, …) launch OR attach
|
|
19
|
+
* to a browser implicitly, and gating only the `browser start` command left every
|
|
20
|
+
* one of them ungated; gating only the create branch of `resolveOrCreateTask` left
|
|
21
|
+
* the attach paths ungated (RUSH-3064). {@link assertRemoteControlAllowed} remains
|
|
22
|
+
* as a fast-fail CLI-side check so a refused `start` never auto-creates a profile.
|
|
14
23
|
*/
|
|
15
24
|
/** Env marker set on every remote `agents` invocation by `buildRemoteAgentsInvocation`. */
|
|
16
25
|
export declare const FLEET_REMOTE_ENV = "AGENTS_FLEET_REMOTE";
|
|
@@ -33,3 +42,20 @@ export declare function assertRemoteControlAllowed(opts?: {
|
|
|
33
42
|
env?: NodeJS.ProcessEnv;
|
|
34
43
|
enabled?: boolean;
|
|
35
44
|
}): void;
|
|
45
|
+
/**
|
|
46
|
+
* The daemon-side consent gate.
|
|
47
|
+
*
|
|
48
|
+
* Reads ONLY the per-request marker, never `process.env`: the browser daemon is
|
|
49
|
+
* shared and long-lived, and it may have been auto-started by a fleet-remote CLI
|
|
50
|
+
* that leaked `AGENTS_FLEET_REMOTE=1` into its environment permanently. Reading
|
|
51
|
+
* the daemon's env would then refuse every subsequent LOCAL drive on this
|
|
52
|
+
* machine until the daemon restarted.
|
|
53
|
+
*
|
|
54
|
+
* `actor` is likewise the caller's forwarded identity, not the daemon's.
|
|
55
|
+
*/
|
|
56
|
+
export declare function assertRemoteControlAllowedForRequest(fleetRemote: boolean | undefined, opts?: {
|
|
57
|
+
actor?: string;
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
}): void;
|
|
60
|
+
/** The refusal text, shared by the CLI-side and daemon-side gates. */
|
|
61
|
+
export declare function remoteControlRefusal(who: string): string;
|
|
@@ -9,8 +9,17 @@
|
|
|
9
9
|
* `browser.remote-control` config key, never synced).
|
|
10
10
|
*
|
|
11
11
|
* Local invocations (no marker) are never gated — this only governs cross-machine
|
|
12
|
-
* drives. Read-only queries are not gated
|
|
13
|
-
*
|
|
12
|
+
* drives. Read-only queries are not gated.
|
|
13
|
+
*
|
|
14
|
+
* The authoritative gate is {@link assertRemoteControlAllowedForRequest}, called
|
|
15
|
+
* inside the browser daemon at the top of `resolveOrCreateTask` — the one
|
|
16
|
+
* chokepoint every task-scoped verb resolves through — plus `BrowserService.start`
|
|
17
|
+
* for the task-less `browser start` command. It has to live there because ~18
|
|
18
|
+
* page verbs (`navigate`, `click`, `screenshot`, `tab-add`, …) launch OR attach
|
|
19
|
+
* to a browser implicitly, and gating only the `browser start` command left every
|
|
20
|
+
* one of them ungated; gating only the create branch of `resolveOrCreateTask` left
|
|
21
|
+
* the attach paths ungated (RUSH-3064). {@link assertRemoteControlAllowed} remains
|
|
22
|
+
* as a fast-fail CLI-side check so a refused `start` never auto-creates a profile.
|
|
14
23
|
*/
|
|
15
24
|
import { getConfigValue } from '../device-config.js';
|
|
16
25
|
/** Env marker set on every remote `agents` invocation by `buildRemoteAgentsInvocation`. */
|
|
@@ -42,7 +51,30 @@ export function assertRemoteControlAllowed(opts) {
|
|
|
42
51
|
if (enabled)
|
|
43
52
|
return;
|
|
44
53
|
const who = env.AGENTS_ACTOR_HOST || env.AGENTS_ACTOR || 'A fleet machine';
|
|
45
|
-
throw new Error(
|
|
54
|
+
throw new Error(remoteControlRefusal(who));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The daemon-side consent gate.
|
|
58
|
+
*
|
|
59
|
+
* Reads ONLY the per-request marker, never `process.env`: the browser daemon is
|
|
60
|
+
* shared and long-lived, and it may have been auto-started by a fleet-remote CLI
|
|
61
|
+
* that leaked `AGENTS_FLEET_REMOTE=1` into its environment permanently. Reading
|
|
62
|
+
* the daemon's env would then refuse every subsequent LOCAL drive on this
|
|
63
|
+
* machine until the daemon restarted.
|
|
64
|
+
*
|
|
65
|
+
* `actor` is likewise the caller's forwarded identity, not the daemon's.
|
|
66
|
+
*/
|
|
67
|
+
export function assertRemoteControlAllowedForRequest(fleetRemote, opts = {}) {
|
|
68
|
+
if (!fleetRemote)
|
|
69
|
+
return;
|
|
70
|
+
const enabled = opts.enabled ?? remoteControlEnabled();
|
|
71
|
+
if (enabled)
|
|
72
|
+
return;
|
|
73
|
+
throw new Error(remoteControlRefusal(opts.actor || 'A fleet machine'));
|
|
74
|
+
}
|
|
75
|
+
/** The refusal text, shared by the CLI-side and daemon-side gates. */
|
|
76
|
+
export function remoteControlRefusal(who) {
|
|
77
|
+
return (`${who} tried to drive this machine's browser over \`browser --device\`, but remote ` +
|
|
46
78
|
`browser control is off here. To allow it, run on THIS machine:\n` +
|
|
47
79
|
` agents browser remote-control on`);
|
|
48
80
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type ProfileDeclaration } from './registry.js';
|
|
2
|
+
import { type BrowserProfile, type ConnectionKey } from './types.js';
|
|
3
|
+
import type { BrowserProfileConfig } from '../types.js';
|
|
4
|
+
export type DeviceProbeResult = {
|
|
5
|
+
reachable: true;
|
|
6
|
+
os?: string;
|
|
7
|
+
} | {
|
|
8
|
+
reachable: false;
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
11
|
+
export type DeviceProbe = (device: string) => DeviceProbeResult;
|
|
12
|
+
export interface ResolvedBrowserTarget {
|
|
13
|
+
name: string;
|
|
14
|
+
kind: 'identity' | 'fungible';
|
|
15
|
+
/** Device the daemon will talk to. */
|
|
16
|
+
device: string;
|
|
17
|
+
local: boolean;
|
|
18
|
+
key: ConnectionKey;
|
|
19
|
+
/** `cdp:` locally, `ssh:` when tunnelling to a declaring device. */
|
|
20
|
+
target: string;
|
|
21
|
+
profile: BrowserProfile;
|
|
22
|
+
/**
|
|
23
|
+
* Human line when the daemon picked a remote device. Command output
|
|
24
|
+
* surfaces this so the caller knows WHERE the browser actually lives.
|
|
25
|
+
*/
|
|
26
|
+
picked?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Fork only fungible Electron profiles; identity-bearing ones share one connection. */
|
|
29
|
+
export declare function shouldForkProfile(kind: 'identity' | 'fungible', conn: {
|
|
30
|
+
electron?: boolean;
|
|
31
|
+
tasks: {
|
|
32
|
+
size: number;
|
|
33
|
+
};
|
|
34
|
+
}): boolean;
|
|
35
|
+
export declare function profileFromDeclaration(name: string, declaration: ProfileDeclaration): BrowserProfile;
|
|
36
|
+
/** Runtime key: profile name plus the resolved device. Drops `@endpoint-N`. */
|
|
37
|
+
export declare function profileConnectionKey(profileName: string, device: string): ConnectionKey;
|
|
38
|
+
/** Declared names that look like `query` — used in the undeclared error. */
|
|
39
|
+
export declare function similarProfileNames(query: string, names: string[]): string[];
|
|
40
|
+
export declare function undeclaredProfileError(name: string): Error;
|
|
41
|
+
export declare function unreachableDeclaringDevicesError(name: string, devices: string[], failures: string[]): Error;
|
|
42
|
+
export declare function sshEndpointForDeclaration(device: string, config: BrowserProfileConfig, endpointName?: string, os?: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Decide where `name` connects. Never claims a central leftover. Never
|
|
45
|
+
* falls back to a local browser when the name is declared elsewhere.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveBrowserTarget(name: string, opts?: {
|
|
48
|
+
endpointName?: string;
|
|
49
|
+
here?: string;
|
|
50
|
+
probe?: DeviceProbe;
|
|
51
|
+
}): ResolvedBrowserTarget;
|
|
52
|
+
/**
|
|
53
|
+
* Rename a leftover `<name>@endpoint-N` runtime dir to the new
|
|
54
|
+
* `<name>@<device>` key so logins in chrome-data are not orphaned.
|
|
55
|
+
*
|
|
56
|
+
* Only when exactly one legacy dir exists and the destination does not —
|
|
57
|
+
* several endpoint dirs stay put so a multi-endpoint profile is not merged.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* Adopt a leftover `@endpoint-N` runtime dir onto the new key ONLY for a
|
|
61
|
+
* local connect. Doing this for a tunnel key would rename this machine's
|
|
62
|
+
* pre-T2 logged-out chrome-data onto `comet-local@zion`, and connectProfile
|
|
63
|
+
* would then attach localhost CDP instead of tunnelling.
|
|
64
|
+
*/
|
|
65
|
+
export declare function adoptLegacyRuntimeIfLocal(local: boolean, profileName: string, newKey: ConnectionKey, runtimeRoot: string): void;
|
|
66
|
+
export declare function migrateLegacyRuntimeDir(profileName: string, newKey: ConnectionKey, runtimeRoot: string): void;
|
|
67
|
+
/** True when a live key is a leftover `@endpoint-N` key of `profileName`. */
|
|
68
|
+
export declare function isLegacyEndpointKey(key: string, profileName: string): boolean;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry-driven browser target resolution.
|
|
3
|
+
*
|
|
4
|
+
* The profile KIND is not a field: it falls out of how many devices declare
|
|
5
|
+
* the name ({@link profileKind}). This module answers where a name should
|
|
6
|
+
* connect, and never claims a leftover central profile (that race is T1).
|
|
7
|
+
*
|
|
8
|
+
* Three outcomes, no fourth:
|
|
9
|
+
* 1. THIS device declares the name → connect locally.
|
|
10
|
+
* 2. Only OTHER devices declare it → tunnel to a reachable declaring device.
|
|
11
|
+
* 3. Nobody declares it → fail loud. Never auto-create a local browser
|
|
12
|
+
* under a name that means a logged-in browser somewhere else.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { machineId } from '../machine-id.js';
|
|
17
|
+
import { centralBrowserProfiles, declaringDevices, profileKind, profileRegistry, } from './registry.js';
|
|
18
|
+
import { parseEndpointUrl, resolveEndpoint } from './profiles.js';
|
|
19
|
+
import { connectionKey, parseConnectionKey, } from './types.js';
|
|
20
|
+
import { probeHost } from '../hosts/ready.js';
|
|
21
|
+
/** Fork only fungible Electron profiles; identity-bearing ones share one connection. */
|
|
22
|
+
export function shouldForkProfile(kind, conn) {
|
|
23
|
+
if (kind === 'identity')
|
|
24
|
+
return false;
|
|
25
|
+
return Boolean(conn.electron && conn.tasks.size > 0);
|
|
26
|
+
}
|
|
27
|
+
export function profileFromDeclaration(name, declaration) {
|
|
28
|
+
const config = declaration.config;
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
description: config.description,
|
|
32
|
+
browser: config.browser,
|
|
33
|
+
binary: config.binary,
|
|
34
|
+
electron: config.electron,
|
|
35
|
+
targetFilter: config.targetFilter,
|
|
36
|
+
endpoints: config.endpoints,
|
|
37
|
+
defaultEndpoint: config.defaultEndpoint,
|
|
38
|
+
chrome: config.chrome,
|
|
39
|
+
secrets: config.secrets,
|
|
40
|
+
viewport: config.viewport,
|
|
41
|
+
logDir: config.logDir,
|
|
42
|
+
logHost: config.logHost,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Runtime key: profile name plus the resolved device. Drops `@endpoint-N`. */
|
|
46
|
+
export function profileConnectionKey(profileName, device) {
|
|
47
|
+
return connectionKey(profileName, device);
|
|
48
|
+
}
|
|
49
|
+
function levenshtein(a, b) {
|
|
50
|
+
if (a === b)
|
|
51
|
+
return 0;
|
|
52
|
+
if (a.length === 0)
|
|
53
|
+
return b.length;
|
|
54
|
+
if (b.length === 0)
|
|
55
|
+
return a.length;
|
|
56
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
57
|
+
for (let i = 0; i < a.length; i++) {
|
|
58
|
+
let prev = row[0];
|
|
59
|
+
row[0] = i + 1;
|
|
60
|
+
for (let j = 0; j < b.length; j++) {
|
|
61
|
+
const cur = row[j + 1];
|
|
62
|
+
const cost = a[i] === b[j] ? 0 : 1;
|
|
63
|
+
row[j + 1] = Math.min(row[j] + 1, row[j + 1] + 1, prev + cost);
|
|
64
|
+
prev = cur;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return row[b.length];
|
|
68
|
+
}
|
|
69
|
+
/** Declared names that look like `query` — used in the undeclared error. */
|
|
70
|
+
export function similarProfileNames(query, names) {
|
|
71
|
+
const q = query.toLowerCase();
|
|
72
|
+
const maxDist = Math.max(2, Math.floor(q.length / 4));
|
|
73
|
+
return names
|
|
74
|
+
.filter((name) => {
|
|
75
|
+
const x = name.toLowerCase();
|
|
76
|
+
if (x === q)
|
|
77
|
+
return false;
|
|
78
|
+
if (x.includes(q) || q.includes(x))
|
|
79
|
+
return Math.min(x.length, q.length) >= 3;
|
|
80
|
+
return levenshtein(x, q) <= maxDist;
|
|
81
|
+
})
|
|
82
|
+
.sort();
|
|
83
|
+
}
|
|
84
|
+
function lines(...parts) {
|
|
85
|
+
return parts.filter((part) => part != null && part !== '').join('\n');
|
|
86
|
+
}
|
|
87
|
+
export function undeclaredProfileError(name) {
|
|
88
|
+
const central = centralBrowserProfiles();
|
|
89
|
+
if (central[name]) {
|
|
90
|
+
return new Error(lines(`Browser profile "${name}" is not declared by any device.`, `It still lives in the central agents.yaml browser: map.`, `Claim it on the machine that hosts that browser with: agents browser profiles claim ${name}`));
|
|
91
|
+
}
|
|
92
|
+
const registry = profileRegistry();
|
|
93
|
+
const similar = similarProfileNames(name, [...registry.keys()]);
|
|
94
|
+
const similarLines = similar.map((candidate) => {
|
|
95
|
+
const devices = (registry.get(candidate) ?? []).map((entry) => entry.device);
|
|
96
|
+
return devices.length > 0
|
|
97
|
+
? ` ${candidate} (declared on ${devices.join(', ')})`
|
|
98
|
+
: ` ${candidate}`;
|
|
99
|
+
});
|
|
100
|
+
return new Error(lines(`Browser profile "${name}" is not declared by any device.`, ...(similarLines.length > 0
|
|
101
|
+
? ['Similar names:', ...similarLines]
|
|
102
|
+
: ['No device declares a similar name.']), `Create it on the machine that hosts the browser: agents browser profiles create ${name} --browser <chrome|comet|chromium|brave|edge|arc|custom>`, `Or, if it is a leftover central profile, claim it: agents browser profiles claim ${name}`));
|
|
103
|
+
}
|
|
104
|
+
export function unreachableDeclaringDevicesError(name, devices, failures) {
|
|
105
|
+
return new Error(lines(`Browser profile "${name}" is declared on ${devices.join(', ')}, but none of those devices are reachable.`, ...failures.map((line) => ` ${line}`), `A local browser will not be launched — that would be a logged-out copy of an identity-bearing profile.`));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Build the SSH endpoint used to tunnel to a declaring device's own browser.
|
|
109
|
+
*
|
|
110
|
+
* A `cdp://localhost:N` declaration on that device is THIS machine's loopback
|
|
111
|
+
* if used as-is — that is the original bug. Rewrite it to `ssh://<device>?port=N`.
|
|
112
|
+
* An already-`ssh://` declaration is used as written.
|
|
113
|
+
*/
|
|
114
|
+
function isWindowsOs(os) {
|
|
115
|
+
if (!os)
|
|
116
|
+
return false;
|
|
117
|
+
const normalized = os.toLowerCase();
|
|
118
|
+
return (normalized.includes('windows') ||
|
|
119
|
+
normalized === 'win32' ||
|
|
120
|
+
normalized === 'win64' ||
|
|
121
|
+
normalized === 'windows_nt');
|
|
122
|
+
}
|
|
123
|
+
export function sshEndpointForDeclaration(device, config, endpointName, os) {
|
|
124
|
+
const profile = profileFromDeclaration('_', { device, config });
|
|
125
|
+
const resolved = resolveEndpoint(profile, endpointName);
|
|
126
|
+
if (resolved.target.startsWith('ssh:'))
|
|
127
|
+
return resolved.target;
|
|
128
|
+
const parsed = parseEndpointUrl(resolved.target);
|
|
129
|
+
const port = parsed?.port ?? 9222;
|
|
130
|
+
const osQuery = isWindowsOs(os) ? '&os=windows' : '';
|
|
131
|
+
return `ssh://${device}?port=${port}${osQuery}`;
|
|
132
|
+
}
|
|
133
|
+
function defaultProbe(device) {
|
|
134
|
+
try {
|
|
135
|
+
const result = probeHost(device);
|
|
136
|
+
if (result.reachable)
|
|
137
|
+
return { reachable: true, os: result.os };
|
|
138
|
+
return { reachable: false, reason: `ssh to ${device} failed` };
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
reachable: false,
|
|
143
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Decide where `name` connects. Never claims a central leftover. Never
|
|
149
|
+
* falls back to a local browser when the name is declared elsewhere.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveBrowserTarget(name, opts = {}) {
|
|
152
|
+
const here = opts.here ?? machineId();
|
|
153
|
+
const kind = profileKind(name);
|
|
154
|
+
const declarations = profileRegistry().get(name) ?? [];
|
|
155
|
+
if (kind === null || declarations.length === 0) {
|
|
156
|
+
throw undeclaredProfileError(name);
|
|
157
|
+
}
|
|
158
|
+
const devices = declaringDevices(name);
|
|
159
|
+
const local = declarations.find((entry) => entry.device === here);
|
|
160
|
+
if (local) {
|
|
161
|
+
const profile = profileFromDeclaration(name, local);
|
|
162
|
+
const resolved = resolveEndpoint(profile, opts.endpointName);
|
|
163
|
+
return {
|
|
164
|
+
name,
|
|
165
|
+
kind,
|
|
166
|
+
device: here,
|
|
167
|
+
local: true,
|
|
168
|
+
key: profileConnectionKey(name, here),
|
|
169
|
+
target: resolved.target,
|
|
170
|
+
profile: {
|
|
171
|
+
...profile,
|
|
172
|
+
binary: resolved.binary,
|
|
173
|
+
targetFilter: resolved.targetFilter,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const probe = opts.probe ?? defaultProbe;
|
|
178
|
+
const candidates = [...declarations].sort((a, b) => a.device.localeCompare(b.device));
|
|
179
|
+
const failures = [];
|
|
180
|
+
for (const declaration of candidates) {
|
|
181
|
+
const result = probe(declaration.device);
|
|
182
|
+
if (!result.reachable) {
|
|
183
|
+
failures.push(`${declaration.device}: ${result.reason}`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const profile = profileFromDeclaration(name, declaration);
|
|
187
|
+
const resolved = resolveEndpoint(profile, opts.endpointName);
|
|
188
|
+
const target = sshEndpointForDeclaration(declaration.device, declaration.config, opts.endpointName, result.os);
|
|
189
|
+
const picked = candidates.length > 1
|
|
190
|
+
? `Using ${name} on ${declaration.device} (declared on ${devices.join(', ')})`
|
|
191
|
+
: `Using ${name} on ${declaration.device}`;
|
|
192
|
+
return {
|
|
193
|
+
name,
|
|
194
|
+
kind,
|
|
195
|
+
device: declaration.device,
|
|
196
|
+
local: false,
|
|
197
|
+
key: profileConnectionKey(name, declaration.device),
|
|
198
|
+
target,
|
|
199
|
+
profile: {
|
|
200
|
+
...profile,
|
|
201
|
+
binary: resolved.binary,
|
|
202
|
+
targetFilter: resolved.targetFilter,
|
|
203
|
+
},
|
|
204
|
+
picked,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
throw unreachableDeclaringDevicesError(name, devices, failures);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Rename a leftover `<name>@endpoint-N` runtime dir to the new
|
|
211
|
+
* `<name>@<device>` key so logins in chrome-data are not orphaned.
|
|
212
|
+
*
|
|
213
|
+
* Only when exactly one legacy dir exists and the destination does not —
|
|
214
|
+
* several endpoint dirs stay put so a multi-endpoint profile is not merged.
|
|
215
|
+
*/
|
|
216
|
+
/**
|
|
217
|
+
* Adopt a leftover `@endpoint-N` runtime dir onto the new key ONLY for a
|
|
218
|
+
* local connect. Doing this for a tunnel key would rename this machine's
|
|
219
|
+
* pre-T2 logged-out chrome-data onto `comet-local@zion`, and connectProfile
|
|
220
|
+
* would then attach localhost CDP instead of tunnelling.
|
|
221
|
+
*/
|
|
222
|
+
export function adoptLegacyRuntimeIfLocal(local, profileName, newKey, runtimeRoot) {
|
|
223
|
+
if (!local)
|
|
224
|
+
return;
|
|
225
|
+
migrateLegacyRuntimeDir(profileName, newKey, runtimeRoot);
|
|
226
|
+
}
|
|
227
|
+
export function migrateLegacyRuntimeDir(profileName, newKey, runtimeRoot) {
|
|
228
|
+
const dest = path.join(runtimeRoot, newKey);
|
|
229
|
+
if (fs.existsSync(dest))
|
|
230
|
+
return;
|
|
231
|
+
if (!fs.existsSync(runtimeRoot))
|
|
232
|
+
return;
|
|
233
|
+
const legacy = fs.readdirSync(runtimeRoot).filter((entry) => {
|
|
234
|
+
const parsed = parseConnectionKey(entry);
|
|
235
|
+
return (parsed.profile === profileName &&
|
|
236
|
+
parsed.endpoint !== undefined &&
|
|
237
|
+
/^endpoint-\d+$/.test(parsed.endpoint) &&
|
|
238
|
+
parsed.fork === undefined);
|
|
239
|
+
});
|
|
240
|
+
if (legacy.length !== 1)
|
|
241
|
+
return;
|
|
242
|
+
fs.renameSync(path.join(runtimeRoot, legacy[0]), dest);
|
|
243
|
+
}
|
|
244
|
+
/** True when a live key is a leftover `@endpoint-N` key of `profileName`. */
|
|
245
|
+
export function isLegacyEndpointKey(key, profileName) {
|
|
246
|
+
const parsed = parseConnectionKey(key);
|
|
247
|
+
return (parsed.profile === profileName &&
|
|
248
|
+
parsed.endpoint !== undefined &&
|
|
249
|
+
/^endpoint-\d+$/.test(parsed.endpoint));
|
|
250
|
+
}
|
|
@@ -1,5 +1,33 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type BrowserProfileWithDeclarations } from './profiles.js';
|
|
2
2
|
import { type ProfileName } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* How many devices declare this profile — the shipped model's two kinds.
|
|
5
|
+
*
|
|
6
|
+
* NOT the deleted `scope: local | fleet`, which described where the config was
|
|
7
|
+
* stored. `fungible` means several devices declare the name and each uses its
|
|
8
|
+
* own browser; `identity` means exactly one does, so the name is a singular
|
|
9
|
+
* browser. Prune only ever sees profiles THIS device declares, so `fungible`
|
|
10
|
+
* here means "declared here and elsewhere too".
|
|
11
|
+
*/
|
|
12
|
+
type PruneProfileClass = 'identity' | 'fungible';
|
|
13
|
+
/**
|
|
14
|
+
* Detect an identity-bearing profile whose stored endpoint is loopback, viewed
|
|
15
|
+
* from a machine that is NOT the declaring device.
|
|
16
|
+
*
|
|
17
|
+
* That is the original `comet-local` bug: five real logins on the declaring
|
|
18
|
+
* box, a bare logged-out chromium answering to the same name on a worker
|
|
19
|
+
* because `cdp://localhost:N` is evaluated on the machine running the command.
|
|
20
|
+
* The daemon now tunnels that shape (`resolve-target.ts`); doctor still flags
|
|
21
|
+
* it so a worker does not report a green local binary/port for someone else's
|
|
22
|
+
* browser. Kind comes from the registry (exactly one declaring device), never
|
|
23
|
+
* from a stored field.
|
|
24
|
+
*/
|
|
25
|
+
export declare function identityLoopbackMismatch(profile: BrowserProfileWithDeclarations): {
|
|
26
|
+
misfiled: false;
|
|
27
|
+
} | {
|
|
28
|
+
misfiled: true;
|
|
29
|
+
why: string;
|
|
30
|
+
};
|
|
3
31
|
/**
|
|
4
32
|
* Per-profile runtime files we persist under
|
|
5
33
|
* `~/.agents/.cache/browser/<composite>/`:
|
|
@@ -51,7 +79,7 @@ export declare function readProfileRuntime(profileName: string): ProfileRuntime
|
|
|
51
79
|
export declare function clearProfileRuntime(profileName: string): void;
|
|
52
80
|
/**
|
|
53
81
|
* Recursively remove the whole profile cache (chrome-data, tasks.json,
|
|
54
|
-
* everything). Used by `profiles
|
|
82
|
+
* everything). Used by `profiles remove` so an old profile name doesn't
|
|
55
83
|
* leak its history into a freshly-recreated one.
|
|
56
84
|
*/
|
|
57
85
|
export declare function removeProfileCache(profileName: string): void;
|
|
@@ -135,7 +163,7 @@ export declare function isProfileInUse(profileName: string): boolean;
|
|
|
135
163
|
export type PruneReason = 'binary-missing' | 'never-used';
|
|
136
164
|
export interface PruneCandidate {
|
|
137
165
|
name: string;
|
|
138
|
-
scope:
|
|
166
|
+
scope: PruneProfileClass;
|
|
139
167
|
reason: PruneReason;
|
|
140
168
|
/** Cache dirs that would be removed along with the config entry. */
|
|
141
169
|
cacheDirs: string[];
|
|
@@ -146,8 +174,15 @@ export interface PrunePlan {
|
|
|
146
174
|
/** Profiles deliberately left alone, with the guard that kept them. */
|
|
147
175
|
kept: Array<{
|
|
148
176
|
name: string;
|
|
149
|
-
scope:
|
|
177
|
+
scope: PruneProfileClass;
|
|
150
178
|
why: string;
|
|
179
|
+
/**
|
|
180
|
+
* True for an identity-bearing profile whose loopback endpoint only makes
|
|
181
|
+
* sense on the declaring device. A structured flag, not a substring of
|
|
182
|
+
* `why`: both the CLI's human output and `--json` consumers branch on it,
|
|
183
|
+
* and sniffing the reason text would couple them to wording.
|
|
184
|
+
*/
|
|
185
|
+
misfiled?: boolean;
|
|
151
186
|
}>;
|
|
152
187
|
}
|
|
153
188
|
export declare const PRUNE_REASON_TEXT: Record<PruneReason, string>;
|
|
@@ -170,8 +205,6 @@ export declare const PRUNE_REASON_TEXT: Record<PruneReason, string>;
|
|
|
170
205
|
* - **the auto-detected profile** (`auto-chrome`, or the `default` an older
|
|
171
206
|
* build wrote) — auto-regenerated by `ensureDefaultBrowserProfile`, so
|
|
172
207
|
* pruning it is pure churn.
|
|
173
|
-
* - **fleet-synced** — deleting one removes it from EVERY machine, so it takes
|
|
174
|
-
* the explicit `includeFleet` opt-in.
|
|
175
208
|
*
|
|
176
209
|
* Known limitation: `BrowserProfileConfig` records no creation time, so a
|
|
177
210
|
* profile created seconds ago and not yet started is indistinguishable from an
|
|
@@ -180,22 +213,22 @@ export declare const PRUNE_REASON_TEXT: Record<PruneReason, string>;
|
|
|
180
213
|
*/
|
|
181
214
|
export declare function planProfilePrune(profiles: Array<{
|
|
182
215
|
name: string;
|
|
183
|
-
scope:
|
|
216
|
+
scope: PruneProfileClass;
|
|
184
217
|
launchableHere: boolean;
|
|
218
|
+
/** Set when this identity-bearing profile's endpoint only makes sense on one machine. */
|
|
219
|
+
misfiledWhy?: string;
|
|
185
220
|
}>, opts?: {
|
|
186
|
-
includeFleet?: boolean;
|
|
187
221
|
configuredDefault?: string;
|
|
188
222
|
}): PrunePlan;
|
|
189
223
|
/**
|
|
190
224
|
* Build the prune plan from live config. Split from {@link planProfilePrune} so
|
|
191
225
|
* the decision rules stay unit-testable without a configured machine.
|
|
192
226
|
*/
|
|
193
|
-
export declare function buildProfilePrunePlan(
|
|
194
|
-
includeFleet?: boolean;
|
|
195
|
-
}): Promise<PrunePlan>;
|
|
227
|
+
export declare function buildProfilePrunePlan(): Promise<PrunePlan>;
|
|
196
228
|
/**
|
|
197
229
|
* Execute a prune plan: drop each candidate's config entry and wipe its cache
|
|
198
230
|
* dirs. Returns the plan that was applied so the caller can report exactly what
|
|
199
231
|
* went, matching what `--dry-run` printed.
|
|
200
232
|
*/
|
|
201
233
|
export declare function pruneProfiles(plan: PrunePlan): Promise<PrunePlan>;
|
|
234
|
+
export {};
|