@phnx-labs/agents-cli 1.22.74 → 1.22.76
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 +152 -0
- package/README.md +21 -9
- package/dist/bootstrap.js +7 -7
- package/dist/cli/command-registry.js +5 -0
- package/dist/commands/artifacts-setup.js +1 -1
- package/dist/commands/artifacts.js +1 -1
- package/dist/commands/auth.js +7 -1
- package/dist/commands/browser.js +104 -10
- package/dist/commands/commands.js +7 -6
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +26 -7
- package/dist/commands/config.js +27 -4
- package/dist/commands/cost.js +6 -4
- package/dist/commands/doctor.d.ts +6 -5
- package/dist/commands/doctor.js +32 -274
- package/dist/commands/exec.d.ts +2 -0
- package/dist/commands/exec.js +9 -2
- package/dist/commands/harness.d.ts +1 -0
- package/dist/commands/harness.js +11 -3
- package/dist/commands/hooks.js +7 -6
- package/dist/commands/mcp.js +7 -6
- package/dist/commands/memory.js +7 -7
- package/dist/commands/monitors.js +3 -2
- package/dist/commands/open.d.ts +25 -12
- package/dist/commands/open.js +24 -10
- package/dist/commands/permissions.js +7 -6
- package/dist/commands/plugins.js +21 -17
- package/dist/commands/route.js +33 -16
- package/dist/commands/rules.js +7 -12
- package/dist/commands/sessions-share.js +1 -1
- package/dist/commands/setup-watchdog.js +2 -2
- package/dist/commands/setup.js +22 -1
- package/dist/commands/share.js +26 -10
- package/dist/commands/skills.js +7 -6
- package/dist/commands/subagents.js +7 -6
- package/dist/commands/sync.js +81 -10
- package/dist/commands/view.js +4 -1
- package/dist/commands/watchdog.d.ts +1 -1
- package/dist/commands/watchdog.js +10 -10
- package/dist/commands/webhook.d.ts +4 -0
- package/dist/commands/webhook.js +22 -4
- package/dist/commands/workflows.js +7 -6
- package/dist/lib/account-registry.js +27 -6
- package/dist/lib/accounting/rotate.d.ts +3 -1
- package/dist/lib/accounting/rotate.js +8 -4
- package/dist/lib/auth-health.d.ts +2 -0
- package/dist/lib/auth-health.js +2 -0
- package/dist/lib/browser/chrome.d.ts +21 -0
- package/dist/lib/browser/chrome.js +60 -3
- package/dist/lib/browser/drivers/local.d.ts +21 -0
- package/dist/lib/browser/drivers/local.js +102 -9
- package/dist/lib/browser/profiles.d.ts +29 -1
- package/dist/lib/browser/profiles.js +50 -1
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/computer/computer-rpc.d.ts +6 -1
- package/dist/lib/computer/computer-rpc.js +23 -3
- package/dist/lib/computer/des.d.ts +1 -0
- package/dist/lib/computer/des.js +114 -0
- package/dist/lib/computer/rfb-client.d.ts +53 -0
- package/dist/lib/computer/rfb-client.js +562 -0
- package/dist/lib/config-keys.d.ts +7 -2
- package/dist/lib/config-keys.js +17 -2
- package/dist/lib/daemon/auth-sync-service.js +3 -0
- package/dist/lib/daemon/daemon.js +17 -10
- package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
- package/dist/lib/daemon/session-summarizer-service.js +39 -0
- package/dist/lib/daemon/usage-sync-service.js +3 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/daemon-ticks.d.ts +2 -2
- package/dist/lib/daemon-ticks.js +2 -1
- package/dist/lib/daemon-webhooks.js +15 -2
- package/dist/lib/deeplink/register.js +10 -9
- package/dist/lib/deeplink/url.d.ts +4 -4
- package/dist/lib/deeplink/url.js +4 -4
- package/dist/lib/device-config.js +25 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -4
- package/dist/lib/devices/doctor-findings.js +14 -8
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/stats-cache.d.ts +4 -0
- package/dist/lib/devices/stats-cache.js +19 -0
- package/dist/lib/drift-sync.d.ts +3 -1
- package/dist/lib/drift-sync.js +16 -5
- package/dist/lib/exec.d.ts +2 -0
- package/dist/lib/exec.js +16 -1
- package/dist/lib/fleet-shared-repo-sync.d.ts +12 -0
- package/dist/lib/fleet-shared-repo-sync.js +101 -4
- package/dist/lib/fleet-shared-state.d.ts +8 -0
- package/dist/lib/heal.d.ts +4 -3
- package/dist/lib/heal.js +5 -4
- package/dist/lib/hosts/ready.d.ts +1 -1
- package/dist/lib/hosts/ready.js +16 -4
- package/dist/lib/hosts/reconnect.js +4 -2
- package/dist/lib/identity/client.d.ts +6 -0
- package/dist/lib/identity/index.d.ts +16 -0
- package/dist/lib/identity/index.js +25 -1
- package/dist/lib/profiles.d.ts +2 -0
- package/dist/lib/profiles.js +28 -9
- package/dist/lib/reconcile-and-repair.d.ts +109 -0
- package/dist/lib/reconcile-and-repair.js +267 -0
- package/dist/lib/routers.d.ts +12 -1
- package/dist/lib/routers.js +30 -1
- package/dist/lib/scheduling/routines.js +8 -2
- package/dist/lib/session/active.d.ts +13 -0
- package/dist/lib/session/db.d.ts +47 -7
- package/dist/lib/session/db.js +114 -12
- package/dist/lib/session/mirror.js +58 -0
- package/dist/lib/session/remote/remote-list.d.ts +2 -0
- package/dist/lib/session/remote/remote-list.js +4 -0
- package/dist/lib/session/remote/watch.js +22 -2
- package/dist/lib/session/session-cache.d.ts +19 -0
- package/dist/lib/session/session-cache.js +46 -0
- package/dist/lib/session/types.d.ts +34 -0
- package/dist/lib/share/backend.d.ts +6 -4
- package/dist/lib/share/backend.js +10 -8
- package/dist/lib/share/config.d.ts +4 -3
- package/dist/lib/share/config.js +10 -1
- package/dist/lib/share/delete.d.ts +1 -1
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/html.d.ts +1 -1
- package/dist/lib/share/html.js +1 -1
- package/dist/lib/share/provision.d.ts +1 -1
- package/dist/lib/share/provision.js +2 -2
- package/dist/lib/share/publish.d.ts +23 -7
- package/dist/lib/share/publish.js +58 -12
- package/dist/lib/share/worker-template.js +221 -60
- package/dist/lib/startup/command-registry.js +2 -2
- package/dist/lib/state.d.ts +15 -0
- package/dist/lib/state.js +29 -7
- package/dist/lib/summarizer/config.d.ts +46 -0
- package/dist/lib/summarizer/config.js +83 -0
- package/dist/lib/summarizer/pass.d.ts +45 -0
- package/dist/lib/summarizer/pass.js +112 -0
- package/dist/lib/summarizer/summarize.d.ts +68 -0
- package/dist/lib/summarizer/summarize.js +120 -0
- package/dist/lib/teams/agents.d.ts +4 -3
- package/dist/lib/teams/agents.js +12 -4
- package/dist/lib/teams/scheduler.d.ts +4 -2
- package/dist/lib/teams/scheduler.js +6 -6
- package/dist/lib/tmux/session.d.ts +2 -0
- package/dist/lib/tmux/session.js +7 -1
- package/dist/lib/types.d.ts +20 -0
- package/dist/lib/verbs.d.ts +23 -0
- package/dist/lib/verbs.js +24 -0
- package/dist/lib/view-types.d.ts +4 -0
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withAliases } from '../lib/verbs.js';
|
|
1
2
|
import chalk from 'chalk';
|
|
2
3
|
import ora from 'ora';
|
|
3
4
|
import * as fs from 'fs';
|
|
@@ -56,8 +57,8 @@ Examples:
|
|
|
56
57
|
# Remove from version homes (and central storage on second run)
|
|
57
58
|
agents workflows remove code-review
|
|
58
59
|
`);
|
|
59
|
-
workflowsCmd
|
|
60
|
-
.command('list [agent]')
|
|
60
|
+
withAliases(workflowsCmd
|
|
61
|
+
.command('list [agent]'), 'list')
|
|
61
62
|
.description('Show installed workflows and which agent versions they are synced to')
|
|
62
63
|
.option('-a, --agent <agent>', 'Filter to a specific agent')
|
|
63
64
|
.action(async (agentArg, options) => {
|
|
@@ -260,8 +261,8 @@ Examples:
|
|
|
260
261
|
process.exit(1);
|
|
261
262
|
}
|
|
262
263
|
});
|
|
263
|
-
workflowsCmd
|
|
264
|
-
.command('remove [name]')
|
|
264
|
+
withAliases(workflowsCmd
|
|
265
|
+
.command('remove [name]'), 'remove')
|
|
265
266
|
.description('Remove a workflow from version homes (interactive picker if no name given)')
|
|
266
267
|
.addHelpText('after', `
|
|
267
268
|
Examples:
|
|
@@ -365,8 +366,8 @@ Examples:
|
|
|
365
366
|
console.log(chalk.gray('Central source unchanged. Use "agents workflows remove <name>" again to remove from ~/.agents/workflows/.'));
|
|
366
367
|
}
|
|
367
368
|
});
|
|
368
|
-
workflowsCmd
|
|
369
|
-
.command('view [name]')
|
|
369
|
+
withAliases(workflowsCmd
|
|
370
|
+
.command('view [name]'), 'view')
|
|
370
371
|
.description('Read workflow details (description, subagents, model, MCP)')
|
|
371
372
|
.addHelpText('after', `
|
|
372
373
|
Examples:
|
|
@@ -187,11 +187,32 @@ function nativeRowsForNameOrId(meta, name) {
|
|
|
187
187
|
return [];
|
|
188
188
|
return nativeIdentityRows(meta, found.agent, found.identityKey);
|
|
189
189
|
}
|
|
190
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Native label names are unique per HARNESS, not globally (PHNX-3887).
|
|
192
|
+
*
|
|
193
|
+
* One human identity is commonly signed into several harnesses —
|
|
194
|
+
* `muqsitnawaz@icloud.com` is a claude login AND a codex login AND a grok login.
|
|
195
|
+
* A global namespace let whichever harness was labelled first squat the good
|
|
196
|
+
* name, forcing prefixed junk (`cxicloud`, `gkicloud`) on the rest. Nothing is
|
|
197
|
+
* actually ambiguous at the point of use: the selector is `<harness>#<label>`,
|
|
198
|
+
* and `findUnifiedAccount` already disambiguates via `preferAgent`.
|
|
199
|
+
*
|
|
200
|
+
* Pass `agent` to scope the check to that harness. Omit it for management
|
|
201
|
+
* lookups with no harness in hand (rename/remove), which keep the old
|
|
202
|
+
* fleet-wide check so a rename cannot collide with an unrelated harness's row.
|
|
203
|
+
*
|
|
204
|
+
* Provider (non-native) accounts stay globally unique — they are selected by
|
|
205
|
+
* bare name via `--account`, with no harness to scope them by.
|
|
206
|
+
*/
|
|
207
|
+
function assertUniqueUnifiedName(name, meta, doc, exceptIds, agent) {
|
|
191
208
|
const needle = name.toLowerCase();
|
|
192
|
-
const nativeHits = listNativeAccounts(meta).filter(account =>
|
|
193
|
-
|
|
194
|
-
|
|
209
|
+
const nativeHits = listNativeAccounts(meta).filter(account => (agent === undefined || account.agent === agent)
|
|
210
|
+
&& (account.id === name || account.name.toLowerCase() === needle || account.identityLabel?.toLowerCase() === needle));
|
|
211
|
+
if (nativeHits.some(account => !exceptIds?.has(account.id))) {
|
|
212
|
+
throw new Error(agent === undefined
|
|
213
|
+
? `Account '${name}' already exists.`
|
|
214
|
+
: `Account '${name}' already exists for the ${agent} harness.`);
|
|
215
|
+
}
|
|
195
216
|
// Same laziness as findUnifiedAccount: a native row that already owns this
|
|
196
217
|
// name (even one we are mutating) means we never open the provider store.
|
|
197
218
|
if (nativeHits.length > 0)
|
|
@@ -203,7 +224,7 @@ function assertUniqueUnifiedName(name, meta, doc, exceptIds) {
|
|
|
203
224
|
export function addNativeAccount(name, agent, identityKey, identityLabel, scope) {
|
|
204
225
|
assertNativeLabel(name);
|
|
205
226
|
const meta = readMeta();
|
|
206
|
-
assertUniqueUnifiedName(name, meta);
|
|
227
|
+
assertUniqueUnifiedName(name, meta, undefined, undefined, agent);
|
|
207
228
|
const duplicate = listNativeAccounts(meta).find(account => account.agent === agent && account.identityKey === identityKey);
|
|
208
229
|
if (duplicate)
|
|
209
230
|
throw new Error(`This ${agent} login is already named '${duplicate.name}'.`);
|
|
@@ -240,7 +261,7 @@ export function labelNativeAccount(agent, identityKey, identityLabel, label, sco
|
|
|
240
261
|
assertNativeLabel(resolvedLabel);
|
|
241
262
|
const meta = readMeta();
|
|
242
263
|
const matches = nativeIdentityRows(meta, agent, identityKey);
|
|
243
|
-
assertUniqueUnifiedName(resolvedLabel, meta, undefined, new Set(matches.map(account => account.id)));
|
|
264
|
+
assertUniqueUnifiedName(resolvedLabel, meta, undefined, new Set(matches.map(account => account.id)), agent);
|
|
244
265
|
if (matches.length === 0)
|
|
245
266
|
return addNativeAccount(resolvedLabel, agent, identityKey, identityLabel, scope);
|
|
246
267
|
// Sweep every row for this identity (PHNX-3206), routing the whole sweep to the
|
|
@@ -50,6 +50,8 @@ export interface RotateCandidate {
|
|
|
50
50
|
* does not gate — a stale/absent probe never blocks a launch.
|
|
51
51
|
*/
|
|
52
52
|
authVerdict: AuthVerdict | null;
|
|
53
|
+
/** Epoch milliseconds of the auth probe behind authVerdict, when present. */
|
|
54
|
+
authCheckedAt?: number | null;
|
|
53
55
|
lastActive: Date | null;
|
|
54
56
|
/**
|
|
55
57
|
* Set only for a candidate that comes from a provider account bundle rather
|
|
@@ -243,7 +245,7 @@ export type AccountReadiness = {
|
|
|
243
245
|
* status — matching the gate — so a stale `out_of_credits` cache is not
|
|
244
246
|
* reported while the account is actually serving requests.
|
|
245
247
|
*/
|
|
246
|
-
export declare function readinessFromCandidate(candidate: RotateCandidate): AccountReadiness;
|
|
248
|
+
export declare function readinessFromCandidate(candidate: RotateCandidate, now?: number): AccountReadiness;
|
|
247
249
|
/**
|
|
248
250
|
* Whether a human sitting at a terminal can clear this exclusion by launching
|
|
249
251
|
* the agent and signing in. The two unhealthy classes are opposites, and the
|
|
@@ -15,7 +15,7 @@ import { emit } from '../feed/events.js';
|
|
|
15
15
|
import { getUsageInfoByIdentity, getUsageLookupKey, deriveUsageStatusFromSnapshot, } from './usage.js';
|
|
16
16
|
import { readAccountHeadroom } from '../fleet-cache.js';
|
|
17
17
|
import { machineId } from '../machine-id.js';
|
|
18
|
-
import { readAuthHealthCache, authCacheKey, isDeadVerdict } from '../auth-health.js';
|
|
18
|
+
import { AUTH_PROBE_MAX_AGE_MS, readAuthHealthCache, authCacheKey, isDeadVerdict } from '../auth-health.js';
|
|
19
19
|
function getRotateDir() {
|
|
20
20
|
const dir = path.join(getHelpersDir(), 'rotate');
|
|
21
21
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -229,7 +229,7 @@ function hasUsageAvailable(candidate) {
|
|
|
229
229
|
* status — matching the gate — so a stale `out_of_credits` cache is not
|
|
230
230
|
* reported while the account is actually serving requests.
|
|
231
231
|
*/
|
|
232
|
-
export function readinessFromCandidate(candidate) {
|
|
232
|
+
export function readinessFromCandidate(candidate, now = Date.now()) {
|
|
233
233
|
if (!candidate.signedIn) {
|
|
234
234
|
return { ready: false, reason: 'signed_out', email: candidate.email };
|
|
235
235
|
}
|
|
@@ -237,7 +237,9 @@ export function readinessFromCandidate(candidate) {
|
|
|
237
237
|
// auth at spawn no matter how much usage headroom it has. Exclude it BEFORE the
|
|
238
238
|
// usage gate so rotation never routes into a doomed login. Fail-open: any other
|
|
239
239
|
// (or null) verdict does not gate — see `RotateCandidate.authVerdict`.
|
|
240
|
-
|
|
240
|
+
const authFresh = candidate.authCheckedAt == null
|
|
241
|
+
|| now - candidate.authCheckedAt <= AUTH_PROBE_MAX_AGE_MS;
|
|
242
|
+
if (authFresh && candidate.authVerdict !== null && isDeadVerdict(candidate.authVerdict)) {
|
|
241
243
|
return { ready: false, reason: 'revoked', email: candidate.email };
|
|
242
244
|
}
|
|
243
245
|
if (hasUsageAvailable(candidate)) {
|
|
@@ -697,7 +699,8 @@ export async function collectRunCandidates(agent) {
|
|
|
697
699
|
// lives — see isLaunchableSignedIn. Do not reuse the active-home fallback
|
|
698
700
|
// identity for routing, or empty version homes look healthy and die at spawn.
|
|
699
701
|
const launchable = isLaunchableSignedIn(info.signedIn, credentialPresence(agent, home));
|
|
700
|
-
const
|
|
702
|
+
const authHealth = authCache[authCacheKey(localHost, agent, version)];
|
|
703
|
+
const authVerdict = authHealth?.verdict ?? null;
|
|
701
704
|
return {
|
|
702
705
|
agent,
|
|
703
706
|
version,
|
|
@@ -710,6 +713,7 @@ export async function collectRunCandidates(agent) {
|
|
|
710
713
|
plan: launchable ? info.plan : null,
|
|
711
714
|
signedIn: launchable,
|
|
712
715
|
authVerdict,
|
|
716
|
+
authCheckedAt: authHealth?.checkedAt ?? null,
|
|
713
717
|
lastActive: info.lastActive,
|
|
714
718
|
};
|
|
715
719
|
}));
|
|
@@ -25,6 +25,8 @@ export interface AuthHealth {
|
|
|
25
25
|
/** account label for display (email / id), when known. Never part of the key. */
|
|
26
26
|
account?: string;
|
|
27
27
|
}
|
|
28
|
+
/** Maximum age of an auth verdict used for automatic routing decisions. */
|
|
29
|
+
export declare const AUTH_PROBE_MAX_AGE_MS: number;
|
|
28
30
|
/** Agents with a live network probe wired up today. The rest are best-effort. */
|
|
29
31
|
export declare const LIVE_PROBE_AGENTS: ReadonlySet<AgentId>;
|
|
30
32
|
/** Map an HTTP status from a live probe to a verdict. */
|
package/dist/lib/auth-health.js
CHANGED
|
@@ -23,6 +23,8 @@ import { getCacheDir } from './state.js';
|
|
|
23
23
|
import { probeClaudeStatus, probeDroidStatus, probeKimiStatus, USAGE_HEADLESS_SCOPE_MARKER, readClaudeUsageCache, } from './accounting/usage.js';
|
|
24
24
|
import { getVersionHomePath, listInstalledVersions } from './installations/versions.js';
|
|
25
25
|
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
26
|
+
/** Maximum age of an auth verdict used for automatic routing decisions. */
|
|
27
|
+
export const AUTH_PROBE_MAX_AGE_MS = 20 * 60_000;
|
|
26
28
|
/** Agents with a live network probe wired up today. The rest are best-effort. */
|
|
27
29
|
export const LIVE_PROBE_AGENTS = new Set(['claude', 'kimi', 'droid']);
|
|
28
30
|
// ---------------------------------------------------------------------------
|
|
@@ -90,6 +90,27 @@ export declare function ensureProfilePreferences(userDataDir: string, profileNam
|
|
|
90
90
|
*/
|
|
91
91
|
export declare function isPortInUse(port: number): boolean;
|
|
92
92
|
export declare function allocatePort(): number;
|
|
93
|
+
/**
|
|
94
|
+
* Read the `--user-data-dir` a running browser process was launched with, by
|
|
95
|
+
* inspecting its command line (PHNX-3967). This is the ownership signal the
|
|
96
|
+
* attach-only guard uses to tell the credentialed canonical browser apart from a
|
|
97
|
+
* foreign port-squatter serving CDP on the same port (e.g. a logged-out
|
|
98
|
+
* `/tmp/...` Comet). Returns null when the pid is gone, exposes no
|
|
99
|
+
* `--user-data-dir`, or the platform probe fails.
|
|
100
|
+
*
|
|
101
|
+
* POSIX: `ps -ww -o command=` (full, un-truncated argv). Windows: the
|
|
102
|
+
* `CommandLine` from `Win32_Process` via PowerShell. The value is captured up to
|
|
103
|
+
* the next ` --<flag>` (or end of line) so a data dir that itself contains a
|
|
104
|
+
* space is not truncated at the space.
|
|
105
|
+
*/
|
|
106
|
+
export declare function getProcessUserDataDir(pid: number): string | null;
|
|
107
|
+
/**
|
|
108
|
+
* Extract the `--user-data-dir` value from a browser command line. Handles both
|
|
109
|
+
* `--user-data-dir=<path>` and `--user-data-dir <path>` and stops the value at
|
|
110
|
+
* the next ` --<flag>` so a path containing spaces survives. Exported for unit
|
|
111
|
+
* testing without a live process.
|
|
112
|
+
*/
|
|
113
|
+
export declare function parseUserDataDirFromCommandLine(cmdline: string): string | null;
|
|
93
114
|
export interface PortOccupant {
|
|
94
115
|
pid: number;
|
|
95
116
|
command: string;
|
|
@@ -277,9 +277,15 @@ isElectron = false) {
|
|
|
277
277
|
// *inside* each user-data-dir. Direct binary spawn with a fresh
|
|
278
278
|
// --user-data-dir creates a fully independent process — the user's
|
|
279
279
|
// normal browser (running under their default user-data-dir) and our
|
|
280
|
-
// sandboxed one coexist as two real processes.
|
|
281
|
-
//
|
|
282
|
-
//
|
|
280
|
+
// sandboxed one coexist as two real processes.
|
|
281
|
+
//
|
|
282
|
+
// These are TWO dock tiles, not one. Measured on macOS 26 (zion, 2026-09-04):
|
|
283
|
+
// a second Comet spawned this way registers its OWN LaunchServices ASN, so
|
|
284
|
+
// `lsappinfo list` reports two `ai.perplexity.comet` entries and the Dock shows
|
|
285
|
+
// two Comet icons — one of them the logged-out sandbox. The Dock does NOT
|
|
286
|
+
// collapse same-bundle processes into one tile. Spawning a rival window is
|
|
287
|
+
// exactly the failure PHNX-3967 set out to end, which is why an attach-only
|
|
288
|
+
// profile never reaches this launcher (see connectLocal / isAttachOnlyProfile).
|
|
283
289
|
const viewport = options.viewport ?? { width: 1512, height: 982 };
|
|
284
290
|
const args = [
|
|
285
291
|
'--remote-debugging-pipe',
|
|
@@ -479,6 +485,57 @@ export function allocatePort() {
|
|
|
479
485
|
}
|
|
480
486
|
throw new Error('No available ports in range 9200-9300');
|
|
481
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* Read the `--user-data-dir` a running browser process was launched with, by
|
|
490
|
+
* inspecting its command line (PHNX-3967). This is the ownership signal the
|
|
491
|
+
* attach-only guard uses to tell the credentialed canonical browser apart from a
|
|
492
|
+
* foreign port-squatter serving CDP on the same port (e.g. a logged-out
|
|
493
|
+
* `/tmp/...` Comet). Returns null when the pid is gone, exposes no
|
|
494
|
+
* `--user-data-dir`, or the platform probe fails.
|
|
495
|
+
*
|
|
496
|
+
* POSIX: `ps -ww -o command=` (full, un-truncated argv). Windows: the
|
|
497
|
+
* `CommandLine` from `Win32_Process` via PowerShell. The value is captured up to
|
|
498
|
+
* the next ` --<flag>` (or end of line) so a data dir that itself contains a
|
|
499
|
+
* space is not truncated at the space.
|
|
500
|
+
*/
|
|
501
|
+
export function getProcessUserDataDir(pid) {
|
|
502
|
+
if (!pid || pid <= 0)
|
|
503
|
+
return null;
|
|
504
|
+
let cmdline = '';
|
|
505
|
+
try {
|
|
506
|
+
if (process.platform === 'win32') {
|
|
507
|
+
cmdline = execFileSync('powershell', [
|
|
508
|
+
'-NoProfile',
|
|
509
|
+
'-Command',
|
|
510
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine`,
|
|
511
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
cmdline = execFileSync('ps', ['-ww', '-o', 'command=', '-p', String(pid)], {
|
|
515
|
+
encoding: 'utf8',
|
|
516
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
return parseUserDataDirFromCommandLine(cmdline);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Extract the `--user-data-dir` value from a browser command line. Handles both
|
|
527
|
+
* `--user-data-dir=<path>` and `--user-data-dir <path>` and stops the value at
|
|
528
|
+
* the next ` --<flag>` so a path containing spaces survives. Exported for unit
|
|
529
|
+
* testing without a live process.
|
|
530
|
+
*/
|
|
531
|
+
export function parseUserDataDirFromCommandLine(cmdline) {
|
|
532
|
+
const line = cmdline.replace(/\r?\n/g, ' ').trim();
|
|
533
|
+
const m = line.match(/--user-data-dir[=\s]+(.*?)(?=\s--[A-Za-z]|$)/);
|
|
534
|
+
if (!m)
|
|
535
|
+
return null;
|
|
536
|
+
const value = m[1].trim().replace(/^["']|["']$/g, '');
|
|
537
|
+
return value.length > 0 ? value : null;
|
|
538
|
+
}
|
|
482
539
|
/**
|
|
483
540
|
* Identify the process listening on a TCP port. Returns null when nothing is bound.
|
|
484
541
|
* Used for clearer error messages when a profile's configured port is taken by a
|
|
@@ -16,6 +16,27 @@ export interface LocalConnection {
|
|
|
16
16
|
* duplicate. Exported so the contract is unit-testable without a real Arc.
|
|
17
17
|
*/
|
|
18
18
|
export declare function arcAttachRequiredError(profileName: string, port: number): Error;
|
|
19
|
+
/**
|
|
20
|
+
* The loud error an ATTACH-ONLY profile raises when nothing debuggable is on its
|
|
21
|
+
* port (PHNX-3967). Same contract as {@link arcAttachRequiredError}, generalized
|
|
22
|
+
* to any browser: agents attach to a browser the user already started and NEVER
|
|
23
|
+
* spawn a rival window, so instead of falling through to `launchBrowser` (which
|
|
24
|
+
* would produce the second, logged-out dock tile this ticket set out to end) we
|
|
25
|
+
* fail loud with the exact relaunch that makes the canonical instance
|
|
26
|
+
* attachable — pinned to the profile's DURABLE `--user-data-dir` so the one-time
|
|
27
|
+
* sign-in persists. Exported so the contract is unit-testable without a browser.
|
|
28
|
+
*/
|
|
29
|
+
export declare function attachOnlyRequiredError(profile: Pick<BrowserProfile, 'name' | 'browser' | 'userDataDir'>, port: number): Error;
|
|
30
|
+
/**
|
|
31
|
+
* The loud error an attach-only profile raises when a browser IS serving CDP on
|
|
32
|
+
* its port but is a FOREIGN instance — its `--user-data-dir` is not the profile's
|
|
33
|
+
* durable dir (PHNX-3967). This closes the port-squat: a logged-out `/tmp/...`
|
|
34
|
+
* Comet answering CDP on the canonical port passes the browser-FAMILY identity
|
|
35
|
+
* check, so without an ownership check agents would drive it as if it were the
|
|
36
|
+
* credentialed browser. Rather than adopt the squatter we reject it and name the
|
|
37
|
+
* fix. Exported for unit testing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function foreignInstanceError(profile: Pick<BrowserProfile, 'name' | 'browser' | 'userDataDir'>, port: number, runningDataDir: string, pid: number): Error;
|
|
19
40
|
export declare function connectLocal(endpoint: string, profile: BrowserProfile,
|
|
20
41
|
/**
|
|
21
42
|
* Runtime key (`<profile>@<endpoint>`) the launched browser's user-data-dir,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as net from 'net';
|
|
2
2
|
import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
|
|
3
|
-
import { launchBrowser, getPortOccupant } from '../chrome.js';
|
|
4
|
-
import { parseEndpointUrl } from '../profiles.js';
|
|
3
|
+
import { launchBrowser, getPortOccupant, getProcessUserDataDir } from '../chrome.js';
|
|
4
|
+
import { parseEndpointUrl, isAttachOnlyProfile, resolveProfileDataDir, normalizeDataDir } from '../profiles.js';
|
|
5
5
|
/**
|
|
6
6
|
* Cheap TCP-level "is something bound here?" probe. Used as a fallback when
|
|
7
7
|
* `getPortOccupant()` (lsof-based) misses the process — Comet and other
|
|
@@ -49,6 +49,89 @@ export function arcAttachRequiredError(profileName, port) {
|
|
|
49
49
|
` open -a Arc --args --remote-debugging-port=${port}\n` +
|
|
50
50
|
`and retry. The port must match the profile's endpoint (\`agents browser profiles list\`).`);
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The loud error an ATTACH-ONLY profile raises when nothing debuggable is on its
|
|
54
|
+
* port (PHNX-3967). Same contract as {@link arcAttachRequiredError}, generalized
|
|
55
|
+
* to any browser: agents attach to a browser the user already started and NEVER
|
|
56
|
+
* spawn a rival window, so instead of falling through to `launchBrowser` (which
|
|
57
|
+
* would produce the second, logged-out dock tile this ticket set out to end) we
|
|
58
|
+
* fail loud with the exact relaunch that makes the canonical instance
|
|
59
|
+
* attachable — pinned to the profile's DURABLE `--user-data-dir` so the one-time
|
|
60
|
+
* sign-in persists. Exported so the contract is unit-testable without a browser.
|
|
61
|
+
*/
|
|
62
|
+
export function attachOnlyRequiredError(profile, port) {
|
|
63
|
+
if (profile.browser === 'arc')
|
|
64
|
+
return arcAttachRequiredError(profile.name, port);
|
|
65
|
+
const app = profile.browser === 'comet' ? 'Comet' : profile.browser;
|
|
66
|
+
const dataDir = resolveProfileDataDir(profile);
|
|
67
|
+
return new Error(`Profile "${profile.name}" is attach-only and nothing is serving the Chrome ` +
|
|
68
|
+
`DevTools Protocol on cdp://127.0.0.1:${port}. agents browser attaches to the ` +
|
|
69
|
+
`${app} you already started and never launches a second one, so it will not spawn ` +
|
|
70
|
+
`a rival window (that is the duplicate, logged-out tile this profile exists to ` +
|
|
71
|
+
`prevent). Launch the canonical ${app} with remote debugging on its durable data dir:\n` +
|
|
72
|
+
` open -a ${app} --args --remote-debugging-port=${port} --user-data-dir=${dataDir}\n` +
|
|
73
|
+
`and retry. Keep signing in once in that window — the data dir is durable, so the ` +
|
|
74
|
+
`login survives quit+relaunch.`);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The loud error an attach-only profile raises when a browser IS serving CDP on
|
|
78
|
+
* its port but is a FOREIGN instance — its `--user-data-dir` is not the profile's
|
|
79
|
+
* durable dir (PHNX-3967). This closes the port-squat: a logged-out `/tmp/...`
|
|
80
|
+
* Comet answering CDP on the canonical port passes the browser-FAMILY identity
|
|
81
|
+
* check, so without an ownership check agents would drive it as if it were the
|
|
82
|
+
* credentialed browser. Rather than adopt the squatter we reject it and name the
|
|
83
|
+
* fix. Exported for unit testing.
|
|
84
|
+
*/
|
|
85
|
+
export function foreignInstanceError(profile, port, runningDataDir, pid) {
|
|
86
|
+
const expected = resolveProfileDataDir(profile);
|
|
87
|
+
const app = profile.browser === 'comet' ? 'Comet' : profile.browser;
|
|
88
|
+
return new Error(`Attach-only ownership check failed for profile "${profile.name}" on ` +
|
|
89
|
+
`cdp://127.0.0.1:${port}: a ${app} is serving CDP there (pid ${pid}) but it is ` +
|
|
90
|
+
`running a FOREIGN user-data-dir\n` +
|
|
91
|
+
` running: ${runningDataDir}\n` +
|
|
92
|
+
` expected: ${expected}\n` +
|
|
93
|
+
`so it is not this profile's credentialed browser. agents will not drive it. Close ` +
|
|
94
|
+
`that instance (\`kill ${pid}\`), then relaunch the canonical ${app}:\n` +
|
|
95
|
+
` open -a ${app} --args --remote-debugging-port=${port} --user-data-dir=${expected}`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Prefix every ownership-rejection message carries, so `connectLocal`'s catch can
|
|
99
|
+
* re-throw it verbatim instead of misreading it as "attach failed, launch fresh".
|
|
100
|
+
*/
|
|
101
|
+
const OWNERSHIP_REJECTION_PREFIX = 'Attach-only ownership check failed';
|
|
102
|
+
/**
|
|
103
|
+
* Verify the browser serving CDP on `port` belongs to this attach-only profile,
|
|
104
|
+
* by comparing its live `--user-data-dir` to the profile's durable dir
|
|
105
|
+
* (PHNX-3967). No-op for a `launch`-policy profile. When an occupant IS present
|
|
106
|
+
* but its data dir can't be read, fail LOUD rather than open: a canonical Comet
|
|
107
|
+
* this profile is meant to attach to was launched by the user with the durable
|
|
108
|
+
* `--user-data-dir` and is readable via `ps`, so an unreadable occupant is the
|
|
109
|
+
* suspicious case, not a legitimate one — driving it would be the exact
|
|
110
|
+
* port-squat this guard exists to stop.
|
|
111
|
+
*/
|
|
112
|
+
function verifyEndpointOwnership(profile, port) {
|
|
113
|
+
if (!isAttachOnlyProfile(profile))
|
|
114
|
+
return;
|
|
115
|
+
// Arc is the user's single running instance under its own default data dir; it
|
|
116
|
+
// has no managed durable dir to compare against and no /tmp-squat vector.
|
|
117
|
+
if (profile.browser === 'arc')
|
|
118
|
+
return;
|
|
119
|
+
const occupant = getPortOccupant(port);
|
|
120
|
+
if (!occupant)
|
|
121
|
+
return;
|
|
122
|
+
const expected = resolveProfileDataDir(profile);
|
|
123
|
+
const runningDataDir = getProcessUserDataDir(occupant.pid);
|
|
124
|
+
if (!runningDataDir) {
|
|
125
|
+
throw new Error(`${OWNERSHIP_REJECTION_PREFIX} for profile "${profile.name}" on cdp://127.0.0.1:${port}: ` +
|
|
126
|
+
`a process (pid ${occupant.pid}) is serving CDP there but its --user-data-dir could not be ` +
|
|
127
|
+
`read, so ownership can't be confirmed. Refusing to attach to an unverified instance. If ` +
|
|
128
|
+
`this is your canonical browser, relaunch it with --user-data-dir=${expected} so the ` +
|
|
129
|
+
`attach-only guard can verify it.`);
|
|
130
|
+
}
|
|
131
|
+
if (normalizeDataDir(runningDataDir) !== normalizeDataDir(expected)) {
|
|
132
|
+
throw foreignInstanceError(profile, port, runningDataDir, occupant.pid);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
52
135
|
export async function connectLocal(endpoint, profile,
|
|
53
136
|
/**
|
|
54
137
|
* Runtime key (`<profile>@<endpoint>`) the launched browser's user-data-dir,
|
|
@@ -81,6 +164,11 @@ key) {
|
|
|
81
164
|
try {
|
|
82
165
|
const { wsUrl, browser } = await discoverBrowserWsUrl(port, 'localhost', profile.name);
|
|
83
166
|
verifyBrowserIdentity(browser, profile.browser, port);
|
|
167
|
+
// Ownership beyond browser FAMILY (PHNX-3967): a foreign /tmp Comet answers
|
|
168
|
+
// CDP on the canonical port and passes verifyBrowserIdentity, so before we
|
|
169
|
+
// adopt the endpoint confirm its --user-data-dir is this profile's durable
|
|
170
|
+
// dir. Rejects the port-squatter loudly instead of driving it.
|
|
171
|
+
verifyEndpointOwnership(profile, port);
|
|
84
172
|
const cdp = new CDPClient();
|
|
85
173
|
await cdp.connect(wsUrl);
|
|
86
174
|
return { cdp, port, pid: 0 };
|
|
@@ -89,13 +177,18 @@ key) {
|
|
|
89
177
|
if (err instanceof Error && err.message.startsWith('Browser identity mismatch')) {
|
|
90
178
|
throw err;
|
|
91
179
|
}
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
180
|
+
// An ownership rejection is a definitive answer — never fall through to a
|
|
181
|
+
// fresh launch or a generic port message. Re-throw it verbatim.
|
|
182
|
+
if (err instanceof Error && err.message.startsWith(OWNERSHIP_REJECTION_PREFIX)) {
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
// An attach-only profile reached the catch, so the attach above failed:
|
|
186
|
+
// nothing debuggable is on this port. Never fall through to launchBrowser —
|
|
187
|
+
// that would spawn the duplicate/stray-window this ticket set out to end
|
|
188
|
+
// (Arc: #2779, PHNX-2399; Comet: PHNX-3967). Fail loud with the relaunch that
|
|
189
|
+
// makes the canonical instance attachable.
|
|
190
|
+
if (isAttachOnlyProfile(profile)) {
|
|
191
|
+
throw attachOnlyRequiredError(profile, port);
|
|
99
192
|
}
|
|
100
193
|
// Distinguish "nothing listening on this port" (fine to launch fresh) from
|
|
101
194
|
// "something is listening but it's not a debuggable browser" (bail loudly —
|
|
@@ -44,6 +44,34 @@ export type BrowserProfileWithDeclarations = BrowserProfile & {
|
|
|
44
44
|
export declare function getConfiguredDefaultProfileName(): string | undefined;
|
|
45
45
|
export declare function getBrowserRuntimeDir(): string;
|
|
46
46
|
export declare function getProfileRuntimeDir(name: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* True when a profile attaches to a browser the user already launched and MUST
|
|
49
|
+
* NOT spawn its own (PHNX-3967). Arc is inherently attach-only — it is
|
|
50
|
+
* single-instance and relaunching it just produces a stray window, never a
|
|
51
|
+
* second debuggable process (issue #2779) — regardless of an explicit policy.
|
|
52
|
+
* Any other browser is attach-only only when the profile says so.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isAttachOnlyProfile(profile: Pick<BrowserProfile, 'browser' | 'launchPolicy'>): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The DURABLE `--user-data-dir` for a profile (PHNX-3967). An explicit
|
|
57
|
+
* `profile.userDataDir` wins; otherwise a per-profile dir under
|
|
58
|
+
* `~/.agents/.history/browser-profiles/<name>/chrome-data`, which is outside
|
|
59
|
+
* `~/.agents/.cache` so a one-time sign-in survives quit+relaunch and the
|
|
60
|
+
* `profiles remove` cache sweep. This is the dir the canonical Comet is launched
|
|
61
|
+
* with and the value the ownership guard compares a running instance against.
|
|
62
|
+
*
|
|
63
|
+
* Note this is distinct from {@link getProfileRuntimeDir}'s cache `chrome-data`,
|
|
64
|
+
* which `launchBrowser` uses for a `launch`-policy profile it spawns itself.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveProfileDataDir(profile: Pick<BrowserProfile, 'name' | 'userDataDir'>): string;
|
|
67
|
+
/**
|
|
68
|
+
* Normalize a `--user-data-dir` path for the attach-only ownership comparison
|
|
69
|
+
* (PHNX-3967): the real path when it resolves on this box, else the literal with
|
|
70
|
+
* trailing slashes stripped. The single source both the runtime guard
|
|
71
|
+
* (`drivers/local.ts` `verifyEndpointOwnership`) and `browser profiles doctor`
|
|
72
|
+
* use, so they can never disagree about whether a running instance is foreign.
|
|
73
|
+
*/
|
|
74
|
+
export declare function normalizeDataDir(dir: string): string;
|
|
47
75
|
/**
|
|
48
76
|
* Default destination for browser downloads for a profile. Set browser-global at
|
|
49
77
|
* connect time (see BrowserService), so downloads land here even when the agent
|
|
@@ -217,7 +245,7 @@ export declare function updateProfile(profile: BrowserProfile): Promise<void>;
|
|
|
217
245
|
* ({@link getProfileRuntimeDir}) and any live `<name>@<endpoint>` connection, so
|
|
218
246
|
* changing either orphans the cached browser data. Delete and recreate instead.
|
|
219
247
|
*/
|
|
220
|
-
export type EditableProfileFields = Partial<Pick<BrowserProfile, 'description' | 'binary' | 'electron' | 'targetFilter' | 'endpoints' | 'chrome' | 'secrets' | 'viewport'>>;
|
|
248
|
+
export type EditableProfileFields = Partial<Pick<BrowserProfile, 'description' | 'binary' | 'electron' | 'targetFilter' | 'endpoints' | 'launchPolicy' | 'userDataDir' | 'chrome' | 'secrets' | 'viewport'>>;
|
|
221
249
|
export interface EditProfileResult {
|
|
222
250
|
profile: BrowserProfile;
|
|
223
251
|
devices: string[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
-
import { getBrowserRuntimeDir as getBrowserRuntimeDirRoot, readMeta, updateMeta, } from '../state.js';
|
|
3
|
+
import { getBrowserRuntimeDir as getBrowserRuntimeDirRoot, getBrowserDurableDir, readMeta, updateMeta, } from '../state.js';
|
|
4
4
|
import { getConfigValue } from '../device-config.js';
|
|
5
5
|
import { machineId } from '../machine-id.js';
|
|
6
6
|
import { declaringDevices, profileRegistry, } from './registry.js';
|
|
@@ -51,6 +51,49 @@ export function getBrowserRuntimeDir() {
|
|
|
51
51
|
export function getProfileRuntimeDir(name) {
|
|
52
52
|
return path.join(getBrowserRuntimeDir(), name);
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* True when a profile attaches to a browser the user already launched and MUST
|
|
56
|
+
* NOT spawn its own (PHNX-3967). Arc is inherently attach-only — it is
|
|
57
|
+
* single-instance and relaunching it just produces a stray window, never a
|
|
58
|
+
* second debuggable process (issue #2779) — regardless of an explicit policy.
|
|
59
|
+
* Any other browser is attach-only only when the profile says so.
|
|
60
|
+
*/
|
|
61
|
+
export function isAttachOnlyProfile(profile) {
|
|
62
|
+
return profile.browser === 'arc' || profile.launchPolicy === 'attach-only';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The DURABLE `--user-data-dir` for a profile (PHNX-3967). An explicit
|
|
66
|
+
* `profile.userDataDir` wins; otherwise a per-profile dir under
|
|
67
|
+
* `~/.agents/.history/browser-profiles/<name>/chrome-data`, which is outside
|
|
68
|
+
* `~/.agents/.cache` so a one-time sign-in survives quit+relaunch and the
|
|
69
|
+
* `profiles remove` cache sweep. This is the dir the canonical Comet is launched
|
|
70
|
+
* with and the value the ownership guard compares a running instance against.
|
|
71
|
+
*
|
|
72
|
+
* Note this is distinct from {@link getProfileRuntimeDir}'s cache `chrome-data`,
|
|
73
|
+
* which `launchBrowser` uses for a `launch`-policy profile it spawns itself.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveProfileDataDir(profile) {
|
|
76
|
+
if (profile.userDataDir)
|
|
77
|
+
return profile.userDataDir;
|
|
78
|
+
return path.join(getBrowserDurableDir(), profile.name, 'chrome-data');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Normalize a `--user-data-dir` path for the attach-only ownership comparison
|
|
82
|
+
* (PHNX-3967): the real path when it resolves on this box, else the literal with
|
|
83
|
+
* trailing slashes stripped. The single source both the runtime guard
|
|
84
|
+
* (`drivers/local.ts` `verifyEndpointOwnership`) and `browser profiles doctor`
|
|
85
|
+
* use, so they can never disagree about whether a running instance is foreign.
|
|
86
|
+
*/
|
|
87
|
+
export function normalizeDataDir(dir) {
|
|
88
|
+
let out = dir;
|
|
89
|
+
try {
|
|
90
|
+
out = fs.realpathSync(dir);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* dir may not exist on this box; compare the literal */
|
|
94
|
+
}
|
|
95
|
+
return out.replace(/\/+$/, '');
|
|
96
|
+
}
|
|
54
97
|
/**
|
|
55
98
|
* Default destination for browser downloads for a profile. Set browser-global at
|
|
56
99
|
* connect time (see BrowserService), so downloads land here even when the agent
|
|
@@ -79,6 +122,8 @@ function configToProfile(name, config, devices = []) {
|
|
|
79
122
|
targetFilter: config.targetFilter,
|
|
80
123
|
endpoints: config.endpoints,
|
|
81
124
|
defaultEndpoint: config.defaultEndpoint,
|
|
125
|
+
launchPolicy: config.launchPolicy,
|
|
126
|
+
userDataDir: config.userDataDir,
|
|
82
127
|
chrome: config.chrome,
|
|
83
128
|
secrets: config.secrets,
|
|
84
129
|
viewport: config.viewport,
|
|
@@ -103,6 +148,10 @@ function profileToConfig(profile) {
|
|
|
103
148
|
config.targetFilter = profile.targetFilter;
|
|
104
149
|
if (profile.defaultEndpoint)
|
|
105
150
|
config.defaultEndpoint = profile.defaultEndpoint;
|
|
151
|
+
if (profile.launchPolicy)
|
|
152
|
+
config.launchPolicy = profile.launchPolicy;
|
|
153
|
+
if (profile.userDataDir)
|
|
154
|
+
config.userDataDir = profile.userDataDir;
|
|
106
155
|
if (profile.chrome)
|
|
107
156
|
config.chrome = profile.chrome;
|
|
108
157
|
if (profile.secrets)
|
|
@@ -109,6 +109,24 @@ export interface BrowserProfile {
|
|
|
109
109
|
*/
|
|
110
110
|
endpoints: string[] | Record<string, EndpointPreset>;
|
|
111
111
|
defaultEndpoint?: string;
|
|
112
|
+
/**
|
|
113
|
+
* How agents obtain a live browser for this profile (PHNX-3967):
|
|
114
|
+
* - `launch` (default when absent): spawn the browser under a managed
|
|
115
|
+
* `--user-data-dir` when nothing is serving CDP on the port.
|
|
116
|
+
* - `attach-only`: NEVER spawn a rival window — attach to a browser the user
|
|
117
|
+
* already started with remote debugging, else fail loud with a relaunch
|
|
118
|
+
* hint. Arc is inherently attach-only; a canonical signed-in Comet uses it.
|
|
119
|
+
* Pairs with a durable {@link userDataDir}.
|
|
120
|
+
*/
|
|
121
|
+
launchPolicy?: 'attach-only' | 'launch';
|
|
122
|
+
/**
|
|
123
|
+
* Absolute durable `--user-data-dir` for this profile (PHNX-3967). When absent,
|
|
124
|
+
* an attach-only profile resolves a default durable dir outside `.cache` so a
|
|
125
|
+
* one-time sign-in survives relaunch. The value the ownership guard compares a
|
|
126
|
+
* running instance against to reject a port-squatter. Resolve via
|
|
127
|
+
* `resolveProfileDataDir(profile)` rather than reading directly.
|
|
128
|
+
*/
|
|
129
|
+
userDataDir?: string;
|
|
112
130
|
chrome?: ChromeOptions;
|
|
113
131
|
secrets?: string;
|
|
114
132
|
viewport?: {
|
|
@@ -25,10 +25,15 @@ export declare function resolveTcpEndpoint(): {
|
|
|
25
25
|
port: number;
|
|
26
26
|
token: string | null;
|
|
27
27
|
} | null;
|
|
28
|
+
export declare function resolveVncEndpoint(): {
|
|
29
|
+
host: string;
|
|
30
|
+
port: number;
|
|
31
|
+
password: string;
|
|
32
|
+
} | null;
|
|
28
33
|
export declare function openComputerClient(): ComputerClient;
|
|
29
34
|
export declare const RPC_TIMEOUT_MS = 30000;
|
|
30
35
|
export declare function resolveRpcTimeoutMs(env: string | undefined): number;
|
|
31
36
|
export declare function describeTransport(): {
|
|
32
|
-
kind: 'socket' | 'stdio' | 'tcp' | 'none';
|
|
37
|
+
kind: 'socket' | 'stdio' | 'tcp' | 'vnc' | 'none';
|
|
33
38
|
path: string | null;
|
|
34
39
|
};
|