@browserbridge/bbx 1.7.8 → 1.9.0
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/PRIVACY.md +57 -12
- package/README.md +19 -8
- package/SECURITY.md +12 -0
- package/package.json +15 -6
- package/packages/agent-client/src/cli-args.js +59 -0
- package/packages/agent-client/src/cli-batch.js +121 -58
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli.js +272 -70
- package/packages/agent-client/src/client.js +296 -8
- package/packages/agent-client/src/command-registry.js +64 -55
- package/packages/agent-client/src/config.js +195 -0
- package/packages/agent-client/src/content-extract.js +198 -0
- package/packages/agent-client/src/npm-self-update.js +305 -0
- package/packages/agent-client/src/remotes.js +4 -2
- package/packages/agent-client/src/runtime.js +864 -39
- package/packages/agent-client/src/types.ts +169 -6
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +69 -6
- package/packages/mcp-server/src/handlers-dom.js +61 -4
- package/packages/mcp-server/src/handlers-navigation.js +2 -1
- package/packages/mcp-server/src/handlers-page.js +211 -54
- package/packages/mcp-server/src/handlers-utils.js +101 -10
- package/packages/mcp-server/src/handlers.js +14 -7
- package/packages/mcp-server/src/server.js +176 -11
- package/packages/native-host/src/artifact-store.js +353 -0
- package/packages/native-host/src/config.js +9 -1
- package/packages/native-host/src/daemon-logger.js +3 -1
- package/packages/native-host/src/daemon.js +701 -33
- package/packages/native-host/src/native-host.js +28 -3
- package/packages/protocol/src/capability-values.js +2 -0
- package/packages/protocol/src/defaults.js +31 -0
- package/packages/protocol/src/errors.js +102 -6
- package/packages/protocol/src/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/protocol.js +612 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +206 -38
- package/packages/protocol/src/summary.js +203 -30
- package/packages/protocol/src/types.ts +721 -2
- package/skills/browser-bridge/SKILL.md +54 -19
- package/skills/browser-bridge/references/interaction.md +55 -11
- package/skills/browser-bridge/references/patch-workflow.md +2 -0
- package/skills/browser-bridge/references/protocol.md +265 -86
- package/skills/browser-bridge/references/token-efficiency.md +29 -13
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
getBridgeDir,
|
|
10
10
|
getDaemonLogPath,
|
|
11
11
|
getDaemonPidPath,
|
|
12
|
+
getBridgeTransport,
|
|
12
13
|
getManifestInstallDir,
|
|
13
14
|
getSocketPath,
|
|
15
|
+
readProxyConfig,
|
|
14
16
|
SUPPORTED_BROWSERS,
|
|
15
17
|
} from '../../native-host/src/config.js';
|
|
16
18
|
import {
|
|
@@ -20,26 +22,75 @@ import {
|
|
|
20
22
|
import { resolveDefaultExtensionId } from '../../native-host/src/install-manifest.js';
|
|
21
23
|
import {
|
|
22
24
|
BridgeError,
|
|
25
|
+
BRIDGE_METHOD_REGISTRY,
|
|
23
26
|
CLIENT_REQUEST_TIMEOUT_MARGIN_MS,
|
|
24
27
|
DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
|
|
25
28
|
getBridgeOperationTimeoutMs,
|
|
29
|
+
getProtocolVersion,
|
|
26
30
|
MAX_CLIENT_REQUEST_TIMEOUT_MS,
|
|
31
|
+
normalizeRecoveryTelemetrySummary,
|
|
32
|
+
RECOVERY_EVENT_KINDS,
|
|
33
|
+
sanitizeIncidentalPath,
|
|
34
|
+
sanitizeIncidentalText,
|
|
27
35
|
} from '../../protocol/src/index.js';
|
|
28
36
|
import { methodNeedsTab } from './cli-helpers.js';
|
|
29
37
|
import { BridgeClient } from './client.js';
|
|
38
|
+
import { applyConfiguredAutoUpdate } from './config.js';
|
|
39
|
+
import { readRemoteConfig } from './remotes.js';
|
|
40
|
+
import { collectSetupStatus } from './setup-status.js';
|
|
30
41
|
|
|
31
42
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
32
43
|
/** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
|
|
33
44
|
/** @typedef {import('./types.js').BridgeRequestSource} BridgeRequestSource */
|
|
34
45
|
/** @typedef {import('./types.js').BridgeResponse} BridgeResponse */
|
|
46
|
+
/** @typedef {import('./types.js').BridgeClientOptions} BridgeClientOptions */
|
|
35
47
|
/** @typedef {import('../../native-host/src/config.js').SupportedBrowser} SupportedBrowser */
|
|
36
48
|
/** @typedef {import('./types.js').BrowserManifestStatus} BrowserManifestStatus */
|
|
37
49
|
/** @typedef {import('./types.js').NativeHostManifestIssue} NativeHostManifestIssue */
|
|
38
50
|
/** @typedef {import('./types.js').DoctorReport} DoctorReport */
|
|
39
51
|
/** @typedef {import('./types.js').DoctorReportOptions} DoctorReportOptions */
|
|
52
|
+
/** @typedef {import('./types.js').DoctorRecentEvent} DoctorRecentEvent */
|
|
40
53
|
|
|
41
54
|
const CHROMIUM_SANDBOXED_MANIFEST_RE =
|
|
42
55
|
/(?:^|[/\\])(?:snap[/\\]chromium|\.var[/\\]app[/\\]org\.chromium\.Chromium)[/\\]/;
|
|
56
|
+
const DOCTOR_LOG_LIMIT = 12;
|
|
57
|
+
const DOCTOR_RECENT_EVENT_LIMIT = 10;
|
|
58
|
+
const DOCTOR_SETUP_ENTRY_LIMIT = 100;
|
|
59
|
+
const MAX_DOCTOR_COUNT = 1_000_000_000;
|
|
60
|
+
const SAFE_VERSION_RE = /^\d+\.\d+(?:\.\d+)?(?:[-+][A-Za-z0-9.-]+)?$/u;
|
|
61
|
+
const CHROME_EXTENSION_ID_RE = /^[a-p]{32}$/u;
|
|
62
|
+
const SAFE_DEBUGGER_STATES = new Set(['idle', 'active', 'conflict', 'detached']);
|
|
63
|
+
const SAFE_ROUTE_REASONS = new Set([
|
|
64
|
+
'enabled',
|
|
65
|
+
'access_disabled',
|
|
66
|
+
'enabled_window_missing',
|
|
67
|
+
'no_routable_active_tab',
|
|
68
|
+
'reconnected',
|
|
69
|
+
'restricted_page',
|
|
70
|
+
]);
|
|
71
|
+
const SAFE_CAPTURE_STATES = new Set([
|
|
72
|
+
'idle',
|
|
73
|
+
'stopped',
|
|
74
|
+
'armed',
|
|
75
|
+
'active',
|
|
76
|
+
'capturing',
|
|
77
|
+
'stop_failed',
|
|
78
|
+
'unavailable',
|
|
79
|
+
]);
|
|
80
|
+
const BRIDGE_METHOD_SET = new Set(Object.keys(BRIDGE_METHOD_REGISTRY));
|
|
81
|
+
const DOCTOR_DIAGNOSTIC_METHODS = new Set([
|
|
82
|
+
'health.ping',
|
|
83
|
+
'setup.get_status',
|
|
84
|
+
'log.tail',
|
|
85
|
+
'daemon.metrics',
|
|
86
|
+
]);
|
|
87
|
+
const SAFE_DEBUGGER_REASONS = new Set([
|
|
88
|
+
'debugger_conflict',
|
|
89
|
+
'debugger_detached',
|
|
90
|
+
'debugger_replaced',
|
|
91
|
+
'debugger_canceled',
|
|
92
|
+
'target_closed',
|
|
93
|
+
]);
|
|
43
94
|
|
|
44
95
|
/**
|
|
45
96
|
* @param {BridgeClient} client
|
|
@@ -55,7 +106,7 @@ export async function ensureClientConnected(client) {
|
|
|
55
106
|
* @param {BridgeClient} client
|
|
56
107
|
* @param {BridgeMethod} method
|
|
57
108
|
* @param {Record<string, unknown>} [params={}]
|
|
58
|
-
* @param {{ tabId?: number | null, source?: BridgeRequestSource, tokenBudget?: number | null }} [options]
|
|
109
|
+
* @param {{ tabId?: number | null, source?: BridgeRequestSource, tokenBudget?: number | null, automaticRetry?: 'mcp_second_attempt' }} [options]
|
|
59
110
|
* @returns {Promise<BridgeResponse>}
|
|
60
111
|
*/
|
|
61
112
|
export async function requestBridge(client, method, params = {}, options = {}) {
|
|
@@ -76,7 +127,7 @@ export async function requestBridge(client, method, params = {}, options = {}) {
|
|
|
76
127
|
method,
|
|
77
128
|
params,
|
|
78
129
|
tabId: methodNeedsTab(method) ? (options.tabId ?? null) : null,
|
|
79
|
-
meta: withRequestMeta(options.source, options.tokenBudget),
|
|
130
|
+
meta: withRequestMeta(options.source, options.tokenBudget, options.automaticRetry),
|
|
80
131
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
81
132
|
});
|
|
82
133
|
}
|
|
@@ -118,9 +169,10 @@ export async function resolveRef(client, refOrSelector, tabId = null, source) {
|
|
|
118
169
|
/**
|
|
119
170
|
* @param {BridgeRequestSource | undefined} source
|
|
120
171
|
* @param {number | null | undefined} tokenBudget
|
|
172
|
+
* @param {'mcp_second_attempt' | undefined} automaticRetry
|
|
121
173
|
* @returns {BridgeMeta}
|
|
122
174
|
*/
|
|
123
|
-
function withRequestMeta(source, tokenBudget) {
|
|
175
|
+
function withRequestMeta(source, tokenBudget, automaticRetry) {
|
|
124
176
|
/** @type {BridgeMeta} */
|
|
125
177
|
const meta = {};
|
|
126
178
|
if (source) {
|
|
@@ -129,16 +181,20 @@ function withRequestMeta(source, tokenBudget) {
|
|
|
129
181
|
if (typeof tokenBudget === 'number' && Number.isFinite(tokenBudget)) {
|
|
130
182
|
meta.token_budget = tokenBudget;
|
|
131
183
|
}
|
|
184
|
+
if (automaticRetry === 'mcp_second_attempt') {
|
|
185
|
+
meta.automatic_retry = { attempt: 2, reason: 'retryable_error' };
|
|
186
|
+
}
|
|
132
187
|
return meta;
|
|
133
188
|
}
|
|
134
189
|
|
|
135
190
|
/**
|
|
136
191
|
* @template T
|
|
137
192
|
* @param {(client: BridgeClient) => Promise<T>} callback
|
|
193
|
+
* @param {BridgeClientOptions} [options]
|
|
138
194
|
* @returns {Promise<T>}
|
|
139
195
|
*/
|
|
140
|
-
export async function withBridgeClient(callback) {
|
|
141
|
-
const client = new BridgeClient();
|
|
196
|
+
export async function withBridgeClient(callback, options) {
|
|
197
|
+
const client = new BridgeClient(await applyConfiguredAutoUpdate(options));
|
|
142
198
|
await ensureClientConnected(client);
|
|
143
199
|
try {
|
|
144
200
|
return await callback(client);
|
|
@@ -188,6 +244,44 @@ export async function checkBrowserManifests() {
|
|
|
188
244
|
);
|
|
189
245
|
}
|
|
190
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Read the allowlisted extension IDs from every installed browser manifest.
|
|
249
|
+
* Invalid manifests are already reported by the native-host health check and
|
|
250
|
+
* do not prevent the remaining browsers from contributing identity data.
|
|
251
|
+
*
|
|
252
|
+
* @param {BrowserManifestStatus[]} browserManifests
|
|
253
|
+
* @param {typeof fs.promises.readFile} [readFile]
|
|
254
|
+
* @returns {Promise<string[]>}
|
|
255
|
+
*/
|
|
256
|
+
export async function readInstalledExtensionIds(
|
|
257
|
+
browserManifests,
|
|
258
|
+
readFile = fs.promises.readFile.bind(fs.promises)
|
|
259
|
+
) {
|
|
260
|
+
/** @type {Set<string>} */
|
|
261
|
+
const extensionIds = new Set();
|
|
262
|
+
for (const entry of browserManifests.slice(0, DOCTOR_SETUP_ENTRY_LIMIT)) {
|
|
263
|
+
if (!entry.installed || extensionIds.size >= DOCTOR_SETUP_ENTRY_LIMIT) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(await readFile(entry.manifestPath, 'utf8'));
|
|
268
|
+
const origins = Array.isArray(parsed?.allowed_origins) ? parsed.allowed_origins : [];
|
|
269
|
+
for (const origin of origins.slice(0, DOCTOR_SETUP_ENTRY_LIMIT)) {
|
|
270
|
+
const match =
|
|
271
|
+
typeof origin === 'string'
|
|
272
|
+
? /^chrome-extension:\/\/([a-p]{32})\/(?:\*)?$/u.exec(origin)
|
|
273
|
+
: null;
|
|
274
|
+
if (match?.[1]) {
|
|
275
|
+
extensionIds.add(match[1]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// The native-host manifest health collector reports malformed files.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return [...extensionIds];
|
|
283
|
+
}
|
|
284
|
+
|
|
191
285
|
/**
|
|
192
286
|
* @param {string} line
|
|
193
287
|
* @param {number} startIndex
|
|
@@ -377,6 +471,405 @@ export async function checkUnwritableBridgePaths() {
|
|
|
377
471
|
return unwritable;
|
|
378
472
|
}
|
|
379
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Doctor must never restart a daemon while it is inspecting version drift.
|
|
476
|
+
*
|
|
477
|
+
* @template T
|
|
478
|
+
* @param {(client: BridgeClient) => Promise<T>} callback
|
|
479
|
+
* @returns {Promise<T>}
|
|
480
|
+
*/
|
|
481
|
+
async function withDoctorBridgeClient(callback) {
|
|
482
|
+
const client = new BridgeClient({ restartDaemonOnVersionMismatch: false });
|
|
483
|
+
try {
|
|
484
|
+
await ensureClientConnected(client);
|
|
485
|
+
return await callback(client);
|
|
486
|
+
} finally {
|
|
487
|
+
await client.close().catch(() => {});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* @param {unknown} value
|
|
493
|
+
* @returns {Record<string, unknown> | null}
|
|
494
|
+
*/
|
|
495
|
+
function asRecord(value) {
|
|
496
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
497
|
+
? /** @type {Record<string, unknown>} */ (value)
|
|
498
|
+
: null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @param {unknown} value
|
|
503
|
+
* @returns {number}
|
|
504
|
+
*/
|
|
505
|
+
function boundedCount(value) {
|
|
506
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
507
|
+
? Math.min(MAX_DOCTOR_COUNT, Math.trunc(value))
|
|
508
|
+
: 0;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @param {unknown} value
|
|
513
|
+
* @returns {string | null}
|
|
514
|
+
*/
|
|
515
|
+
function safeVersion(value) {
|
|
516
|
+
return typeof value === 'string' && value.length <= 32 && SAFE_VERSION_RE.test(value)
|
|
517
|
+
? value
|
|
518
|
+
: null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* @param {unknown} value
|
|
523
|
+
* @returns {string[]}
|
|
524
|
+
*/
|
|
525
|
+
function safeVersions(value) {
|
|
526
|
+
if (!Array.isArray(value)) {
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
/** @type {Set<string>} */
|
|
530
|
+
const versions = new Set();
|
|
531
|
+
for (const candidate of value) {
|
|
532
|
+
const version = safeVersion(candidate);
|
|
533
|
+
if (version) {
|
|
534
|
+
versions.add(version);
|
|
535
|
+
}
|
|
536
|
+
if (versions.size === 10) {
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return [...versions];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* @param {string} left
|
|
545
|
+
* @param {string} right
|
|
546
|
+
* @returns {number}
|
|
547
|
+
*/
|
|
548
|
+
function compareVersions(left, right) {
|
|
549
|
+
const leftParts = left.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
550
|
+
const rightParts = right.split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
551
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
552
|
+
for (let index = 0; index < length; index += 1) {
|
|
553
|
+
const difference = (leftParts[index] || 0) - (rightParts[index] || 0);
|
|
554
|
+
if (difference !== 0) {
|
|
555
|
+
return difference > 0 ? 1 : -1;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return 0;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Convert untrusted daemon log entries into a fixed allowlisted shape. Raw
|
|
563
|
+
* messages are inspected only to derive known cause categories and are never
|
|
564
|
+
* returned.
|
|
565
|
+
*
|
|
566
|
+
* @param {unknown} value
|
|
567
|
+
* @returns {DoctorRecentEvent['cause'] | undefined}
|
|
568
|
+
*/
|
|
569
|
+
function classifyRecentCause(value) {
|
|
570
|
+
const entry = asRecord(value);
|
|
571
|
+
if (!entry) {
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
const error = asRecord(entry.error);
|
|
575
|
+
const candidates = [
|
|
576
|
+
entry.cause,
|
|
577
|
+
entry.reason,
|
|
578
|
+
entry.errorCode,
|
|
579
|
+
entry.code,
|
|
580
|
+
entry.message,
|
|
581
|
+
entry.summary,
|
|
582
|
+
error?.code,
|
|
583
|
+
error?.message,
|
|
584
|
+
]
|
|
585
|
+
.filter((candidate) => typeof candidate === 'string')
|
|
586
|
+
.map((candidate) => candidate.slice(0, 200))
|
|
587
|
+
.join(' ')
|
|
588
|
+
.slice(0, 1000);
|
|
589
|
+
if (/another debugger|debugger.{0,40}(?:already attached|conflict|in use)/iu.test(candidates)) {
|
|
590
|
+
return 'debugger_conflict';
|
|
591
|
+
}
|
|
592
|
+
if (/debugger.{0,40}detach|detach.{0,40}debugger/iu.test(candidates)) {
|
|
593
|
+
return 'debugger_detached';
|
|
594
|
+
}
|
|
595
|
+
if (/DIALOG_ACTION_CONFLICT/iu.test(candidates)) {
|
|
596
|
+
return 'dialog_conflict';
|
|
597
|
+
}
|
|
598
|
+
if (/EXTENSION_DISCONNECTED|extension.{0,40}disconnect/iu.test(candidates)) {
|
|
599
|
+
return 'extension_disconnected';
|
|
600
|
+
}
|
|
601
|
+
if (/TAB_MISMATCH|wrong.{0,20}window|enabled window.{0,30}mismatch/iu.test(candidates)) {
|
|
602
|
+
return 'wrong_window';
|
|
603
|
+
}
|
|
604
|
+
return undefined;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* @param {unknown} value
|
|
609
|
+
* @returns {DoctorRecentEvent[]}
|
|
610
|
+
*/
|
|
611
|
+
function summarizeRecentEvents(value) {
|
|
612
|
+
const result = asRecord(value);
|
|
613
|
+
const entries = Array.isArray(result?.entries)
|
|
614
|
+
? result.entries
|
|
615
|
+
.slice(-DOCTOR_LOG_LIMIT)
|
|
616
|
+
.filter((candidate) => {
|
|
617
|
+
const entry = asRecord(candidate);
|
|
618
|
+
return !DOCTOR_DIAGNOSTIC_METHODS.has(
|
|
619
|
+
typeof entry?.method === 'string' ? entry.method : ''
|
|
620
|
+
);
|
|
621
|
+
})
|
|
622
|
+
.slice(-DOCTOR_RECENT_EVENT_LIMIT)
|
|
623
|
+
: [];
|
|
624
|
+
/** @type {DoctorRecentEvent[]} */
|
|
625
|
+
const events = [];
|
|
626
|
+
for (const candidate of entries) {
|
|
627
|
+
const entry = asRecord(candidate);
|
|
628
|
+
if (!entry) {
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
/** @type {DoctorRecentEvent} */
|
|
632
|
+
const event = {};
|
|
633
|
+
if (typeof entry.at === 'string' && /^\d{4}-\d{2}-\d{2}T/u.test(entry.at)) {
|
|
634
|
+
const timestamp = Date.parse(entry.at);
|
|
635
|
+
if (Number.isFinite(timestamp)) {
|
|
636
|
+
event.at = new Date(timestamp).toISOString();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (typeof entry.method === 'string' && BRIDGE_METHOD_SET.has(entry.method)) {
|
|
640
|
+
event.method = /** @type {BridgeMethod} */ (entry.method);
|
|
641
|
+
}
|
|
642
|
+
if (typeof entry.ok === 'boolean') {
|
|
643
|
+
event.ok = entry.ok;
|
|
644
|
+
}
|
|
645
|
+
if (entry.source === 'cli' || entry.source === 'mcp') {
|
|
646
|
+
event.source = entry.source;
|
|
647
|
+
}
|
|
648
|
+
event.cause = classifyRecentCause(entry);
|
|
649
|
+
if (Object.values(event).some((field) => field !== undefined)) {
|
|
650
|
+
events.push(event);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return events;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* @param {unknown} value
|
|
658
|
+
* @returns {import('./types.js').DoctorDaemonMetrics | null}
|
|
659
|
+
*/
|
|
660
|
+
function summarizeMetrics(value) {
|
|
661
|
+
const metrics = asRecord(value);
|
|
662
|
+
if (!metrics) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
uptimeMs: boundedCount(metrics.uptimeMs),
|
|
667
|
+
activeAgents: boundedCount(metrics.activeAgents),
|
|
668
|
+
activeExtensions: boundedCount(metrics.activeExtensions),
|
|
669
|
+
pendingRequests: boundedCount(metrics.pendingRequests),
|
|
670
|
+
requestsProcessed: boundedCount(metrics.requestsProcessed),
|
|
671
|
+
requestsFailed: boundedCount(metrics.requestsFailed),
|
|
672
|
+
avgResponseTimeMs: boundedCount(metrics.avgResponseTimeMs),
|
|
673
|
+
recovery: normalizeRecoveryTelemetrySummary(metrics.recovery, 'daemon'),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* @param {Record<string, unknown> | null} health
|
|
679
|
+
* @param {import('./types.js').DoctorDaemonMetrics | null} metrics
|
|
680
|
+
* @returns {import('./types.js').DoctorRecoveryDiagnostics}
|
|
681
|
+
*/
|
|
682
|
+
function summarizeRecovery(health, metrics) {
|
|
683
|
+
const recovery = asRecord(health?.recovery);
|
|
684
|
+
const daemon = metrics?.recovery ?? normalizeRecoveryTelemetrySummary(recovery?.daemon, 'daemon');
|
|
685
|
+
const routedExtension = normalizeRecoveryTelemetrySummary(
|
|
686
|
+
recovery?.routedExtension,
|
|
687
|
+
'routedExtension'
|
|
688
|
+
);
|
|
689
|
+
return {
|
|
690
|
+
daemon,
|
|
691
|
+
routedExtension,
|
|
692
|
+
activeLoops: RECOVERY_EVENT_KINDS.filter(
|
|
693
|
+
(kind) =>
|
|
694
|
+
daemon?.events[kind].activeLoop === true ||
|
|
695
|
+
routedExtension?.events[kind].activeLoop === true
|
|
696
|
+
),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* @param {unknown} value
|
|
702
|
+
* @param {'daemon' | 'direct'} source
|
|
703
|
+
* @returns {import('./types.js').DoctorSetupSummary | null}
|
|
704
|
+
*/
|
|
705
|
+
function summarizeSetupStatus(value, source) {
|
|
706
|
+
const status = asRecord(value);
|
|
707
|
+
if (
|
|
708
|
+
(status?.scope !== 'global' && status?.scope !== 'local') ||
|
|
709
|
+
!Array.isArray(status.mcpClients) ||
|
|
710
|
+
!Array.isArray(status.skillTargets)
|
|
711
|
+
) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
const mcpClients = status.mcpClients.slice(0, DOCTOR_SETUP_ENTRY_LIMIT).map(asRecord);
|
|
715
|
+
const skillTargets = status.skillTargets.slice(0, DOCTOR_SETUP_ENTRY_LIMIT).map(asRecord);
|
|
716
|
+
return {
|
|
717
|
+
source,
|
|
718
|
+
scope: status.scope,
|
|
719
|
+
mcp: {
|
|
720
|
+
detected: boundedCount(mcpClients.filter((client) => client?.detected === true).length),
|
|
721
|
+
configured: boundedCount(mcpClients.filter((client) => client?.configured === true).length),
|
|
722
|
+
},
|
|
723
|
+
skills: {
|
|
724
|
+
detected: boundedCount(skillTargets.filter((target) => target?.detected === true).length),
|
|
725
|
+
installed: boundedCount(skillTargets.filter((target) => target?.installed === true).length),
|
|
726
|
+
managed: boundedCount(skillTargets.filter((target) => target?.managed === true).length),
|
|
727
|
+
updatesAvailable: boundedCount(
|
|
728
|
+
skillTargets.filter((target) => target?.updateAvailable === true).length
|
|
729
|
+
),
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* @param {Record<string, unknown> | null} health
|
|
736
|
+
* @returns {import('./types.js').DoctorDebuggerDiagnostics}
|
|
737
|
+
*/
|
|
738
|
+
function summarizeDebugger(health) {
|
|
739
|
+
const debuggerHealth = asRecord(health?.debugger);
|
|
740
|
+
const captureHealth = asRecord(health?.capture);
|
|
741
|
+
const rawState = debuggerHealth?.state ?? debuggerHealth?.status ?? health?.debuggerState;
|
|
742
|
+
/** @type {import('./types.js').DoctorDebuggerDiagnostics['state']} */
|
|
743
|
+
let state =
|
|
744
|
+
typeof rawState === 'string' && SAFE_DEBUGGER_STATES.has(rawState)
|
|
745
|
+
? /** @type {'idle' | 'active' | 'conflict' | 'detached'} */ (rawState)
|
|
746
|
+
: 'unknown';
|
|
747
|
+
if (debuggerHealth?.conflict === true) {
|
|
748
|
+
state = 'conflict';
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const attachedTabs = debuggerHealth?.attachedTabs;
|
|
752
|
+
const attachedTabCount = Array.isArray(attachedTabs)
|
|
753
|
+
? boundedCount(attachedTabs.length)
|
|
754
|
+
: typeof debuggerHealth?.attachedTabCount === 'number'
|
|
755
|
+
? boundedCount(debuggerHealth.attachedTabCount)
|
|
756
|
+
: null;
|
|
757
|
+
const heldTabCount =
|
|
758
|
+
typeof debuggerHealth?.heldTabCount === 'number'
|
|
759
|
+
? boundedCount(debuggerHealth.heldTabCount)
|
|
760
|
+
: null;
|
|
761
|
+
const pendingTabCount =
|
|
762
|
+
typeof debuggerHealth?.pendingTabCount === 'number'
|
|
763
|
+
? boundedCount(debuggerHealth.pendingTabCount)
|
|
764
|
+
: null;
|
|
765
|
+
const recentReason =
|
|
766
|
+
typeof debuggerHealth?.recentReason === 'string' &&
|
|
767
|
+
SAFE_DEBUGGER_REASONS.has(debuggerHealth.recentReason)
|
|
768
|
+
? /** @type {import('./types.js').DoctorDebuggerDiagnostics['recentReason']} */ (
|
|
769
|
+
debuggerHealth.recentReason
|
|
770
|
+
)
|
|
771
|
+
: null;
|
|
772
|
+
const rawCaptureState =
|
|
773
|
+
captureHealth?.state ?? debuggerHealth?.captureState ?? health?.captureState;
|
|
774
|
+
const captureState =
|
|
775
|
+
typeof rawCaptureState === 'string' && SAFE_CAPTURE_STATES.has(rawCaptureState)
|
|
776
|
+
? /** @type {import('./types.js').DoctorDebuggerDiagnostics['captureState']} */ (
|
|
777
|
+
rawCaptureState
|
|
778
|
+
)
|
|
779
|
+
: 'unknown';
|
|
780
|
+
const captureActiveTabCount =
|
|
781
|
+
typeof captureHealth?.activeTabCount === 'number'
|
|
782
|
+
? boundedCount(captureHealth.activeTabCount)
|
|
783
|
+
: null;
|
|
784
|
+
const captureOwnershipCount =
|
|
785
|
+
typeof captureHealth?.ownershipCount === 'number'
|
|
786
|
+
? boundedCount(captureHealth.ownershipCount)
|
|
787
|
+
: null;
|
|
788
|
+
const captureInflightCount =
|
|
789
|
+
typeof captureHealth?.inflightCount === 'number'
|
|
790
|
+
? boundedCount(captureHealth.inflightCount)
|
|
791
|
+
: null;
|
|
792
|
+
const interceptionActiveTabCount =
|
|
793
|
+
typeof captureHealth?.interceptionActiveTabCount === 'number'
|
|
794
|
+
? boundedCount(captureHealth.interceptionActiveTabCount)
|
|
795
|
+
: null;
|
|
796
|
+
const interceptionRuleCount =
|
|
797
|
+
typeof captureHealth?.interceptionRuleCount === 'number'
|
|
798
|
+
? boundedCount(captureHealth.interceptionRuleCount)
|
|
799
|
+
: null;
|
|
800
|
+
return {
|
|
801
|
+
state,
|
|
802
|
+
attachedTabCount,
|
|
803
|
+
heldTabCount,
|
|
804
|
+
pendingTabCount,
|
|
805
|
+
recentReason,
|
|
806
|
+
captureState,
|
|
807
|
+
captureActiveTabCount,
|
|
808
|
+
captureOwnershipCount,
|
|
809
|
+
captureInflightCount,
|
|
810
|
+
interceptionActiveTabCount,
|
|
811
|
+
interceptionRuleCount,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* @param {Record<string, unknown> | null} health
|
|
817
|
+
* @param {boolean} extensionConnected
|
|
818
|
+
* @returns {import('./types.js').DoctorProtocolDiagnostics}
|
|
819
|
+
*/
|
|
820
|
+
function summarizeProtocol(health, extensionConnected) {
|
|
821
|
+
const clientVersion = getProtocolVersion();
|
|
822
|
+
const advertisedDaemonVersions = safeVersions(health?.daemon_supported_versions);
|
|
823
|
+
const daemonSupportedVersions =
|
|
824
|
+
advertisedDaemonVersions.length > 0 || extensionConnected
|
|
825
|
+
? advertisedDaemonVersions
|
|
826
|
+
: safeVersions(health?.supported_versions);
|
|
827
|
+
const extensionSupportedVersions = extensionConnected
|
|
828
|
+
? Array.isArray(health?.extension_supported_versions)
|
|
829
|
+
? safeVersions(health.extension_supported_versions)
|
|
830
|
+
: safeVersions(health?.supported_versions)
|
|
831
|
+
: [];
|
|
832
|
+
const daemonCompatible = daemonSupportedVersions.length
|
|
833
|
+
? daemonSupportedVersions.includes(clientVersion)
|
|
834
|
+
: null;
|
|
835
|
+
const extensionCompatible = extensionConnected
|
|
836
|
+
? extensionSupportedVersions.length
|
|
837
|
+
? extensionSupportedVersions.includes(clientVersion)
|
|
838
|
+
: null
|
|
839
|
+
: null;
|
|
840
|
+
const knownCompatibility = [daemonCompatible, extensionCompatible].filter(
|
|
841
|
+
(value) => value !== null
|
|
842
|
+
);
|
|
843
|
+
const compatible = knownCompatibility.includes(false)
|
|
844
|
+
? false
|
|
845
|
+
: knownCompatibility.length
|
|
846
|
+
? true
|
|
847
|
+
: null;
|
|
848
|
+
|
|
849
|
+
/** @type {import('./types.js').DoctorProtocolDiagnostics['migration']} */
|
|
850
|
+
let migration = compatible === true ? 'none' : 'unknown';
|
|
851
|
+
const latestExtension = extensionSupportedVersions[0];
|
|
852
|
+
const latestDaemon = daemonSupportedVersions[0];
|
|
853
|
+
if (extensionCompatible === false && latestExtension) {
|
|
854
|
+
migration =
|
|
855
|
+
compareVersions(latestExtension, clientVersion) > 0 ? 'update_client' : 'update_extension';
|
|
856
|
+
} else if (daemonCompatible === false && latestDaemon) {
|
|
857
|
+
migration =
|
|
858
|
+
compareVersions(latestDaemon, clientVersion) > 0 ? 'update_client' : 'restart_daemon';
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return {
|
|
862
|
+
clientVersion,
|
|
863
|
+
daemonVersion: safeVersion(health?.daemonVersion),
|
|
864
|
+
daemonSupportedVersions,
|
|
865
|
+
extensionSupportedVersions,
|
|
866
|
+
daemonCompatible,
|
|
867
|
+
extensionCompatible,
|
|
868
|
+
compatible,
|
|
869
|
+
migration,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
380
873
|
/**
|
|
381
874
|
* @param {DoctorReportOptions} [options={}]
|
|
382
875
|
* @returns {Promise<DoctorReport>}
|
|
@@ -404,6 +897,53 @@ export async function getDoctorReport(options = {}) {
|
|
|
404
897
|
CHROMIUM_SANDBOXED_MANIFEST_RE.test(entry.manifestPath)
|
|
405
898
|
);
|
|
406
899
|
|
|
900
|
+
/** @type {'socket' | 'tcp' | 'unknown'} */
|
|
901
|
+
let transportKind = 'unknown';
|
|
902
|
+
let proxyConfigured = false;
|
|
903
|
+
/** @type {string[]} */
|
|
904
|
+
const diagnosticFailures = [];
|
|
905
|
+
/** @type {string[]} */
|
|
906
|
+
let installedExtensionIds = [];
|
|
907
|
+
try {
|
|
908
|
+
installedExtensionIds = await (options.readInstalledExtensionIds || readInstalledExtensionIds)(
|
|
909
|
+
browserManifests
|
|
910
|
+
);
|
|
911
|
+
installedExtensionIds = installedExtensionIds
|
|
912
|
+
.filter((extensionId) => CHROME_EXTENSION_ID_RE.test(extensionId))
|
|
913
|
+
.slice(0, DOCTOR_SETUP_ENTRY_LIMIT);
|
|
914
|
+
} catch {
|
|
915
|
+
diagnosticFailures.push('manifest_identity_unavailable');
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
transportKind = (options.getLocalTransport || getBridgeTransport)().type;
|
|
919
|
+
} catch {
|
|
920
|
+
diagnosticFailures.push('transport_config_unavailable');
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
proxyConfigured = Boolean((options.readProxyConfig || readProxyConfig)());
|
|
924
|
+
} catch {
|
|
925
|
+
diagnosticFailures.push('proxy_config_unavailable');
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** @type {import('./types.js').DoctorRemoteDiagnostics} */
|
|
929
|
+
let remoteDestinations = {
|
|
930
|
+
configuredCount: 0,
|
|
931
|
+
status: 'not_configured',
|
|
932
|
+
credentials: 'not_configured',
|
|
933
|
+
};
|
|
934
|
+
try {
|
|
935
|
+
const remoteConfig = await (options.readRemoteConfig || readRemoteConfig)();
|
|
936
|
+
const configuredCount = remoteConfig.remotes.length;
|
|
937
|
+
remoteDestinations = {
|
|
938
|
+
configuredCount,
|
|
939
|
+
status: configuredCount > 0 ? 'not_probed_local_only' : 'not_configured',
|
|
940
|
+
credentials: configuredCount > 0 ? 'unverified' : 'not_configured',
|
|
941
|
+
};
|
|
942
|
+
} catch {
|
|
943
|
+
diagnosticFailures.push('remote_config_unavailable');
|
|
944
|
+
remoteDestinations.status = 'config_unavailable';
|
|
945
|
+
}
|
|
946
|
+
|
|
407
947
|
/** @type {DoctorReport} */
|
|
408
948
|
const report = {
|
|
409
949
|
manifestInstalled,
|
|
@@ -412,6 +952,7 @@ export async function getDoctorReport(options = {}) {
|
|
|
412
952
|
defaultExtensionId: defaultExtensionId.extensionId,
|
|
413
953
|
defaultExtensionIdSource: defaultExtensionId.source,
|
|
414
954
|
daemonReachable: false,
|
|
955
|
+
healthAvailable: false,
|
|
415
956
|
extensionConnected: false,
|
|
416
957
|
accessEnabled: false,
|
|
417
958
|
enabledWindowId: null,
|
|
@@ -425,35 +966,207 @@ export async function getDoctorReport(options = {}) {
|
|
|
425
966
|
issues: [],
|
|
426
967
|
nextSteps: [],
|
|
427
968
|
browserManifests,
|
|
969
|
+
transport: {
|
|
970
|
+
kind: transportKind,
|
|
971
|
+
local: true,
|
|
972
|
+
status: 'offline',
|
|
973
|
+
proxyConfigured,
|
|
974
|
+
proxyExposed: null,
|
|
975
|
+
credentials: transportKind === 'socket' ? 'not_required' : 'unknown',
|
|
976
|
+
},
|
|
977
|
+
connections: {
|
|
978
|
+
extensionCount: 0,
|
|
979
|
+
profileCount: 0,
|
|
980
|
+
},
|
|
981
|
+
protocol: summarizeProtocol(null, false),
|
|
982
|
+
debugger: summarizeDebugger(null),
|
|
983
|
+
recovery: summarizeRecovery(null, null),
|
|
984
|
+
metrics: null,
|
|
985
|
+
recentEvents: [],
|
|
986
|
+
recentCauses: [],
|
|
987
|
+
setup: null,
|
|
988
|
+
remoteDestinations,
|
|
989
|
+
diagnosticFailures,
|
|
428
990
|
};
|
|
429
991
|
|
|
992
|
+
/** @type {Record<string, unknown> | null} */
|
|
993
|
+
let healthResult = null;
|
|
994
|
+
/** @type {unknown} */
|
|
995
|
+
let setupStatus = null;
|
|
996
|
+
/** @type {Record<string, unknown> | null} */
|
|
997
|
+
let metricsResult = null;
|
|
998
|
+
/** @type {Record<string, unknown> | null} */
|
|
999
|
+
let logsResult = null;
|
|
1000
|
+
/** @type {unknown} */
|
|
1001
|
+
let connectionError = null;
|
|
1002
|
+
|
|
430
1003
|
try {
|
|
431
|
-
await (options.bridgeClientRunner ||
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
1004
|
+
await (options.bridgeClientRunner || withDoctorBridgeClient)(async (client) => {
|
|
1005
|
+
report.daemonReachable = true;
|
|
1006
|
+
/**
|
|
1007
|
+
* @param {BridgeMethod} method
|
|
1008
|
+
* @param {Record<string, unknown>} [params]
|
|
1009
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
1010
|
+
*/
|
|
1011
|
+
async function requestDiagnostic(method, params = {}) {
|
|
1012
|
+
try {
|
|
1013
|
+
const response = await client.request({ method, params });
|
|
1014
|
+
if (!response.ok) {
|
|
1015
|
+
diagnosticFailures.push(`${method}_failed`);
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
report.daemonReachable = true;
|
|
1019
|
+
return asRecord(response.result);
|
|
1020
|
+
} catch {
|
|
1021
|
+
diagnosticFailures.push(`${method}_failed`);
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
healthResult = await requestDiagnostic('health.ping');
|
|
1027
|
+
if (options.includeSetupStatus !== false) {
|
|
1028
|
+
const setupResult = await requestDiagnostic('setup.get_status');
|
|
1029
|
+
if (setupResult) {
|
|
1030
|
+
setupStatus = setupResult;
|
|
1031
|
+
}
|
|
435
1032
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
windowId?: number | null,
|
|
439
|
-
routeTabId?: number | null,
|
|
440
|
-
routeReady?: boolean,
|
|
441
|
-
reason?: string
|
|
442
|
-
} }} */ (response.result);
|
|
443
|
-
report.daemonReachable = result.daemon === 'ok';
|
|
444
|
-
report.extensionConnected = result.extensionConnected === true;
|
|
445
|
-
report.accessEnabled = result.access?.enabled === true;
|
|
446
|
-
report.enabledWindowId =
|
|
447
|
-
typeof result.access?.windowId === 'number' ? result.access.windowId : null;
|
|
448
|
-
report.routeTabId =
|
|
449
|
-
typeof result.access?.routeTabId === 'number' ? result.access.routeTabId : null;
|
|
450
|
-
report.routeReady = result.access?.routeReady === true;
|
|
451
|
-
report.routeReason =
|
|
452
|
-
typeof result.access?.reason === 'string' ? result.access.reason : 'access_disabled';
|
|
1033
|
+
logsResult = await requestDiagnostic('log.tail', { limit: DOCTOR_LOG_LIMIT });
|
|
1034
|
+
metricsResult = await requestDiagnostic('daemon.metrics');
|
|
453
1035
|
});
|
|
454
|
-
} catch {
|
|
455
|
-
|
|
456
|
-
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
connectionError = error;
|
|
1038
|
+
diagnosticFailures.push('daemon_connection_failed');
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (!report.daemonReachable && options.includeSetupStatus !== false) {
|
|
1042
|
+
try {
|
|
1043
|
+
setupStatus = await (options.collectSetupStatus || collectSetupStatus)();
|
|
1044
|
+
const summary = summarizeSetupStatus(setupStatus, 'direct');
|
|
1045
|
+
if (summary) {
|
|
1046
|
+
report.setup = summary;
|
|
1047
|
+
} else {
|
|
1048
|
+
diagnosticFailures.push('setup_status_invalid');
|
|
1049
|
+
}
|
|
1050
|
+
} catch {
|
|
1051
|
+
diagnosticFailures.push('setup_direct_failed');
|
|
1052
|
+
}
|
|
1053
|
+
} else if (setupStatus) {
|
|
1054
|
+
const summary = summarizeSetupStatus(setupStatus, 'daemon');
|
|
1055
|
+
if (summary) {
|
|
1056
|
+
report.setup = summary;
|
|
1057
|
+
} else {
|
|
1058
|
+
diagnosticFailures.push('setup_status_invalid');
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const connectionMessage =
|
|
1063
|
+
connectionError instanceof Error ? connectionError.message.slice(0, 200) : '';
|
|
1064
|
+
const authenticationFailed = /authentication failed|access denied|invalid.{0,20}token/iu.test(
|
|
1065
|
+
connectionMessage
|
|
1066
|
+
);
|
|
1067
|
+
report.transport.status = authenticationFailed
|
|
1068
|
+
? 'authentication_failed'
|
|
1069
|
+
: report.daemonReachable
|
|
1070
|
+
? 'reachable'
|
|
1071
|
+
: 'offline';
|
|
1072
|
+
report.transport.credentials =
|
|
1073
|
+
transportKind === 'socket'
|
|
1074
|
+
? 'not_required'
|
|
1075
|
+
: authenticationFailed
|
|
1076
|
+
? 'rejected'
|
|
1077
|
+
: report.daemonReachable
|
|
1078
|
+
? 'accepted'
|
|
1079
|
+
: 'unknown';
|
|
1080
|
+
|
|
1081
|
+
const resolvedHealth = /** @type {Record<string, unknown> | null} */ (
|
|
1082
|
+
/** @type {unknown} */ (healthResult)
|
|
1083
|
+
);
|
|
1084
|
+
report.healthAvailable = resolvedHealth?.daemon === 'ok';
|
|
1085
|
+
if (
|
|
1086
|
+
report.daemonReachable &&
|
|
1087
|
+
!report.healthAvailable &&
|
|
1088
|
+
!diagnosticFailures.includes('health.ping_failed')
|
|
1089
|
+
) {
|
|
1090
|
+
diagnosticFailures.push('health.ping_invalid');
|
|
1091
|
+
}
|
|
1092
|
+
const healthAccess = report.healthAvailable ? asRecord(resolvedHealth?.access) : null;
|
|
1093
|
+
if (report.healthAvailable) {
|
|
1094
|
+
report.extensionConnected = resolvedHealth?.extensionConnected === true;
|
|
1095
|
+
report.accessEnabled = healthAccess?.enabled === true;
|
|
1096
|
+
report.enabledWindowId =
|
|
1097
|
+
typeof healthAccess?.windowId === 'number' ? boundedCount(healthAccess.windowId) : null;
|
|
1098
|
+
report.routeTabId =
|
|
1099
|
+
typeof healthAccess?.routeTabId === 'number' ? boundedCount(healthAccess.routeTabId) : null;
|
|
1100
|
+
report.routeReady = healthAccess?.routeReady === true;
|
|
1101
|
+
report.routeReason =
|
|
1102
|
+
typeof healthAccess?.reason === 'string' && SAFE_ROUTE_REASONS.has(healthAccess.reason)
|
|
1103
|
+
? healthAccess.reason
|
|
1104
|
+
: 'access_disabled';
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const connectedExtensions =
|
|
1108
|
+
report.healthAvailable && Array.isArray(resolvedHealth?.connectedExtensions)
|
|
1109
|
+
? resolvedHealth.connectedExtensions.slice(0, 100)
|
|
1110
|
+
: [];
|
|
1111
|
+
let profileCount = 0;
|
|
1112
|
+
for (const extension of connectedExtensions) {
|
|
1113
|
+
const extensionRecord = asRecord(extension);
|
|
1114
|
+
if (typeof extensionRecord?.profileLabel === 'string' && extensionRecord.profileLabel) {
|
|
1115
|
+
profileCount += 1;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
report.metrics = summarizeMetrics(metricsResult);
|
|
1119
|
+
report.connections.extensionCount = connectedExtensions.length
|
|
1120
|
+
? connectedExtensions.length
|
|
1121
|
+
: report.metrics?.activeExtensions || (report.extensionConnected ? 1 : 0);
|
|
1122
|
+
report.connections.profileCount = profileCount;
|
|
1123
|
+
if (report.healthAvailable && report.connections.extensionCount > 0) {
|
|
1124
|
+
report.extensionConnected = true;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const proxyHealth = report.healthAvailable ? asRecord(resolvedHealth?.proxy) : null;
|
|
1128
|
+
report.transport.proxyConfigured = proxyConfigured;
|
|
1129
|
+
report.transport.proxyExposed =
|
|
1130
|
+
typeof proxyHealth?.enabled === 'boolean' ? proxyHealth.enabled : null;
|
|
1131
|
+
report.protocol = summarizeProtocol(
|
|
1132
|
+
report.healthAvailable ? resolvedHealth : null,
|
|
1133
|
+
report.extensionConnected
|
|
1134
|
+
);
|
|
1135
|
+
report.recentEvents = summarizeRecentEvents(logsResult);
|
|
1136
|
+
report.recentCauses = [
|
|
1137
|
+
...new Set(
|
|
1138
|
+
report.recentEvents.map((event) => event.cause).filter((cause) => cause !== undefined)
|
|
1139
|
+
),
|
|
1140
|
+
];
|
|
1141
|
+
report.debugger = summarizeDebugger(report.healthAvailable ? resolvedHealth : null);
|
|
1142
|
+
report.recovery = summarizeRecovery(
|
|
1143
|
+
report.healthAvailable ? resolvedHealth : null,
|
|
1144
|
+
report.metrics
|
|
1145
|
+
);
|
|
1146
|
+
if (report.debugger.recentReason && !report.recentCauses.includes(report.debugger.recentReason)) {
|
|
1147
|
+
report.recentCauses.push(report.debugger.recentReason);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Keep optional diagnostics deterministic even if several RPCs fail.
|
|
1151
|
+
report.diagnosticFailures = [...new Set(diagnosticFailures)].slice(0, 12);
|
|
1152
|
+
|
|
1153
|
+
const connectedBrowserExtensionIds = connectedExtensions
|
|
1154
|
+
.map((extension) => asRecord(extension)?.browserExtensionId)
|
|
1155
|
+
.filter(
|
|
1156
|
+
(extensionId) => typeof extensionId === 'string' && CHROME_EXTENSION_ID_RE.test(extensionId)
|
|
1157
|
+
);
|
|
1158
|
+
const extensionIdentityMismatch =
|
|
1159
|
+
connectedBrowserExtensionIds.length > 0 &&
|
|
1160
|
+
installedExtensionIds.length > 0 &&
|
|
1161
|
+
connectedBrowserExtensionIds.some(
|
|
1162
|
+
(extensionId) => !installedExtensionIds.includes(/** @type {string} */ (extensionId))
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
if (authenticationFailed) {
|
|
1166
|
+
report.issues.push('proxy_credentials_stale');
|
|
1167
|
+
report.nextSteps.push(
|
|
1168
|
+
'Local TCP authentication failed. Run `bbx proxy enable --rotate-token`, restart the daemon, and update configured remote clients with `bbx remote add`.'
|
|
1169
|
+
);
|
|
457
1170
|
}
|
|
458
1171
|
|
|
459
1172
|
if (unwritableBridgePaths.length > 0) {
|
|
@@ -482,11 +1195,23 @@ export async function getDoctorReport(options = {}) {
|
|
|
482
1195
|
: 'Run `bbx install <extension-id>` (or `bbx install --all`) to install the native host manifest.'
|
|
483
1196
|
);
|
|
484
1197
|
}
|
|
485
|
-
if (!report.daemonReachable) {
|
|
1198
|
+
if (!report.daemonReachable && !authenticationFailed) {
|
|
486
1199
|
report.issues.push('daemon_offline');
|
|
487
1200
|
report.nextSteps.push('Run `bbx-daemon` and retry `bbx status` or `bbx doctor`.');
|
|
488
1201
|
}
|
|
489
|
-
if (report.daemonReachable && !report.
|
|
1202
|
+
if (report.daemonReachable && !report.healthAvailable) {
|
|
1203
|
+
report.issues.push('health_unavailable');
|
|
1204
|
+
report.nextSteps.push(
|
|
1205
|
+
'The daemon is reachable but `health.ping` did not return valid core health. Run `bbx restart`, then update the Browser Bridge CLI and extension if the health check still fails.'
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
if (report.healthAvailable && extensionIdentityMismatch) {
|
|
1209
|
+
report.issues.push('extension_id_mismatch');
|
|
1210
|
+
report.nextSteps.push(
|
|
1211
|
+
'A connected Chrome extension ID is not allowed by any installed native host manifest. Reinstall the matching browser manifest with `bbx install --browser <browser>` and reload that extension.'
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
if (report.healthAvailable && !report.extensionConnected) {
|
|
490
1215
|
report.issues.push('extension_disconnected');
|
|
491
1216
|
if (chromiumSandboxedManifestInstalled) {
|
|
492
1217
|
report.issues.push('chromium_sandboxed_native_host_limited');
|
|
@@ -495,20 +1220,120 @@ export async function getDoctorReport(options = {}) {
|
|
|
495
1220
|
);
|
|
496
1221
|
}
|
|
497
1222
|
report.nextSteps.push(
|
|
498
|
-
'Open Chrome and make sure the Browser Bridge extension is installed and active.'
|
|
1223
|
+
'Open Chrome and make sure the Browser Bridge extension is installed and active. If Chrome reports a missing native host, run `bbx install --all` and reload the extension.'
|
|
499
1224
|
);
|
|
500
1225
|
}
|
|
501
|
-
if (report.
|
|
502
|
-
report.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1226
|
+
if (report.healthAvailable && report.extensionConnected && !report.accessEnabled) {
|
|
1227
|
+
if (report.routeReason === 'enabled_window_missing') {
|
|
1228
|
+
report.issues.push('enabled_window_missing');
|
|
1229
|
+
report.nextSteps.push(
|
|
1230
|
+
'The previously enabled window no longer exists. Focus the intended Chrome window and click Enable in the Browser Bridge popup or side panel.'
|
|
1231
|
+
);
|
|
1232
|
+
} else {
|
|
1233
|
+
report.issues.push('access_disabled');
|
|
1234
|
+
report.nextSteps.push(
|
|
1235
|
+
'If a Browser Bridge call returns ACCESS_DENIED, stop requesting access. Ask the user to focus the needed Chrome window and click Enable for the needed window, then tell you when that window is ready.'
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
} else if (report.healthAvailable && report.extensionConnected && !report.routeReady) {
|
|
507
1239
|
report.issues.push(report.routeReason || 'no_routable_active_tab');
|
|
508
1240
|
report.nextSteps.push(
|
|
509
1241
|
'Switch to a supported page in the enabled window, or use an explicit tabId override.'
|
|
510
1242
|
);
|
|
511
1243
|
}
|
|
512
1244
|
|
|
513
|
-
|
|
1245
|
+
if (report.protocol.compatible === false) {
|
|
1246
|
+
report.issues.push('protocol_mismatch');
|
|
1247
|
+
if (report.protocol.migration === 'update_extension') {
|
|
1248
|
+
report.nextSteps.push(
|
|
1249
|
+
'Update or reload the Browser Bridge extension so it supports the client protocol, then retry `bbx doctor`.'
|
|
1250
|
+
);
|
|
1251
|
+
} else if (report.protocol.migration === 'restart_daemon') {
|
|
1252
|
+
report.nextSteps.push(
|
|
1253
|
+
'Run `bbx restart` to replace the stale daemon with this installed CLI version, then retry `bbx doctor`.'
|
|
1254
|
+
);
|
|
1255
|
+
} else {
|
|
1256
|
+
report.nextSteps.push(
|
|
1257
|
+
'Update the Browser Bridge CLI/npm package, run `bbx restart`, and retry `bbx doctor`.'
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (report.debugger.state === 'conflict') {
|
|
1263
|
+
report.issues.push('debugger_conflict');
|
|
1264
|
+
}
|
|
1265
|
+
if (report.debugger.state === 'conflict' || report.recentCauses.includes('debugger_conflict')) {
|
|
1266
|
+
report.nextSteps.push(
|
|
1267
|
+
'Close DevTools or any other debugger attached to the target tab, then retry the debugger-backed Browser Bridge operation.'
|
|
1268
|
+
);
|
|
1269
|
+
} else if (
|
|
1270
|
+
report.debugger.state === 'detached' ||
|
|
1271
|
+
report.recentCauses.includes('debugger_detached')
|
|
1272
|
+
) {
|
|
1273
|
+
report.nextSteps.push(
|
|
1274
|
+
'A recent debugger session detached. Keep the target tab open and retry the debugger-backed operation; close competing debuggers if detaches continue.'
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (report.recentCauses.includes('wrong_window')) {
|
|
1279
|
+
report.nextSteps.push(
|
|
1280
|
+
'A recent request targeted the wrong window. Focus the intended Chrome window, click Enable there, and retry without reusing a tabId from another window.'
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
for (const loop of report.recovery.activeLoops) {
|
|
1285
|
+
const issue = `${loop}_loop`;
|
|
1286
|
+
if (!report.issues.includes(issue)) report.issues.push(issue);
|
|
1287
|
+
if (loop === 'stale_ref_recovery') {
|
|
1288
|
+
report.nextSteps.push(
|
|
1289
|
+
'Stale-reference recovery is repeatedly failing. Re-query the target for a fresh elementRef and retry once without reusing old refs.'
|
|
1290
|
+
);
|
|
1291
|
+
} else if (
|
|
1292
|
+
loop === 'debugger_reattach' &&
|
|
1293
|
+
report.debugger.state !== 'conflict' &&
|
|
1294
|
+
!report.recentCauses.includes('debugger_conflict')
|
|
1295
|
+
) {
|
|
1296
|
+
report.nextSteps.push(
|
|
1297
|
+
'Debugger reattachment is repeatedly failing. Keep the tab open, close competing debuggers, and retry one debugger-backed operation.'
|
|
1298
|
+
);
|
|
1299
|
+
} else if (loop === 'content_script_reinjection') {
|
|
1300
|
+
report.nextSteps.push(
|
|
1301
|
+
'Content-script reinjection is repeatedly failing. Reload the Browser Bridge extension and target page, then enable the window again.'
|
|
1302
|
+
);
|
|
1303
|
+
} else if (loop === 'native_host_reconnect' && !authenticationFailed) {
|
|
1304
|
+
report.nextSteps.push(
|
|
1305
|
+
'Native-host reconnects are repeatedly failing. Run `bbx restart`; for remote TCP proxy use, verify the endpoint and rotate stale credentials with `bbx proxy enable --rotate-token`.'
|
|
1306
|
+
);
|
|
1307
|
+
} else if (loop === 'automatic_mcp_retry') {
|
|
1308
|
+
report.nextSteps.push(
|
|
1309
|
+
'Automatic MCP retries are repeatedly failing. Stop the retry loop, resolve the reported bridge readiness issue, then retry the original read once.'
|
|
1310
|
+
);
|
|
1311
|
+
} else if (loop === 'request_outcome') {
|
|
1312
|
+
report.nextSteps.push(
|
|
1313
|
+
'Browser requests are repeatedly failing. Inspect the first typed bridge error, narrow the request or target as directed, and retry once after correcting that specific failure.'
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
return {
|
|
1319
|
+
...report,
|
|
1320
|
+
manifestPath: sanitizeDoctorPath(report.manifestPath),
|
|
1321
|
+
daemonLogPath: sanitizeDoctorPath(report.daemonLogPath),
|
|
1322
|
+
unwritableBridgePaths: report.unwritableBridgePaths.map(sanitizeDoctorPath),
|
|
1323
|
+
nativeHostManifestIssues: report.nativeHostManifestIssues.map((issue) => ({
|
|
1324
|
+
...issue,
|
|
1325
|
+
manifestPath: sanitizeDoctorPath(issue.manifestPath),
|
|
1326
|
+
message: sanitizeIncidentalText(issue.message),
|
|
1327
|
+
})),
|
|
1328
|
+
browserManifests: report.browserManifests.map((browser) => ({
|
|
1329
|
+
...browser,
|
|
1330
|
+
manifestPath: sanitizeDoctorPath(browser.manifestPath),
|
|
1331
|
+
})),
|
|
1332
|
+
nextSteps: report.nextSteps.map((step) => sanitizeIncidentalText(step)),
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/** @param {string} value */
|
|
1337
|
+
function sanitizeDoctorPath(value) {
|
|
1338
|
+
return value ? sanitizeIncidentalPath(value) : '';
|
|
514
1339
|
}
|