@phnx-labs/agents-cli 1.22.117 → 1.22.119
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 +301 -0
- package/dist/bootstrap.js +4 -11
- package/dist/commands/accounts.js +7 -4
- 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.js +53 -1
- package/dist/commands/projects.js +92 -7
- 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.js +9 -9
- 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/add.d.ts +3 -1
- package/dist/lib/accounts/add.js +24 -13
- 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 +7 -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-account-token.d.ts +7 -1
- package/dist/lib/claude-account-token.js +26 -4
- 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 +10 -13
- 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/github/pr-mergeable.d.ts +4 -2
- package/dist/lib/github/pr-mergeable.js +5 -3
- package/dist/lib/github/project-prs.d.ts +58 -3
- package/dist/lib/github/project-prs.js +205 -7
- package/dist/lib/harness/adapters/cursor.js +14 -4
- package/dist/lib/harness-auth-capabilities.d.ts +4 -3
- package/dist/lib/harness-auth-capabilities.js +1 -2
- 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/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.d.ts +1 -1
- package/dist/lib/installations/shims.js +32 -12
- package/dist/lib/installations/store.d.ts +9 -0
- package/dist/lib/installations/store.js +24 -0
- package/dist/lib/installations/update.js +21 -15
- package/dist/lib/installations/versions.js +15 -3
- 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/profiles.js +0 -2
- package/dist/lib/projects.d.ts +16 -0
- package/dist/lib/projects.js +31 -0
- 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/active.d.ts +17 -6
- package/dist/lib/session/active.js +105 -49
- 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/pid-registry.d.ts +6 -3
- package/dist/lib/session/pid-registry.js +18 -8
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +7 -3
- 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 +5 -10
- 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 +16 -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,89 +0,0 @@
|
|
|
1
|
-
import type { CDPClient } from './cdp.js';
|
|
2
|
-
export interface RefOpts {
|
|
3
|
-
interactive?: boolean;
|
|
4
|
-
limit?: number;
|
|
5
|
-
compact?: boolean;
|
|
6
|
-
}
|
|
7
|
-
export interface RefNode {
|
|
8
|
-
ref: number;
|
|
9
|
-
role: string;
|
|
10
|
-
name: string;
|
|
11
|
-
attrs: string[];
|
|
12
|
-
backendNodeId?: number;
|
|
13
|
-
/** Stable selector used by non-CDP DOM backends such as native Arc. */
|
|
14
|
-
selector?: string;
|
|
15
|
-
editor?: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Stable, navigation-surviving identity for a ref. Unlike {@link RefNode}, this
|
|
19
|
-
* carries no `backendNodeId` (which goes stale the moment the DOM re-renders) —
|
|
20
|
-
* only the accessible descriptor a fresh `getRefs` can be re-matched against.
|
|
21
|
-
* Cached in per-task state so a later `click <ref>` can self-heal when the
|
|
22
|
-
* integer ref has drifted (see {@link healRef}).
|
|
23
|
-
*/
|
|
24
|
-
export interface RefDescriptor {
|
|
25
|
-
ref: number;
|
|
26
|
-
role: string;
|
|
27
|
-
name: string;
|
|
28
|
-
attrs: string[];
|
|
29
|
-
/**
|
|
30
|
-
* Best-effort CSS selector captured via DOM. Reserved for future
|
|
31
|
-
* selector-based healing; matching today is role+name+attrs+position.
|
|
32
|
-
*/
|
|
33
|
-
selector?: string;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* A cached ref listing for one tab: the stable descriptors plus the resolved
|
|
37
|
-
* {@link RefOpts} the listing was built with. A later `click`/`type` MUST
|
|
38
|
-
* rebuild its node map with these same opts, or the integer refs it looks up
|
|
39
|
-
* are numbered against a *different* accessibility filter than the one the
|
|
40
|
-
* user saw in `browser refs` — the drift that makes self-heal silently click
|
|
41
|
-
* the wrong element on the second click.
|
|
42
|
-
*/
|
|
43
|
-
export interface RefSnapshot {
|
|
44
|
-
descriptors: RefDescriptor[];
|
|
45
|
-
opts: {
|
|
46
|
-
interactive: boolean;
|
|
47
|
-
limit: number;
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
/** Snapshot the stable descriptor for every ref in a freshly-built node map. */
|
|
51
|
-
export declare function describeRefs(nodeMap: Map<number, RefNode>): RefDescriptor[];
|
|
52
|
-
/**
|
|
53
|
-
* Re-resolve a stale ref against a freshly-built node map by matching the
|
|
54
|
-
* cached descriptor. Pure — no CDP, no DOM.
|
|
55
|
-
*
|
|
56
|
-
* The integer ref assigned by {@link getRefs} is positional: it shifts whenever
|
|
57
|
-
* the accessibility tree changes (navigation, re-render, or even a different
|
|
58
|
-
* `interactive` filter). The descriptor's (role, name) pair is the coarse
|
|
59
|
-
* identity, but a page can carry duplicate (role, name) elements — two "Submit"
|
|
60
|
-
* buttons, one action button per repeated list row. Matching on (role, name)
|
|
61
|
-
* alone would heal to whichever duplicate the fresh scan happens to hit first,
|
|
62
|
-
* which can be the wrong one. So among (role, name) matches we tie-break, in
|
|
63
|
-
* priority order:
|
|
64
|
-
*
|
|
65
|
-
* 1. **DOM-backed** — a candidate with a `backendNodeId` is resolvable to
|
|
66
|
-
* coordinates; one without is unclickable, so healing to it is useless.
|
|
67
|
-
* 2. **Attribute similarity** — the cached `attrs` (disabled / checked /
|
|
68
|
-
* selected / …) disambiguate otherwise-identical twins (the *disabled*
|
|
69
|
-
* Submit vs the enabled one).
|
|
70
|
-
* 3. **Positional proximity** — the fresh ref closest to the original cached
|
|
71
|
-
* ref index wins, so a repeated list-row action heals to its nearest
|
|
72
|
-
* positional twin rather than jumping across the page.
|
|
73
|
-
*
|
|
74
|
-
* Returns the new integer ref, or `null` when no node with the same role+name
|
|
75
|
-
* exists in the fresh map (unhealable — the element is genuinely gone).
|
|
76
|
-
*/
|
|
77
|
-
export declare function healRef(descriptor: Pick<RefDescriptor, 'ref' | 'role' | 'name' | 'attrs'>, freshNodeMap: Map<number, RefNode>): number | null;
|
|
78
|
-
export declare function getRefs(cdp: CDPClient, sessionId: string, opts?: RefOpts): Promise<{
|
|
79
|
-
refs: string;
|
|
80
|
-
nodeMap: Map<number, RefNode>;
|
|
81
|
-
opts: {
|
|
82
|
-
interactive: boolean;
|
|
83
|
-
limit: number;
|
|
84
|
-
};
|
|
85
|
-
}>;
|
|
86
|
-
export declare function resolveRefToCoords(cdp: CDPClient, sessionId: string, nodeMap: Map<number, RefNode>, ref: number): Promise<{
|
|
87
|
-
x: number;
|
|
88
|
-
y: number;
|
|
89
|
-
}>;
|
package/dist/lib/browser/refs.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { truncate } from '../format.js';
|
|
2
|
-
/** Snapshot the stable descriptor for every ref in a freshly-built node map. */
|
|
3
|
-
export function describeRefs(nodeMap) {
|
|
4
|
-
return Array.from(nodeMap.values()).map((n) => ({
|
|
5
|
-
ref: n.ref,
|
|
6
|
-
role: n.role,
|
|
7
|
-
name: n.name,
|
|
8
|
-
attrs: n.attrs.slice(),
|
|
9
|
-
selector: n.selector,
|
|
10
|
-
}));
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Re-resolve a stale ref against a freshly-built node map by matching the
|
|
14
|
-
* cached descriptor. Pure — no CDP, no DOM.
|
|
15
|
-
*
|
|
16
|
-
* The integer ref assigned by {@link getRefs} is positional: it shifts whenever
|
|
17
|
-
* the accessibility tree changes (navigation, re-render, or even a different
|
|
18
|
-
* `interactive` filter). The descriptor's (role, name) pair is the coarse
|
|
19
|
-
* identity, but a page can carry duplicate (role, name) elements — two "Submit"
|
|
20
|
-
* buttons, one action button per repeated list row. Matching on (role, name)
|
|
21
|
-
* alone would heal to whichever duplicate the fresh scan happens to hit first,
|
|
22
|
-
* which can be the wrong one. So among (role, name) matches we tie-break, in
|
|
23
|
-
* priority order:
|
|
24
|
-
*
|
|
25
|
-
* 1. **DOM-backed** — a candidate with a `backendNodeId` is resolvable to
|
|
26
|
-
* coordinates; one without is unclickable, so healing to it is useless.
|
|
27
|
-
* 2. **Attribute similarity** — the cached `attrs` (disabled / checked /
|
|
28
|
-
* selected / …) disambiguate otherwise-identical twins (the *disabled*
|
|
29
|
-
* Submit vs the enabled one).
|
|
30
|
-
* 3. **Positional proximity** — the fresh ref closest to the original cached
|
|
31
|
-
* ref index wins, so a repeated list-row action heals to its nearest
|
|
32
|
-
* positional twin rather than jumping across the page.
|
|
33
|
-
*
|
|
34
|
-
* Returns the new integer ref, or `null` when no node with the same role+name
|
|
35
|
-
* exists in the fresh map (unhealable — the element is genuinely gone).
|
|
36
|
-
*/
|
|
37
|
-
export function healRef(descriptor, freshNodeMap) {
|
|
38
|
-
const { ref, role, name, attrs } = descriptor;
|
|
39
|
-
const cachedAttrs = new Set(attrs);
|
|
40
|
-
// How well a candidate's attribute set matches the cached one: reward shared
|
|
41
|
-
// attrs, penalize both extras the candidate carries and cached attrs it is
|
|
42
|
-
// missing. Exact set equality scores highest; an all-attrs-empty comparison
|
|
43
|
-
// scores 0 for every candidate (so the tie-break falls through to proximity).
|
|
44
|
-
const attrScore = (node) => {
|
|
45
|
-
let shared = 0;
|
|
46
|
-
for (const a of node.attrs)
|
|
47
|
-
if (cachedAttrs.has(a))
|
|
48
|
-
shared += 1;
|
|
49
|
-
const extra = node.attrs.length - shared;
|
|
50
|
-
const missing = cachedAttrs.size - shared;
|
|
51
|
-
return shared * 2 - extra - missing;
|
|
52
|
-
};
|
|
53
|
-
let best = null;
|
|
54
|
-
let bestKey = null;
|
|
55
|
-
for (const node of freshNodeMap.values()) {
|
|
56
|
-
if (node.role !== role || node.name !== name)
|
|
57
|
-
continue;
|
|
58
|
-
// Lexicographic key, all "higher is better":
|
|
59
|
-
// [ DOM-backed, attribute similarity, -distance-to-original-ref ].
|
|
60
|
-
const key = [
|
|
61
|
-
node.backendNodeId !== undefined || node.selector !== undefined ? 1 : 0,
|
|
62
|
-
attrScore(node),
|
|
63
|
-
-Math.abs(node.ref - ref),
|
|
64
|
-
];
|
|
65
|
-
if (bestKey === null ||
|
|
66
|
-
key[0] > bestKey[0] ||
|
|
67
|
-
(key[0] === bestKey[0] && key[1] > bestKey[1]) ||
|
|
68
|
-
(key[0] === bestKey[0] && key[1] === bestKey[1] && key[2] > bestKey[2])) {
|
|
69
|
-
best = node;
|
|
70
|
-
bestKey = key;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return best ? best.ref : null;
|
|
74
|
-
}
|
|
75
|
-
const EDITOR_DETECT_FN = `(function() {
|
|
76
|
-
let el = this;
|
|
77
|
-
for (let i = 0; i < 5; i++) {
|
|
78
|
-
if (!el || el === document.documentElement) break;
|
|
79
|
-
if (el.hasAttribute && el.hasAttribute('data-lexical-editor')) return 'lexical';
|
|
80
|
-
if (el.classList && el.classList.contains('ProseMirror')) return 'prosemirror';
|
|
81
|
-
if (el.hasAttribute && el.hasAttribute('data-slate-editor')) return 'slate';
|
|
82
|
-
if (el.classList && Array.from(el.classList).some(function(c) { return /^DraftEditor-/.test(c); })) return 'draft';
|
|
83
|
-
if (el.classList && el.classList.contains('ql-editor')) return 'quill';
|
|
84
|
-
if (el.classList && el.classList.contains('ck-editor__editable')) return 'ckeditor5';
|
|
85
|
-
if (el.tagName === 'TRIX-EDITOR') return 'trix';
|
|
86
|
-
el = el.parentElement;
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
89
|
-
})`;
|
|
90
|
-
async function detectEditorForNode(cdp, sessionId, backendNodeId) {
|
|
91
|
-
const { object } = await cdp.send('DOM.resolveNode', { backendNodeId }, sessionId);
|
|
92
|
-
if (!object.objectId)
|
|
93
|
-
return undefined;
|
|
94
|
-
const objectId = object.objectId;
|
|
95
|
-
try {
|
|
96
|
-
const { result } = await cdp.send('Runtime.callFunctionOn', { objectId, functionDeclaration: EDITOR_DETECT_FN, returnByValue: true }, sessionId);
|
|
97
|
-
return result.value ?? undefined;
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
await cdp.send('Runtime.releaseObject', { objectId }, sessionId);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const INTERACTIVE_ROLES = new Set([
|
|
104
|
-
'button',
|
|
105
|
-
'link',
|
|
106
|
-
'textbox',
|
|
107
|
-
'checkbox',
|
|
108
|
-
'radio',
|
|
109
|
-
'combobox',
|
|
110
|
-
'listbox',
|
|
111
|
-
'option',
|
|
112
|
-
'menuitem',
|
|
113
|
-
'tab',
|
|
114
|
-
'slider',
|
|
115
|
-
'spinbutton',
|
|
116
|
-
'searchbox',
|
|
117
|
-
'switch',
|
|
118
|
-
'menuitemcheckbox',
|
|
119
|
-
'menuitemradio',
|
|
120
|
-
'treeitem',
|
|
121
|
-
]);
|
|
122
|
-
export async function getRefs(cdp, sessionId, opts = {}) {
|
|
123
|
-
const { interactive = true, limit = 500, compact = false } = opts;
|
|
124
|
-
const { nodes } = (await cdp.send('Accessibility.getFullAXTree', {}, sessionId));
|
|
125
|
-
const nodeMap = new Map();
|
|
126
|
-
const lines = [];
|
|
127
|
-
let refCounter = 1;
|
|
128
|
-
for (const node of nodes) {
|
|
129
|
-
if (refCounter > limit)
|
|
130
|
-
break;
|
|
131
|
-
const role = node.role?.value?.toLowerCase() || '';
|
|
132
|
-
if (!role || role === 'none' || role === 'generic')
|
|
133
|
-
continue;
|
|
134
|
-
if (interactive && !INTERACTIVE_ROLES.has(role))
|
|
135
|
-
continue;
|
|
136
|
-
const name = node.name?.value || '';
|
|
137
|
-
const attrs = [];
|
|
138
|
-
for (const prop of node.properties || []) {
|
|
139
|
-
const propName = prop.name;
|
|
140
|
-
const propValue = prop.value?.value;
|
|
141
|
-
if (propName === 'disabled' && propValue === true)
|
|
142
|
-
attrs.push('disabled');
|
|
143
|
-
if (propName === 'checked' && propValue === true)
|
|
144
|
-
attrs.push('checked');
|
|
145
|
-
if (propName === 'selected' && propValue === true)
|
|
146
|
-
attrs.push('selected');
|
|
147
|
-
if (propName === 'expanded' && propValue === true)
|
|
148
|
-
attrs.push('expanded');
|
|
149
|
-
if (propName === 'required' && propValue === true)
|
|
150
|
-
attrs.push('required');
|
|
151
|
-
if (propName === 'readonly' && propValue === true)
|
|
152
|
-
attrs.push('readonly');
|
|
153
|
-
if (propName === 'invalid' && propValue === true)
|
|
154
|
-
attrs.push('invalid');
|
|
155
|
-
}
|
|
156
|
-
const ref = refCounter++;
|
|
157
|
-
const refNode = {
|
|
158
|
-
ref,
|
|
159
|
-
role,
|
|
160
|
-
name,
|
|
161
|
-
attrs,
|
|
162
|
-
backendNodeId: node.backendDOMNodeId,
|
|
163
|
-
};
|
|
164
|
-
if (role === 'textbox' && node.backendDOMNodeId) {
|
|
165
|
-
const editor = await detectEditorForNode(cdp, sessionId, node.backendDOMNodeId);
|
|
166
|
-
if (editor)
|
|
167
|
-
refNode.editor = editor;
|
|
168
|
-
}
|
|
169
|
-
nodeMap.set(ref, refNode);
|
|
170
|
-
const attrStr = attrs.length > 0 ? ` [${attrs.join('] [')}]` : '';
|
|
171
|
-
const editorStr = refNode.editor ? ` [editor=${refNode.editor}]` : '';
|
|
172
|
-
const nameStr = name ? ` "${truncate(name, 50)}"` : '';
|
|
173
|
-
const line = compact
|
|
174
|
-
? `${role}${nameStr} [ref=${ref}]${attrStr}${editorStr}`
|
|
175
|
-
: `- ${role}${nameStr} [ref=${ref}]${attrStr}${editorStr}`;
|
|
176
|
-
lines.push(line);
|
|
177
|
-
}
|
|
178
|
-
return { refs: lines.join('\n'), nodeMap, opts: { interactive, limit } };
|
|
179
|
-
}
|
|
180
|
-
export async function resolveRefToCoords(cdp, sessionId, nodeMap, ref) {
|
|
181
|
-
const node = nodeMap.get(ref);
|
|
182
|
-
if (!node)
|
|
183
|
-
throw new Error(`Ref ${ref} not found`);
|
|
184
|
-
if (!node.backendNodeId)
|
|
185
|
-
throw new Error(`Ref ${ref} has no DOM node`);
|
|
186
|
-
const { model } = (await cdp.send('DOM.getBoxModel', { backendNodeId: node.backendNodeId }, sessionId));
|
|
187
|
-
const [x1, y1, x2, y2, x3, y3, x4, y4] = model.content;
|
|
188
|
-
const centerX = (x1 + x2 + x3 + x4) / 4;
|
|
189
|
-
const centerY = (y1 + y2 + y3 + y4) / 4;
|
|
190
|
-
return { x: centerX, y: centerY };
|
|
191
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { BrowserProfileConfig } from '../types.js';
|
|
2
|
-
export interface ProfileDeclaration {
|
|
3
|
-
device: string;
|
|
4
|
-
config: BrowserProfileConfig;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* The leftover central `browser:` tombstone from before per-device declarations.
|
|
8
|
-
* Empty when none remain — and `agents sync` now drains it automatically via
|
|
9
|
-
* {@link autoEvictCentralBrowserProfiles}, so on a synced box this is empty and
|
|
10
|
-
* {@link profileRegistry} is the single source of truth. It survives only to
|
|
11
|
-
* fold in migration and to explain an as-yet-unclaimed profile in a resolve
|
|
12
|
-
* error (a profile hostable on a peer that has not synced yet), never as a
|
|
13
|
-
* parallel store callers read for resolution.
|
|
14
|
-
*/
|
|
15
|
-
export declare function centralBrowserProfiles(): Record<string, BrowserProfileConfig>;
|
|
16
|
-
interface CentralClaimResult {
|
|
17
|
-
claimed: string[];
|
|
18
|
-
skipped: string[];
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Fold leftover central `browser:` entries into THIS device's declaration file.
|
|
22
|
-
*
|
|
23
|
-
* The EXPLICIT operator action (`agents browser profiles claim`), run on the
|
|
24
|
-
* machine that actually owns the browser. It is never called implicitly from a
|
|
25
|
-
* read, and that is the point: every device can read the central map, so an
|
|
26
|
-
* implicit claim races — whichever box reads first claims the name, {@link
|
|
27
|
-
* profileKind} then reports it `identity`, and the daemon tunnels to that box,
|
|
28
|
-
* which for a fleet-wide `cdp://localhost:*` profile is a logged-out headless
|
|
29
|
-
* browser wearing a credentialed browser's name. {@link autoEvictCentralBrowserProfiles}
|
|
30
|
-
* closes that race for the automatic path (host-gated + non-throwing on sync);
|
|
31
|
-
* this one is the manual, throw-on-conflict counterpart.
|
|
32
|
-
*
|
|
33
|
-
* `canHostHere` is supplied by the command layer so this module stays a leaf —
|
|
34
|
-
* it must not import `isProfileLaunchableHere` (that would cycle through
|
|
35
|
-
* chrome.ts → profiles.ts). Only profiles this machine can host are claimed; the
|
|
36
|
-
* rest stay central, undeclared, and fail loudly on resolve.
|
|
37
|
-
*/
|
|
38
|
-
export declare function migrateCentralBrowserProfiles(canHostHere: (config: BrowserProfileConfig) => boolean, name?: string): CentralClaimResult;
|
|
39
|
-
/**
|
|
40
|
-
* Automatic, self-draining counterpart to {@link migrateCentralBrowserProfiles}:
|
|
41
|
-
* fold every lingering central `browser:` profile THIS machine can host into its
|
|
42
|
-
* device doc and clear it from central, so the tombstone drains itself on
|
|
43
|
-
* `agents sync` with no manual `agents browser profiles claim`.
|
|
44
|
-
*
|
|
45
|
-
* Safe to call implicitly, unlike a claim at registry-read time (which races —
|
|
46
|
-
* see the `profileRegistry does not claim central declarations` guard), but the
|
|
47
|
-
* CALLER owns one ordering invariant: invoke it AFTER the sync's repo pull. Two
|
|
48
|
-
* boxes can both host the same profile (`canHostHere` is launchability, not
|
|
49
|
-
* ownership), so a box acting on a PRE-pull view of central could re-claim a
|
|
50
|
-
* profile a peer already drained and pushed — both writes land in different
|
|
51
|
-
* device files with no git conflict, and the profile flips identity->fungible.
|
|
52
|
-
* Running post-pull means central already reflects peers' drains, so the tombstone
|
|
53
|
-
* is gone locally before this box looks. The selection is also computed INSIDE
|
|
54
|
-
* the meta lock, and the path never throws — an unhostable profile or one that
|
|
55
|
-
* conflicts with an existing local declaration is left central for an explicit
|
|
56
|
-
* claim rather than wedging the sync. After it runs, {@link profileRegistry} is
|
|
57
|
-
* the single source of truth: a claimed profile lives in the device doc alone,
|
|
58
|
-
* never double-counted across the two stores.
|
|
59
|
-
*/
|
|
60
|
-
export declare function autoEvictCentralBrowserProfiles(canHostHere: (config: BrowserProfileConfig) => boolean): CentralClaimResult;
|
|
61
|
-
/**
|
|
62
|
-
* Every profile any device declares, keyed by name to all declaring devices.
|
|
63
|
-
* Reads every `devices/<name>/agents.yaml`; declarations never overwrite.
|
|
64
|
-
*/
|
|
65
|
-
export declare function profileRegistry(): Map<string, ProfileDeclaration[]>;
|
|
66
|
-
/** Devices declaring `name`, empty when nobody does. */
|
|
67
|
-
export declare function declaringDevices(name: string): string[];
|
|
68
|
-
/** Exactly one declaring device is identity-bearing; several are fungible. */
|
|
69
|
-
export declare function profileKind(name: string): 'identity' | 'fungible' | null;
|
|
70
|
-
export {};
|
|
@@ -1,252 +0,0 @@
|
|
|
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 { commitCentralConfigAfterWrite, getUserAgentsDir, readMeta, updateMeta, withMetaLock, writeMetaUnlocked } from '../state.js';
|
|
6
|
-
function isRecord(value) {
|
|
7
|
-
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* The leftover central `browser:` tombstone from before per-device declarations.
|
|
11
|
-
* Empty when none remain — and `agents sync` now drains it automatically via
|
|
12
|
-
* {@link autoEvictCentralBrowserProfiles}, so on a synced box this is empty and
|
|
13
|
-
* {@link profileRegistry} is the single source of truth. It survives only to
|
|
14
|
-
* fold in migration and to explain an as-yet-unclaimed profile in a resolve
|
|
15
|
-
* error (a profile hostable on a peer that has not synced yet), never as a
|
|
16
|
-
* parallel store callers read for resolution.
|
|
17
|
-
*/
|
|
18
|
-
export function centralBrowserProfiles() {
|
|
19
|
-
const central = readMeta().browser;
|
|
20
|
-
if (!central || Object.keys(central).length === 0)
|
|
21
|
-
return {};
|
|
22
|
-
return { ...central };
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Pick the central `browser:` entries this machine may fold into its device doc.
|
|
26
|
-
*
|
|
27
|
-
* A profile is claimable only when `canHostHere` accepts it — the endpoint or
|
|
28
|
-
* binary resolves to THIS machine — so a `cdp://localhost:*` profile owned by a
|
|
29
|
-
* peer is never claimed here (the logged-out-headless-browser bug this module
|
|
30
|
-
* exists to prevent). A name already declared locally with an IDENTICAL config
|
|
31
|
-
* is folded too, which just drops the redundant central copy. A name declared
|
|
32
|
-
* locally with a DIFFERENT config is a genuine conflict: `onConflict: 'throw'`
|
|
33
|
-
* (the explicit `claim` command) surfaces it; `onConflict: 'skip'` (automatic
|
|
34
|
-
* eviction on sync) leaves it central so a stray duplicate can never wedge a
|
|
35
|
-
* sync — an operator resolves it with an explicit claim.
|
|
36
|
-
*/
|
|
37
|
-
function selectClaimableCentral(central, local, canHostHere, opts) {
|
|
38
|
-
const toClaim = {};
|
|
39
|
-
const skipped = [];
|
|
40
|
-
for (const [profileName, config] of Object.entries(central)) {
|
|
41
|
-
if (opts.name && profileName !== opts.name)
|
|
42
|
-
continue;
|
|
43
|
-
if (!canHostHere(config)) {
|
|
44
|
-
skipped.push(profileName);
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
const existing = local[profileName];
|
|
48
|
-
if (existing && !isDeepStrictEqual(existing, config)) {
|
|
49
|
-
if (opts.onConflict === 'throw') {
|
|
50
|
-
throw new Error(`Cannot migrate browser profile "${profileName}": central agents.yaml and this device's ` +
|
|
51
|
-
`agents.yaml declare different configurations. Resolve the duplicate before retrying.`);
|
|
52
|
-
}
|
|
53
|
-
skipped.push(profileName);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
toClaim[profileName] = config;
|
|
57
|
-
}
|
|
58
|
-
return { toClaim, skipped };
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Return `current` with `toClaim` moved out of the central `browser:` map and
|
|
62
|
-
* into this device's `deviceBrowser` doc. The central key is dropped entirely
|
|
63
|
-
* once drained, so the tombstone disappears rather than lingering as an empty
|
|
64
|
-
* map. Pure — the caller applies it under the meta lock.
|
|
65
|
-
*/
|
|
66
|
-
function buildEvictedMeta(current, toClaim) {
|
|
67
|
-
const legacy = current;
|
|
68
|
-
const remaining = {};
|
|
69
|
-
for (const [profileName, config] of Object.entries(legacy.browser ?? {})) {
|
|
70
|
-
if (!(profileName in toClaim))
|
|
71
|
-
remaining[profileName] = config;
|
|
72
|
-
}
|
|
73
|
-
const { browser: _removed, ...withoutCentralBrowser } = legacy;
|
|
74
|
-
return {
|
|
75
|
-
...withoutCentralBrowser,
|
|
76
|
-
...(Object.keys(remaining).length > 0 ? { browser: remaining } : {}),
|
|
77
|
-
deviceBrowser: { ...current.deviceBrowser, ...toClaim },
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Commit a claim computed by the EXPLICIT path (`migrateCentralBrowserProfiles`).
|
|
82
|
-
* `toClaim` was selected from an unlocked snapshot — acceptable for that manual,
|
|
83
|
-
* deliberate command; the automatic path instead selects INSIDE the lock (see
|
|
84
|
-
* {@link autoEvictCentralBrowserProfiles}) so an unrelated concurrent write can
|
|
85
|
-
* never be clobbered by a stale selection.
|
|
86
|
-
*/
|
|
87
|
-
function commitCentralClaim(toClaim) {
|
|
88
|
-
updateMeta((current) => buildEvictedMeta(current, toClaim));
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Fold leftover central `browser:` entries into THIS device's declaration file.
|
|
92
|
-
*
|
|
93
|
-
* The EXPLICIT operator action (`agents browser profiles claim`), run on the
|
|
94
|
-
* machine that actually owns the browser. It is never called implicitly from a
|
|
95
|
-
* read, and that is the point: every device can read the central map, so an
|
|
96
|
-
* implicit claim races — whichever box reads first claims the name, {@link
|
|
97
|
-
* profileKind} then reports it `identity`, and the daemon tunnels to that box,
|
|
98
|
-
* which for a fleet-wide `cdp://localhost:*` profile is a logged-out headless
|
|
99
|
-
* browser wearing a credentialed browser's name. {@link autoEvictCentralBrowserProfiles}
|
|
100
|
-
* closes that race for the automatic path (host-gated + non-throwing on sync);
|
|
101
|
-
* this one is the manual, throw-on-conflict counterpart.
|
|
102
|
-
*
|
|
103
|
-
* `canHostHere` is supplied by the command layer so this module stays a leaf —
|
|
104
|
-
* it must not import `isProfileLaunchableHere` (that would cycle through
|
|
105
|
-
* chrome.ts → profiles.ts). Only profiles this machine can host are claimed; the
|
|
106
|
-
* rest stay central, undeclared, and fail loudly on resolve.
|
|
107
|
-
*/
|
|
108
|
-
export function migrateCentralBrowserProfiles(canHostHere, name) {
|
|
109
|
-
const meta = readMeta();
|
|
110
|
-
const central = meta.browser;
|
|
111
|
-
if (!central || Object.keys(central).length === 0) {
|
|
112
|
-
if (name) {
|
|
113
|
-
throw new Error(`No leftover central browser profile named "${name}".`);
|
|
114
|
-
}
|
|
115
|
-
return { claimed: [], skipped: [] };
|
|
116
|
-
}
|
|
117
|
-
const local = meta.deviceBrowser ?? {};
|
|
118
|
-
if (name) {
|
|
119
|
-
const config = central[name];
|
|
120
|
-
if (!config) {
|
|
121
|
-
throw new Error(local[name]
|
|
122
|
-
? `Browser profile "${name}" is already declared on this device.`
|
|
123
|
-
: `No leftover central browser profile named "${name}".`);
|
|
124
|
-
}
|
|
125
|
-
if (!canHostHere(config)) {
|
|
126
|
-
throw new Error(`Cannot claim browser profile "${name}": this machine cannot host it ` +
|
|
127
|
-
`(its browser/binary isn't installed here). Run this command on the machine that has that browser.`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const { toClaim, skipped } = selectClaimableCentral(central, local, canHostHere, {
|
|
131
|
-
name,
|
|
132
|
-
onConflict: 'throw',
|
|
133
|
-
});
|
|
134
|
-
const claimed = Object.keys(toClaim).sort();
|
|
135
|
-
skipped.sort();
|
|
136
|
-
if (claimed.length === 0)
|
|
137
|
-
return { claimed, skipped };
|
|
138
|
-
commitCentralClaim(toClaim);
|
|
139
|
-
return { claimed, skipped };
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Automatic, self-draining counterpart to {@link migrateCentralBrowserProfiles}:
|
|
143
|
-
* fold every lingering central `browser:` profile THIS machine can host into its
|
|
144
|
-
* device doc and clear it from central, so the tombstone drains itself on
|
|
145
|
-
* `agents sync` with no manual `agents browser profiles claim`.
|
|
146
|
-
*
|
|
147
|
-
* Safe to call implicitly, unlike a claim at registry-read time (which races —
|
|
148
|
-
* see the `profileRegistry does not claim central declarations` guard), but the
|
|
149
|
-
* CALLER owns one ordering invariant: invoke it AFTER the sync's repo pull. Two
|
|
150
|
-
* boxes can both host the same profile (`canHostHere` is launchability, not
|
|
151
|
-
* ownership), so a box acting on a PRE-pull view of central could re-claim a
|
|
152
|
-
* profile a peer already drained and pushed — both writes land in different
|
|
153
|
-
* device files with no git conflict, and the profile flips identity->fungible.
|
|
154
|
-
* Running post-pull means central already reflects peers' drains, so the tombstone
|
|
155
|
-
* is gone locally before this box looks. The selection is also computed INSIDE
|
|
156
|
-
* the meta lock, and the path never throws — an unhostable profile or one that
|
|
157
|
-
* conflicts with an existing local declaration is left central for an explicit
|
|
158
|
-
* claim rather than wedging the sync. After it runs, {@link profileRegistry} is
|
|
159
|
-
* the single source of truth: a claimed profile lives in the device doc alone,
|
|
160
|
-
* never double-counted across the two stores.
|
|
161
|
-
*/
|
|
162
|
-
export function autoEvictCentralBrowserProfiles(canHostHere) {
|
|
163
|
-
// Read fresh, select, and commit inside ONE meta-lock acquisition. Reading
|
|
164
|
-
// central before the lock and committing after (the manual path's shape) leaves
|
|
165
|
-
// a window where a concurrent write on THIS machine changes central/deviceBrowser
|
|
166
|
-
// between snapshot and commit, so the stale selection would be merged over the
|
|
167
|
-
// fresher value. Selecting from the state the lock just handed us closes that
|
|
168
|
-
// window. We write ONLY when something is claimed, so a no-op sync never touches
|
|
169
|
-
// any doc (writeMetaUnlocked would otherwise re-serialize the device doc every
|
|
170
|
-
// run). This mirrors updateMeta's own body, minus the unconditional write.
|
|
171
|
-
let centralChanged = false;
|
|
172
|
-
const result = withMetaLock(() => {
|
|
173
|
-
const meta = readMeta();
|
|
174
|
-
const central = meta.browser;
|
|
175
|
-
if (!central || Object.keys(central).length === 0)
|
|
176
|
-
return { claimed: [], skipped: [] };
|
|
177
|
-
const local = meta.deviceBrowser ?? {};
|
|
178
|
-
const { toClaim, skipped } = selectClaimableCentral(central, local, canHostHere, {
|
|
179
|
-
onConflict: 'skip',
|
|
180
|
-
});
|
|
181
|
-
const claimed = Object.keys(toClaim).sort();
|
|
182
|
-
skipped.sort();
|
|
183
|
-
if (claimed.length === 0)
|
|
184
|
-
return { claimed, skipped };
|
|
185
|
-
centralChanged = writeMetaUnlocked(buildEvictedMeta(meta, toClaim));
|
|
186
|
-
return { claimed, skipped };
|
|
187
|
-
});
|
|
188
|
-
// Commit the central eviction AFTER the meta lock releases (the invariant:
|
|
189
|
-
// every non-daemon central write commits after the lock, so agents.yaml is
|
|
190
|
-
// never left dirty at rest to re-open the pull-trip window). Gated inside the
|
|
191
|
-
// helper on centralChanged && !isDaemonProcess().
|
|
192
|
-
commitCentralConfigAfterWrite(centralChanged);
|
|
193
|
-
return result;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Every profile any device declares, keyed by name to all declaring devices.
|
|
197
|
-
* Reads every `devices/<name>/agents.yaml`; declarations never overwrite.
|
|
198
|
-
*/
|
|
199
|
-
export function profileRegistry() {
|
|
200
|
-
const registry = new Map();
|
|
201
|
-
const devicesDir = path.join(getUserAgentsDir(), 'devices');
|
|
202
|
-
if (!fs.existsSync(devicesDir))
|
|
203
|
-
return registry;
|
|
204
|
-
const devices = fs
|
|
205
|
-
.readdirSync(devicesDir, { withFileTypes: true })
|
|
206
|
-
.filter((entry) => entry.isDirectory())
|
|
207
|
-
.map((entry) => entry.name)
|
|
208
|
-
.sort();
|
|
209
|
-
for (const device of devices) {
|
|
210
|
-
const file = path.join(devicesDir, device, 'agents.yaml');
|
|
211
|
-
if (!fs.existsSync(file))
|
|
212
|
-
continue;
|
|
213
|
-
let parsed;
|
|
214
|
-
try {
|
|
215
|
-
parsed = yaml.parse(fs.readFileSync(file, 'utf8'));
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
throw new Error(`Cannot read browser declarations from ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
|
-
}
|
|
220
|
-
if (parsed == null)
|
|
221
|
-
continue;
|
|
222
|
-
if (!isRecord(parsed)) {
|
|
223
|
-
throw new Error(`Cannot read browser declarations from ${file}: document root must be a map.`);
|
|
224
|
-
}
|
|
225
|
-
const browser = parsed.browser;
|
|
226
|
-
if (browser == null)
|
|
227
|
-
continue;
|
|
228
|
-
if (!isRecord(browser)) {
|
|
229
|
-
throw new Error(`Cannot read browser declarations from ${file}: browser must be a map.`);
|
|
230
|
-
}
|
|
231
|
-
for (const [name, config] of Object.entries(browser)) {
|
|
232
|
-
if (!isRecord(config)) {
|
|
233
|
-
throw new Error(`Cannot read browser declaration "${name}" from ${file}: profile must be a map.`);
|
|
234
|
-
}
|
|
235
|
-
const declarations = registry.get(name) ?? [];
|
|
236
|
-
declarations.push({ device, config: config });
|
|
237
|
-
registry.set(name, declarations);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return registry;
|
|
241
|
-
}
|
|
242
|
-
/** Devices declaring `name`, empty when nobody does. */
|
|
243
|
-
export function declaringDevices(name) {
|
|
244
|
-
return (profileRegistry().get(name) ?? []).map((declaration) => declaration.device);
|
|
245
|
-
}
|
|
246
|
-
/** Exactly one declaring device is identity-bearing; several are fungible. */
|
|
247
|
-
export function profileKind(name) {
|
|
248
|
-
const count = profileRegistry().get(name)?.length ?? 0;
|
|
249
|
-
if (count === 0)
|
|
250
|
-
return null;
|
|
251
|
-
return count === 1 ? 'identity' : 'fungible';
|
|
252
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Consent gate for driving THIS machine's browser from another fleet machine.
|
|
3
|
-
*
|
|
4
|
-
* `browser --device <device>` routes a browser command to `<device>` over SSH (the
|
|
5
|
-
* fleet passthrough) and runs `agents browser ...` there. `agents ssh <device>
|
|
6
|
-
* agents browser …` is the same remote drive through the ssh wrapper. Every such
|
|
7
|
-
* remote invocation carries the {@link FLEET_REMOTE_ENV} marker, set at the fleet
|
|
8
|
-
* dispatch site (`maybeRunOnHost` / `streamAgentsOnHost`) and on the ssh wrapper
|
|
9
|
-
* (`buildSshInvocation` via `markFleetRemote`). A machine only accepts being driven
|
|
10
|
-
* when its owner has opted in via `agents browser remote-control on` (the
|
|
11
|
-
* device-scope `browser.remote-control` config key, never synced).
|
|
12
|
-
*
|
|
13
|
-
* Local invocations (no marker) are never gated — this only governs cross-machine
|
|
14
|
-
* drives. Read-only queries are not gated.
|
|
15
|
-
*
|
|
16
|
-
* The authoritative gate is {@link assertRemoteControlAllowedForRequest}, called
|
|
17
|
-
* inside the browser daemon at the top of `resolveOrCreateTask` — the one
|
|
18
|
-
* chokepoint every task-scoped verb resolves through — plus `BrowserService.start`
|
|
19
|
-
* for the task-less `browser start` command and `BrowserService.stopProfile` for
|
|
20
|
-
* the task-less `browser stop --profile` command. It has to live there because ~18
|
|
21
|
-
* page verbs (`navigate`, `click`, `screenshot`, `tab-add`, …) launch OR attach
|
|
22
|
-
* to a browser implicitly, and gating only the `browser start` command left every
|
|
23
|
-
* one of them ungated; gating only the create branch of `resolveOrCreateTask` left
|
|
24
|
-
* the attach paths ungated (RUSH-3064); `stop --profile` is handled by `bindTask`'s
|
|
25
|
-
* early return before `resolveOrCreateTask` ever runs, so it needed its own gate
|
|
26
|
-
* call too (PHNX-3317). {@link assertRemoteControlAllowed} remains as a fast-fail
|
|
27
|
-
* CLI-side check so a refused `start` never auto-creates a profile.
|
|
28
|
-
*/
|
|
29
|
-
/** Env marker set on every remote `agents` invocation by `buildRemoteAgentsInvocation` and `markFleetRemote`. */
|
|
30
|
-
export declare const FLEET_REMOTE_ENV = "AGENTS_FLEET_REMOTE";
|
|
31
|
-
/** True when this process was dispatched to this machine by a fleet `--device` or `agents ssh … agents browser` run. */
|
|
32
|
-
export declare function isFleetRemoteInvocation(env?: NodeJS.ProcessEnv): boolean;
|
|
33
|
-
/**
|
|
34
|
-
* Guard the browser-drive entry point. A fleet-remote invocation may only drive
|
|
35
|
-
* this machine's browser when the owner opted in; otherwise throw a clear,
|
|
36
|
-
* actionable error. A local invocation is never gated.
|
|
37
|
-
*
|
|
38
|
-
* `env` and `enabled` are injectable so the decision is unit-testable without a
|
|
39
|
-
* real remote hop or a real config store.
|
|
40
|
-
*/
|
|
41
|
-
export declare function assertRemoteControlAllowed(opts?: {
|
|
42
|
-
env?: NodeJS.ProcessEnv;
|
|
43
|
-
enabled?: boolean;
|
|
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;
|