@openscout/scout 0.2.72 → 0.2.73

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.
Files changed (46) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +41 -7
  3. package/bin/scout +34 -0
  4. package/bin/scout.mjs +195 -18
  5. package/bin/scoutd +0 -0
  6. package/dist/client/apple-touch-icon.png +0 -0
  7. package/dist/client/assets/RepoDiffViewer-D8gg36W3.js +56 -0
  8. package/dist/client/assets/{addon-fit-C5ueiijZ.js → addon-fit-D4KdY-pj.js} +1 -1
  9. package/dist/client/assets/{addon-webgl-Cm7sl8RO.js → addon-webgl-Bo_tmKBp.js} +1 -1
  10. package/dist/client/assets/arc.es-D0gujIKy.js +188 -0
  11. package/dist/client/assets/embed-entry-CUVNwyB_.js +1 -0
  12. package/dist/client/assets/index-CRkjiso5.js +257 -0
  13. package/dist/client/assets/index-D_RX2cRb.css +1 -0
  14. package/dist/client/assets/{xterm-FFux-PdF.js → xterm-RfflDFod.js} +1 -1
  15. package/dist/client/favicon-16.png +0 -0
  16. package/dist/client/favicon-32.png +0 -0
  17. package/dist/client/favicon.ico +0 -0
  18. package/dist/client/favicon.svg +48 -0
  19. package/dist/client/index.html +8 -2
  20. package/dist/client/openscout-icon.png +0 -0
  21. package/dist/client/site.webmanifest +19 -0
  22. package/dist/client/web-app-icon-192.png +0 -0
  23. package/dist/client/web-app-icon-512.png +0 -0
  24. package/dist/drizzle/0000_curly_iron_monger.sql +592 -0
  25. package/dist/drizzle/0001_invocation-status-columns.sql +7 -0
  26. package/dist/drizzle/0002_invocation-flight-metadata.sql +21 -0
  27. package/dist/drizzle/README.md +81 -0
  28. package/dist/drizzle/meta/0000_snapshot.json +4575 -0
  29. package/dist/drizzle/meta/0001_snapshot.json +4624 -0
  30. package/dist/drizzle/meta/0002_snapshot.json +4631 -0
  31. package/dist/drizzle/meta/_journal.json +27 -0
  32. package/dist/main.mjs +72583 -60970
  33. package/dist/node/main.mjs +7607 -0
  34. package/dist/openscout-terminal-relay.mjs +3819 -158
  35. package/dist/{pair-supervisor.mjs → pairing-runtime-controller.mjs} +51525 -46801
  36. package/dist/runtime/base-daemon.mjs +2153 -502
  37. package/dist/runtime/broker-daemon.mjs +34730 -27036
  38. package/dist/runtime/broker-process-manager.mjs +2038 -421
  39. package/dist/runtime/mesh-discover.mjs +2002 -420
  40. package/dist/scout-control-plane-web.mjs +54522 -39921
  41. package/dist/scout-web-server.mjs +54522 -39921
  42. package/dist/statusline.mjs +287 -0
  43. package/package.json +7 -3
  44. package/dist/client/assets/RepoDiffViewer-DRzXs-TP.js +0 -1
  45. package/dist/client/assets/index-CYs6TkOv.js +0 -191
  46. package/dist/client/assets/index-n_fpvwq8.css +0 -1
@@ -1,12 +1,13 @@
1
1
  // @bun
2
2
  // packages/runtime/src/broker-process-manager.ts
3
- import { spawnSync } from "child_process";
4
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
5
- import { homedir as homedir3 } from "os";
6
- import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
3
+ import { spawn as spawn2, spawnSync } from "child_process";
4
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
5
+ import { homedir as homedir5 } from "os";
6
+ import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
 
9
9
  // packages/runtime/src/broker-api.ts
10
+ import { existsSync } from "fs";
10
11
  import { request as httpRequest } from "http";
11
12
  var DEFAULT_BROKER_JSON_REQUEST_TIMEOUT_MS = 30000;
12
13
  function normalizeBaseUrl(baseUrl) {
@@ -55,13 +56,17 @@ async function requestBrokerOverHttp(baseUrl, path, options) {
55
56
  }
56
57
  function shouldFallbackFromUnixSocket(error) {
57
58
  const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
58
- return code === "ENOENT" || code === "ECONNREFUSED" || code === "ENOTSOCK" || code === "EACCES" || code === "FailedToOpenSocket";
59
+ return code === "ENOENT" || code === "ECONNREFUSED" || code === "ENOTSOCK" || code === "EACCES" || code === "EINVAL" || code === "FailedToOpenSocket";
59
60
  }
60
61
  function errorMessage(error) {
61
62
  return error instanceof Error ? error.message : String(error);
62
63
  }
63
64
  function requestBrokerOverUnixSocket(socketPath, baseUrl, path, options) {
64
65
  return new Promise((resolve, reject) => {
66
+ if (!existsSync(socketPath)) {
67
+ reject(Object.assign(new Error(`Scout broker socket does not exist: ${socketPath}`), { code: "ENOENT" }));
68
+ return;
69
+ }
65
70
  const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
66
71
  const url = new URL(path, normalizedBaseUrl);
67
72
  const body = requestBody(options);
@@ -142,100 +147,1585 @@ async function requestBrokerWire(baseUrl, path, options) {
142
147
  }
143
148
  }
144
149
  return {
145
- response: await requestBrokerOverHttp(baseUrl, path, requestOptions),
146
- trace: {
147
- transport: "http"
148
- }
150
+ response: await requestBrokerOverHttp(baseUrl, path, requestOptions),
151
+ trace: {
152
+ transport: "http"
153
+ }
154
+ };
155
+ }
156
+ async function requestScoutBrokerJsonWithTrace(baseUrl, path, options = {}) {
157
+ const { response, trace } = await requestBrokerWire(baseUrl, path, options);
158
+ let parsed;
159
+ let parsedJson = false;
160
+ if (response.text.length > 0) {
161
+ try {
162
+ parsed = JSON.parse(response.text);
163
+ parsedJson = true;
164
+ } catch {
165
+ parsedJson = false;
166
+ }
167
+ }
168
+ if (!response.ok) {
169
+ if (parsedJson && options.acceptErrorJson?.(parsed)) {
170
+ return { value: parsed, trace };
171
+ }
172
+ throw new Error(`${path} returned ${response.status}: ${response.text}`);
173
+ }
174
+ if (parsedJson) {
175
+ return { value: parsed, trace };
176
+ }
177
+ return { value: undefined, trace };
178
+ }
179
+
180
+ // packages/runtime/src/support-paths.ts
181
+ import { homedir } from "os";
182
+ import { join } from "path";
183
+ var OPENSCOUT_RPC_CUTOVER_MARKER = "rpc-runtime-cutover-v1";
184
+ function resolveOpenScoutSupportPaths() {
185
+ const home = homedir();
186
+ const supportDirectory = process.env.OPENSCOUT_SUPPORT_DIRECTORY ?? join(home, "Library", "Application Support", "OpenScout");
187
+ const logsDirectory = join(supportDirectory, "logs");
188
+ const runtimeDirectory = join(supportDirectory, "runtime");
189
+ const catalogDirectory = join(supportDirectory, "catalog");
190
+ const controlHome = process.env.OPENSCOUT_CONTROL_HOME ?? join(home, ".openscout", "control-plane");
191
+ const knowledgeDirectory = join(controlHome, "knowledge");
192
+ return {
193
+ supportDirectory,
194
+ logsDirectory,
195
+ appLogsDirectory: join(logsDirectory, "app"),
196
+ brokerLogsDirectory: join(logsDirectory, "broker"),
197
+ runtimeDirectory,
198
+ catalogDirectory,
199
+ relayAgentsDirectory: join(runtimeDirectory, "agents"),
200
+ settingsPath: join(supportDirectory, "settings.json"),
201
+ harnessCatalogPath: join(catalogDirectory, "harness-catalog.json"),
202
+ relayAgentsRegistryPath: join(supportDirectory, "relay-agents.json"),
203
+ managedInstallsPath: join(supportDirectory, "managed-installs.json"),
204
+ hostInfoPath: join(supportDirectory, ".host-info"),
205
+ relayHubDirectory: process.env.OPENSCOUT_RELAY_HUB ?? join(home, ".openscout", "relay"),
206
+ controlHome,
207
+ knowledgeDirectory,
208
+ knowledgeQmdDirectory: join(knowledgeDirectory, "qmd"),
209
+ knowledgeSqlitePath: join(knowledgeDirectory, "knowledge.sqlite"),
210
+ desktopStatusPath: join(supportDirectory, "agent-status.json"),
211
+ workspaceStatePath: join(supportDirectory, "workspace-state.json"),
212
+ cutoverMarkerPath: join(supportDirectory, OPENSCOUT_RPC_CUTOVER_MARKER)
213
+ };
214
+ }
215
+
216
+ // packages/runtime/src/open-scout-network.ts
217
+ import { readFileSync } from "fs";
218
+ var DEFAULT_OPENSCOUT_NETWORK_RENDEZVOUS_URL = "https://mesh.oscout.net";
219
+ var DEFAULT_OPENSCOUT_NETWORK_PAIRING_RELAY_URL = "wss://mesh.oscout.net/v1/relay";
220
+ function defaultOpenScoutNetworkSettings() {
221
+ return {
222
+ discoveryEnabled: false,
223
+ rendezvousUrl: DEFAULT_OPENSCOUT_NETWORK_RENDEZVOUS_URL,
224
+ pairingRelayUrl: DEFAULT_OPENSCOUT_NETWORK_PAIRING_RELAY_URL,
225
+ keepPairingRelayRunning: true
226
+ };
227
+ }
228
+ function normalizeOpenScoutNetworkSettings(input) {
229
+ const base = defaultOpenScoutNetworkSettings();
230
+ const record = isRecord(input) ? input : {};
231
+ return {
232
+ discoveryEnabled: typeof record.discoveryEnabled === "boolean" ? record.discoveryEnabled : base.discoveryEnabled,
233
+ rendezvousUrl: normalizeUrlString(record.rendezvousUrl, base.rendezvousUrl),
234
+ pairingRelayUrl: normalizeUrlString(record.pairingRelayUrl, base.pairingRelayUrl),
235
+ keepPairingRelayRunning: typeof record.keepPairingRelayRunning === "boolean" ? record.keepPairingRelayRunning : base.keepPairingRelayRunning
236
+ };
237
+ }
238
+ function readOpenScoutNetworkSettingsSync() {
239
+ try {
240
+ const raw = readFileSync(resolveOpenScoutSupportPaths().settingsPath, "utf8");
241
+ const parsed = JSON.parse(raw);
242
+ const network = isRecord(parsed) && isRecord(parsed.network) ? parsed.network : {};
243
+ return normalizeOpenScoutNetworkSettings(isRecord(network.openScoutNetwork) ? network.openScoutNetwork : {});
244
+ } catch {
245
+ return defaultOpenScoutNetworkSettings();
246
+ }
247
+ }
248
+ function openScoutNetworkDiscoveryEnabled(env = process.env) {
249
+ const explicit = readBooleanEnv(env.OPENSCOUT_NETWORK_DISCOVERY_ENABLED) ?? readBooleanEnv(env.OPENSCOUT_OSN_DISCOVERY_ENABLED);
250
+ if (explicit !== undefined) {
251
+ return explicit;
252
+ }
253
+ return readOpenScoutNetworkSettingsSync().discoveryEnabled;
254
+ }
255
+ function normalizeUrlString(value, fallback) {
256
+ if (typeof value !== "string") {
257
+ return fallback;
258
+ }
259
+ const trimmed = value.trim().replace(/\/$/, "");
260
+ return trimmed || fallback;
261
+ }
262
+ function readBooleanEnv(value) {
263
+ const normalized = value?.trim().toLowerCase();
264
+ if (!normalized)
265
+ return;
266
+ if (["1", "true", "yes", "on"].includes(normalized))
267
+ return true;
268
+ if (["0", "false", "no", "off"].includes(normalized))
269
+ return false;
270
+ return;
271
+ }
272
+ function isRecord(value) {
273
+ return typeof value === "object" && value !== null && !Array.isArray(value);
274
+ }
275
+
276
+ // packages/runtime/src/tailscale.ts
277
+ import { readFileSync as readFileSync2 } from "fs";
278
+ import { execFileSync } from "child_process";
279
+
280
+ // packages/runtime/src/system-probes/tailscale-status.ts
281
+ import { readFile } from "fs/promises";
282
+
283
+ // packages/runtime/src/system-probes/registry.ts
284
+ var DEFAULT_MIN_MAX_STALE_MS = 2 * 60000;
285
+ var PROBE_RUN_OUTPUT = Symbol("openscout.probeRunOutput");
286
+ var registeredProbes = [];
287
+
288
+ class ProbeBackendError extends Error {
289
+ backend;
290
+ fallbackSince;
291
+ fallbackReason;
292
+ code;
293
+ timedOut;
294
+ constructor(message, metadata, cause) {
295
+ super(message);
296
+ this.name = "ProbeBackendError";
297
+ this.backend = metadata.backend;
298
+ this.fallbackSince = metadata.fallbackSince;
299
+ this.fallbackReason = metadata.fallbackReason;
300
+ const details = probeErrorDetails(cause);
301
+ if (details.code) {
302
+ this.code = details.code;
303
+ }
304
+ if (details.timedOut !== undefined) {
305
+ this.timedOut = details.timedOut;
306
+ }
307
+ }
308
+ }
309
+ function probeRunOutput(value, metadata) {
310
+ return {
311
+ [PROBE_RUN_OUTPUT]: true,
312
+ value,
313
+ ...metadata
314
+ };
315
+ }
316
+ function isProbeRunOutput(value) {
317
+ return typeof value === "object" && value !== null && value[PROBE_RUN_OUTPUT] === true;
318
+ }
319
+ function backendMetadataFromError(error) {
320
+ if (error instanceof ProbeBackendError) {
321
+ return {
322
+ backend: error.backend,
323
+ fallbackSince: error.fallbackSince,
324
+ fallbackReason: error.fallbackReason
325
+ };
326
+ }
327
+ if (typeof error === "object" && error !== null) {
328
+ const record = error;
329
+ if (record.backend === "local" || record.backend === "scoutd" || record.backend === "local-fallback") {
330
+ return {
331
+ backend: record.backend,
332
+ fallbackSince: typeof record.fallbackSince === "number" ? record.fallbackSince : undefined,
333
+ fallbackReason: typeof record.fallbackReason === "string" ? record.fallbackReason : undefined
334
+ };
335
+ }
336
+ }
337
+ return null;
338
+ }
339
+ function probeErrorDetails(error) {
340
+ if (typeof error !== "object" || error === null) {
341
+ return {};
342
+ }
343
+ const record = error;
344
+ const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : undefined;
345
+ const timedOut = record.timedOut === true || code === "timeout" ? true : record.timedOut === false ? false : undefined;
346
+ return { code, timedOut };
347
+ }
348
+ function assertProbeSpec(spec) {
349
+ if (!spec.id.trim()) {
350
+ throw new Error("Probe id is required");
351
+ }
352
+ if (!Number.isFinite(spec.ttlMs) || spec.ttlMs <= 0) {
353
+ throw new Error(`Probe ${spec.id} must declare a positive ttlMs`);
354
+ }
355
+ if (!Number.isFinite(spec.timeoutMs) || spec.timeoutMs <= 0) {
356
+ throw new Error(`Probe ${spec.id} must declare a positive timeoutMs`);
357
+ }
358
+ if (spec.maxStaleMs !== undefined && (!Number.isFinite(spec.maxStaleMs) || spec.maxStaleMs <= 0)) {
359
+ throw new Error(`Probe ${spec.id} maxStaleMs must be positive when set`);
360
+ }
361
+ }
362
+ function maxStaleMsFor(spec) {
363
+ return spec.maxStaleMs ?? Math.max(DEFAULT_MIN_MAX_STALE_MS, spec.ttlMs * 10);
364
+ }
365
+ function failureBackoffMs(consecutiveFailures) {
366
+ if (consecutiveFailures <= 0) {
367
+ return 0;
368
+ }
369
+ return Math.min(30000, 1000 * 2 ** Math.min(consecutiveFailures - 1, 5));
370
+ }
371
+ function probeErrorFromUnknown(error, at, timedOut = false) {
372
+ if (typeof error === "object" && error !== null) {
373
+ const record = error;
374
+ const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : timedOut ? "timeout" : typeof record.name === "string" && record.name.trim() ? record.name.trim() : "error";
375
+ const message = typeof record.message === "string" && record.message.trim() ? record.message : String(error);
376
+ return { code, message, at, ...timedOut ? { timedOut: true } : {} };
377
+ }
378
+ return {
379
+ code: timedOut ? "timeout" : "error",
380
+ message: String(error),
381
+ at,
382
+ ...timedOut ? { timedOut: true } : {}
383
+ };
384
+ }
385
+ function timeoutError(probeId, timeoutMs) {
386
+ return {
387
+ code: "timeout",
388
+ message: `Probe ${probeId} timed out after ${timeoutMs}ms`,
389
+ at: Date.now(),
390
+ timedOut: true
391
+ };
392
+ }
393
+ function staleTooLongError(probeId, ageMs, maxStaleMs) {
394
+ return {
395
+ code: "max_stale_exceeded",
396
+ message: `Probe ${probeId} last good snapshot is ${ageMs}ms old, exceeding maxStaleMs ${maxStaleMs}`,
397
+ at: Date.now()
398
+ };
399
+ }
400
+ function isFreshEnough(state, maxAgeMs) {
401
+ if (state.at === null || state.invalidated) {
402
+ return false;
403
+ }
404
+ return Date.now() - state.at <= maxAgeMs;
405
+ }
406
+ function logBackendTransition(input) {
407
+ if (input.from === input.to) {
408
+ return;
409
+ }
410
+ const keySuffix = input.key === undefined ? "" : ` key=${JSON.stringify(input.key)}`;
411
+ const reasonSuffix = input.reason ? ` (${input.reason})` : "";
412
+ console.warn(`[openscout] system probe ${input.id}${keySuffix} backend ${input.from} -> ${input.to}${reasonSuffix}`);
413
+ }
414
+
415
+ class ProbeInstance {
416
+ spec;
417
+ key;
418
+ runProbe;
419
+ scheduleRun;
420
+ state = {
421
+ value: null,
422
+ at: null,
423
+ error: null,
424
+ consecutiveFailures: 0,
425
+ inFlight: null,
426
+ invalidated: false,
427
+ invalidationReason: null,
428
+ invalidationSerial: 0,
429
+ nextRetryAt: 0,
430
+ backend: "local",
431
+ fallbackSince: null,
432
+ fallbackReason: null
433
+ };
434
+ metricState;
435
+ lastAccessAt = Date.now();
436
+ constructor(options) {
437
+ this.spec = options.spec;
438
+ this.key = options.key;
439
+ this.runProbe = options.run;
440
+ this.scheduleRun = options.scheduleRun ?? ((task) => task());
441
+ this.metricState = {
442
+ id: options.spec.id,
443
+ ...options.key !== undefined ? { key: options.key } : {},
444
+ backend: "local",
445
+ runCount: 0,
446
+ failureCount: 0,
447
+ timeoutCount: 0,
448
+ staleServedCount: 0,
449
+ lastRunAt: null,
450
+ lastDurationMs: null,
451
+ lastSuccessAt: null
452
+ };
453
+ }
454
+ read() {
455
+ this.touch();
456
+ if (this.shouldRefreshForRead(Date.now())) {
457
+ this.ensureRefresh(false, this.spec.ttlMs);
458
+ }
459
+ const snap = this.snapshot();
460
+ if (snap.status === "stale" || snap.status === "failed" && snap.at !== null) {
461
+ this.metricState.staleServedCount += 1;
462
+ }
463
+ return snap;
464
+ }
465
+ async fresh(options = {}) {
466
+ this.touch();
467
+ const maxAgeMs = options.maxAgeMs ?? this.spec.ttlMs;
468
+ for (let attempt = 0;attempt < 3; attempt += 1) {
469
+ if (isFreshEnough(this.state, maxAgeMs)) {
470
+ return this.snapshot();
471
+ }
472
+ await this.ensureRefresh(true, maxAgeMs);
473
+ if (!this.state.invalidated) {
474
+ return this.snapshot();
475
+ }
476
+ }
477
+ return this.snapshot();
478
+ }
479
+ snapshot() {
480
+ const now = Date.now();
481
+ const ageMs = this.state.at === null ? null : Math.max(0, now - this.state.at);
482
+ const maxStaleMs = maxStaleMsFor(this.spec);
483
+ const exceededMaxStale = ageMs !== null && ageMs > maxStaleMs;
484
+ const fresh = ageMs !== null && !this.state.invalidated && ageMs <= this.spec.ttlMs;
485
+ const status = this.state.at === null ? this.state.error ? "failed" : "empty" : exceededMaxStale ? "failed" : fresh ? "fresh" : "stale";
486
+ const error = exceededMaxStale ? this.state.error ?? staleTooLongError(this.spec.id, ageMs ?? 0, maxStaleMs) : this.state.error;
487
+ return {
488
+ id: this.spec.id,
489
+ ...this.key !== undefined ? { key: this.key } : {},
490
+ value: status === "failed" && exceededMaxStale ? null : this.state.value,
491
+ at: this.state.at,
492
+ ageMs,
493
+ stale: status === "stale" || status === "failed" && this.state.at !== null,
494
+ refreshing: this.state.inFlight !== null,
495
+ status,
496
+ error,
497
+ consecutiveFailures: this.state.consecutiveFailures,
498
+ backend: this.state.backend,
499
+ ...this.state.fallbackSince !== null ? { fallbackSince: this.state.fallbackSince } : {},
500
+ ...this.state.fallbackReason !== null ? { fallbackReason: this.state.fallbackReason } : {}
501
+ };
502
+ }
503
+ invalidate(reason) {
504
+ this.touch();
505
+ this.state.invalidated = true;
506
+ this.state.invalidationReason = reason ?? null;
507
+ this.state.invalidationSerial += 1;
508
+ this.state.nextRetryAt = 0;
509
+ }
510
+ metrics() {
511
+ return {
512
+ ...this.metricState,
513
+ consecutiveFailures: this.state.consecutiveFailures,
514
+ inFlight: this.state.inFlight !== null,
515
+ ...this.state.fallbackSince !== null ? { fallbackSince: this.state.fallbackSince } : {},
516
+ ...this.state.fallbackReason !== null ? { fallbackReason: this.state.fallbackReason } : {}
517
+ };
518
+ }
519
+ isRefreshing() {
520
+ return this.state.inFlight !== null;
521
+ }
522
+ touch() {
523
+ this.lastAccessAt = Date.now();
524
+ }
525
+ shouldRefreshForRead(now) {
526
+ if (this.state.inFlight !== null) {
527
+ return false;
528
+ }
529
+ if (this.state.nextRetryAt > now) {
530
+ return false;
531
+ }
532
+ if (this.state.at === null) {
533
+ return true;
534
+ }
535
+ if (this.state.invalidated) {
536
+ return true;
537
+ }
538
+ return now - this.state.at > this.spec.ttlMs;
539
+ }
540
+ ensureRefresh(force, maxAgeMs) {
541
+ if (this.state.inFlight) {
542
+ return this.state.inFlight;
543
+ }
544
+ const now = Date.now();
545
+ if (!force && this.state.nextRetryAt > now) {
546
+ return Promise.resolve();
547
+ }
548
+ const inFlight = this.scheduleRun(() => this.executeRun(maxAgeMs)).finally(() => {
549
+ if (this.state.inFlight === inFlight) {
550
+ this.state.inFlight = null;
551
+ }
552
+ });
553
+ this.state.inFlight = inFlight;
554
+ return inFlight;
555
+ }
556
+ async executeRun(maxAgeMs) {
557
+ const startedAt = Date.now();
558
+ const previousBackend = this.state.backend;
559
+ const invalidationSerialAtStart = this.state.invalidationSerial;
560
+ const controller = new AbortController;
561
+ let timeout = null;
562
+ this.metricState.runCount += 1;
563
+ this.metricState.lastRunAt = startedAt;
564
+ const ctx = {
565
+ probeId: this.spec.id,
566
+ ...this.key !== undefined ? { key: this.key } : {},
567
+ signal: controller.signal,
568
+ timeoutMs: this.spec.timeoutMs,
569
+ maxAgeMs,
570
+ startedAt
571
+ };
572
+ const timeoutProbeError = timeoutError(this.spec.id, this.spec.timeoutMs);
573
+ const runPromise = this.runProbe(ctx);
574
+ runPromise.catch(() => {
575
+ return;
576
+ });
577
+ try {
578
+ const result = await new Promise((resolve, reject) => {
579
+ timeout = setTimeout(() => {
580
+ controller.abort(timeoutProbeError);
581
+ reject(timeoutProbeError);
582
+ }, this.spec.timeoutMs);
583
+ runPromise.then(resolve, reject);
584
+ });
585
+ const output = isProbeRunOutput(result) ? result : probeRunOutput(result, { backend: "local" });
586
+ this.state.value = output.value;
587
+ this.state.at = output.generatedAt ?? Date.now();
588
+ this.state.error = null;
589
+ this.state.consecutiveFailures = 0;
590
+ if (this.state.invalidationSerial === invalidationSerialAtStart) {
591
+ this.state.invalidated = false;
592
+ this.state.invalidationReason = null;
593
+ }
594
+ this.state.nextRetryAt = 0;
595
+ this.state.backend = output.backend;
596
+ this.state.fallbackSince = output.fallbackSince ?? null;
597
+ this.state.fallbackReason = output.fallbackReason ?? null;
598
+ this.metricState.backend = output.backend;
599
+ this.metricState.lastSuccessAt = this.state.at;
600
+ logBackendTransition({
601
+ id: this.spec.id,
602
+ key: this.key,
603
+ from: previousBackend,
604
+ to: output.backend,
605
+ reason: output.fallbackReason
606
+ });
607
+ } catch (error) {
608
+ const timedOut = error === timeoutProbeError || typeof error === "object" && error !== null && (error.code === "timeout" || error.timedOut === true);
609
+ const at = Date.now();
610
+ this.state.error = error === timeoutProbeError ? timeoutProbeError : probeErrorFromUnknown(error, at, timedOut);
611
+ this.state.consecutiveFailures += 1;
612
+ this.state.nextRetryAt = at + failureBackoffMs(this.state.consecutiveFailures);
613
+ const backendMetadata = backendMetadataFromError(error);
614
+ if (backendMetadata) {
615
+ this.state.backend = backendMetadata.backend;
616
+ this.state.fallbackSince = backendMetadata.fallbackSince ?? null;
617
+ this.state.fallbackReason = backendMetadata.fallbackReason ?? null;
618
+ this.metricState.backend = backendMetadata.backend;
619
+ logBackendTransition({
620
+ id: this.spec.id,
621
+ key: this.key,
622
+ from: previousBackend,
623
+ to: backendMetadata.backend,
624
+ reason: backendMetadata.fallbackReason
625
+ });
626
+ }
627
+ this.metricState.failureCount += 1;
628
+ if (this.state.error.timedOut || this.state.error.code === "timeout") {
629
+ this.metricState.timeoutCount += 1;
630
+ }
631
+ } finally {
632
+ if (timeout) {
633
+ clearTimeout(timeout);
634
+ }
635
+ this.metricState.lastDurationMs = Date.now() - startedAt;
636
+ }
637
+ }
638
+ }
639
+
640
+ class FamilyLimiter {
641
+ maxConcurrent;
642
+ active = 0;
643
+ queue = [];
644
+ constructor(maxConcurrent) {
645
+ this.maxConcurrent = maxConcurrent;
646
+ }
647
+ queued() {
648
+ return this.queue.length;
649
+ }
650
+ async run(task) {
651
+ if (this.active >= this.maxConcurrent) {
652
+ await new Promise((resolve) => this.queue.push(resolve));
653
+ }
654
+ this.active += 1;
655
+ try {
656
+ return await task();
657
+ } finally {
658
+ this.active -= 1;
659
+ const next = this.queue.shift();
660
+ if (next) {
661
+ next();
662
+ }
663
+ }
664
+ }
665
+ }
666
+
667
+ class ProbeFamily {
668
+ spec;
669
+ entries = new Map;
670
+ limiter;
671
+ constructor(spec) {
672
+ this.spec = spec;
673
+ assertProbeSpec(spec);
674
+ if (!Number.isInteger(spec.maxKeys) || spec.maxKeys <= 0) {
675
+ throw new Error(`Probe family ${spec.id} must declare a positive maxKeys`);
676
+ }
677
+ if (!Number.isFinite(spec.idleKeyTtlMs) || spec.idleKeyTtlMs <= 0) {
678
+ throw new Error(`Probe family ${spec.id} must declare a positive idleKeyTtlMs`);
679
+ }
680
+ if (!Number.isInteger(spec.maxConcurrentKeys) || spec.maxConcurrentKeys <= 0) {
681
+ throw new Error(`Probe family ${spec.id} must declare a positive maxConcurrentKeys`);
682
+ }
683
+ this.limiter = new FamilyLimiter(spec.maxConcurrentKeys);
684
+ }
685
+ for(rawKey) {
686
+ const key = this.normalize(rawKey);
687
+ this.cleanupIdle(Date.now());
688
+ let entry = this.entries.get(key);
689
+ if (!entry) {
690
+ entry = new ProbeInstance({
691
+ spec: this.spec,
692
+ key,
693
+ run: (ctx) => this.spec.run(key, ctx),
694
+ scheduleRun: (task) => this.limiter.run(task)
695
+ });
696
+ this.entries.set(key, entry);
697
+ }
698
+ entry.lastAccessAt = Date.now();
699
+ this.evictLruIfNeeded();
700
+ return entry;
701
+ }
702
+ snapshot(key) {
703
+ return this.for(key).snapshot();
704
+ }
705
+ invalidate(key, reason) {
706
+ this.for(key).invalidate(reason);
707
+ }
708
+ metrics() {
709
+ this.cleanupIdle(Date.now());
710
+ return {
711
+ id: this.spec.id,
712
+ keyCount: this.entries.size,
713
+ maxKeys: this.spec.maxKeys,
714
+ activeRuns: this.limiter.active,
715
+ queuedRuns: this.limiter.queued(),
716
+ keys: Array.from(this.entries.values(), (entry) => entry.metrics())
717
+ };
718
+ }
719
+ keys() {
720
+ this.cleanupIdle(Date.now());
721
+ return Array.from(this.entries.keys());
722
+ }
723
+ normalize(rawKey) {
724
+ const key = this.spec.normalizeKey(rawKey).trim();
725
+ if (!key) {
726
+ throw new Error(`Probe family ${this.spec.id} normalized an empty key`);
727
+ }
728
+ return key;
729
+ }
730
+ cleanupIdle(now) {
731
+ for (const [key, entry] of this.entries) {
732
+ if (!entry.isRefreshing() && now - entry.lastAccessAt > this.spec.idleKeyTtlMs) {
733
+ this.entries.delete(key);
734
+ }
735
+ }
736
+ }
737
+ evictLruIfNeeded() {
738
+ while (this.entries.size > this.spec.maxKeys) {
739
+ let oldestKey = null;
740
+ let oldestAccess = Number.POSITIVE_INFINITY;
741
+ for (const [key, entry] of this.entries) {
742
+ if (entry.isRefreshing()) {
743
+ continue;
744
+ }
745
+ if (entry.lastAccessAt < oldestAccess) {
746
+ oldestAccess = entry.lastAccessAt;
747
+ oldestKey = key;
748
+ }
749
+ }
750
+ if (!oldestKey) {
751
+ return;
752
+ }
753
+ this.entries.delete(oldestKey);
754
+ }
755
+ }
756
+ }
757
+ function defineProbe(spec) {
758
+ assertProbeSpec(spec);
759
+ const handle = new ProbeInstance({
760
+ spec,
761
+ run: spec.run
762
+ });
763
+ registeredProbes.push({
764
+ kind: "probe",
765
+ id: spec.id,
766
+ handle
767
+ });
768
+ return handle;
769
+ }
770
+
771
+ // packages/runtime/src/system-probes/exec.ts
772
+ import { spawn } from "child_process";
773
+
774
+ // packages/runtime/src/system-probes/scoutd-client.ts
775
+ import { existsSync as existsSync2 } from "fs";
776
+ import { homedir as homedir2 } from "os";
777
+ import { join as join2 } from "path";
778
+ import { Socket } from "net";
779
+
780
+ // packages/runtime/src/system-probes/scout-host-catalog.ts
781
+ var SCOUT_HOST_PROBE_SCHEMA_VERSIONS = {
782
+ "tailscale.status": 1,
783
+ "git.buildInfo": 1,
784
+ "git.revParse": 1,
785
+ "git.diffShortstat": 1,
786
+ "git.statusPorcelain": 1,
787
+ "git.mergeBase": 1,
788
+ "git.logLastCommitUnix": 1,
789
+ "git.worktreeListPorcelain": 1,
790
+ "tmux.sessions": 1,
791
+ "tmux.panes": 1,
792
+ "zellij.sessions": 1,
793
+ "ps.runtime": 1,
794
+ "ps.discovery": 1,
795
+ "ps.cwd": 1,
796
+ "net.listeners": 1,
797
+ "repo.scan": 1,
798
+ "repo.diff": 1
799
+ };
800
+ var SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS = {
801
+ "tmux.sendKeys": 1,
802
+ "tmux.sendKeysLiteral": 1,
803
+ "tmux.loadBuffer": 1,
804
+ "tmux.pasteBuffer": 1,
805
+ "tmux.deleteBuffer": 1,
806
+ "tmux.killSession": 1,
807
+ "tmux.newSession": 1,
808
+ "tmux.detachClient": 1,
809
+ "tailscale.cert": 1,
810
+ "reveal.open": 1
811
+ };
812
+ function expectedScoutHostProbeSchemaVersion(probeId) {
813
+ return Object.hasOwn(SCOUT_HOST_PROBE_SCHEMA_VERSIONS, probeId) ? SCOUT_HOST_PROBE_SCHEMA_VERSIONS[probeId] : null;
814
+ }
815
+ function expectedScoutHostExecVerbSchemaVersion(verb) {
816
+ return Object.hasOwn(SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS, verb) ? SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS[verb] : null;
817
+ }
818
+
819
+ // packages/runtime/src/system-probes/scoutd-client.ts
820
+ var CAPABILITIES_SCHEMA = "openscout.probe.capabilities/v1";
821
+ var REQUEST_SCHEMA = "openscout.probe.request/v1";
822
+ var SNAPSHOT_SCHEMA = "openscout.probe.snapshot/v1";
823
+ var ERROR_SCHEMA = "openscout.probe.error/v1";
824
+ var EXEC_REQUEST_SCHEMA = "openscout.exec.request/v1";
825
+ var EXEC_RESPONSE_SCHEMA = "openscout.exec.response/v1";
826
+ var CAPABILITY_RECHECK_MS = 1e4;
827
+ var SOCKET_TIMEOUT_MS = 900;
828
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
829
+ function readString(value) {
830
+ return typeof value === "string" && value.trim() ? value.trim() : null;
831
+ }
832
+ function readNumber(value) {
833
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
834
+ }
835
+ function isRecord2(value) {
836
+ return typeof value === "object" && value !== null && !Array.isArray(value);
837
+ }
838
+ function fallbackMessage(error) {
839
+ if (error instanceof Error && error.message.trim()) {
840
+ return error.message;
841
+ }
842
+ return String(error);
843
+ }
844
+
845
+ class ScoutdExecResponseError extends Error {
846
+ code;
847
+ timedOut;
848
+ constructor(message, options = {}) {
849
+ super(message);
850
+ this.name = "ScoutdExecResponseError";
851
+ this.code = options.code ?? "exec_error";
852
+ this.timedOut = options.timedOut ?? false;
853
+ }
854
+ }
855
+
856
+ class ScoutdProbeResponseError extends Error {
857
+ code;
858
+ timedOut;
859
+ constructor(message, options = {}) {
860
+ super(message);
861
+ this.name = "ScoutdProbeResponseError";
862
+ this.code = options.code ?? "probe_error";
863
+ this.timedOut = options.timedOut ?? false;
864
+ }
865
+ }
866
+ function resolveScoutdProbesSocketPath(env = process.env) {
867
+ const explicit = env.OPENSCOUT_PROBES_SOCKET?.trim();
868
+ if (explicit) {
869
+ return explicit;
870
+ }
871
+ const openScoutHome = env.OPENSCOUT_HOME?.trim() || join2(homedir2(), ".openscout");
872
+ return join2(openScoutHome, "run", "scoutd-probes.sock");
873
+ }
874
+
875
+ class ScoutdProbeClient {
876
+ env;
877
+ capabilities = null;
878
+ lastCapabilityCheckAt = null;
879
+ lastError = null;
880
+ daemonObserved = false;
881
+ fallbackByProbe = new Map;
882
+ fallbackByExec = new Map;
883
+ constructor(env = process.env) {
884
+ this.env = env;
885
+ }
886
+ async requestProbe(input) {
887
+ const socketPath = resolveScoutdProbesSocketPath(this.env);
888
+ const socketTimeoutMs = probeSocketTimeoutMs(input.timeoutMs);
889
+ const socketExists = existsSync2(socketPath);
890
+ if (!socketExists) {
891
+ this.capabilities = null;
892
+ this.lastError = null;
893
+ return this.daemonObserved ? this.fallback(input.probeId, input.key, `probe socket is missing: ${socketPath}`) : { state: "local" };
894
+ }
895
+ this.daemonObserved = true;
896
+ const capabilities = await this.ensureCapabilities(socketPath, socketTimeoutMs);
897
+ if (!capabilities) {
898
+ return this.fallback(input.probeId, input.key, this.lastError ?? "probe capabilities unavailable");
899
+ }
900
+ const capability = capabilities.families.get(input.probeId);
901
+ if (!capability) {
902
+ return this.fallback(input.probeId, input.key, `scoutd does not serve ${input.probeId}`);
903
+ }
904
+ const expectedSchemaVersion = expectedScoutHostProbeSchemaVersion(input.probeId);
905
+ if (expectedSchemaVersion === null) {
906
+ return this.fallback(input.probeId, input.key, `client has no compiled schema version for ${input.probeId}`);
907
+ }
908
+ if (capability.schemaVersion !== expectedSchemaVersion) {
909
+ return this.fallback(input.probeId, input.key, `scoutd serves ${input.probeId} schema v${capability.schemaVersion}, expected v${expectedSchemaVersion}`);
910
+ }
911
+ try {
912
+ const response = await requestJson(socketPath, {
913
+ schema: REQUEST_SCHEMA,
914
+ schemaVersion: expectedSchemaVersion,
915
+ probeId: input.probeId,
916
+ key: input.key ?? null,
917
+ maxAgeMs: input.maxAgeMs,
918
+ opTimeoutMs: requestOpTimeoutMs(input.timeoutMs)
919
+ }, { timeoutMs: socketTimeoutMs });
920
+ if (!isRecord2(response) || response.schema !== SNAPSHOT_SCHEMA) {
921
+ throw new Error("scoutd returned an invalid probe snapshot envelope");
922
+ }
923
+ if (response.error) {
924
+ const error = isRecord2(response.error) ? response.error : {};
925
+ const code = readString(error.code) ?? "probe_error";
926
+ const message = readString(error.message) ?? code;
927
+ const timedOut = error.timedOut === true || error.timed_out === true;
928
+ throw new ScoutdProbeResponseError(message, { code, timedOut });
929
+ }
930
+ this.fallbackByProbe.delete(fallbackKey(input.probeId, input.key));
931
+ this.lastError = null;
932
+ return {
933
+ state: "scoutd",
934
+ value: response.value,
935
+ generatedAt: readNumber(response.generatedAt) ?? Date.now(),
936
+ daemonVersion: readString(response.daemonVersion) ?? capabilities.daemonVersion
937
+ };
938
+ } catch (error) {
939
+ if (!(error instanceof ScoutdProbeResponseError)) {
940
+ this.capabilities = null;
941
+ this.lastCapabilityCheckAt = null;
942
+ }
943
+ this.lastError = fallbackMessage(error);
944
+ return this.fallback(input.probeId, input.key, this.lastError);
945
+ }
946
+ }
947
+ async requestExecVerb(input) {
948
+ const socketPath = resolveScoutdProbesSocketPath(this.env);
949
+ const socketTimeoutMs = execSocketTimeoutMs(input.args);
950
+ const socketExists = existsSync2(socketPath);
951
+ if (!socketExists) {
952
+ this.capabilities = null;
953
+ this.lastError = null;
954
+ return this.daemonObserved ? this.execFallback(input.verb, `probe socket is missing: ${socketPath}`) : { state: "local" };
955
+ }
956
+ this.daemonObserved = true;
957
+ const capabilities = await this.ensureCapabilities(socketPath, socketTimeoutMs);
958
+ if (!capabilities) {
959
+ return this.execFallback(input.verb, this.lastError ?? "probe capabilities unavailable");
960
+ }
961
+ const capability = capabilities.verbs.get(input.verb);
962
+ if (!capability) {
963
+ return this.execFallback(input.verb, `scoutd does not serve ${input.verb}`);
964
+ }
965
+ const expectedSchemaVersion = expectedScoutHostExecVerbSchemaVersion(input.verb);
966
+ if (expectedSchemaVersion === null) {
967
+ return this.execFallback(input.verb, `client has no compiled schema version for ${input.verb}`);
968
+ }
969
+ if (capability.schemaVersion !== expectedSchemaVersion) {
970
+ return this.execFallback(input.verb, `scoutd serves ${input.verb} schema v${capability.schemaVersion}, expected v${expectedSchemaVersion}`);
971
+ }
972
+ try {
973
+ const response = await requestJson(socketPath, {
974
+ schema: EXEC_REQUEST_SCHEMA,
975
+ schemaVersion: expectedSchemaVersion,
976
+ verb: input.verb,
977
+ args: input.args
978
+ }, { timeoutMs: socketTimeoutMs });
979
+ if (!isRecord2(response) || response.schema !== EXEC_RESPONSE_SCHEMA) {
980
+ throw new Error("scoutd returned an invalid exec response envelope");
981
+ }
982
+ if (!response.ok) {
983
+ const error = isRecord2(response.error) ? response.error : {};
984
+ const code = readString(error.code) ?? "exec_error";
985
+ const message = readString(error.message) ?? code;
986
+ const timedOut = error.timedOut === true || error.timed_out === true;
987
+ throw new ScoutdExecResponseError(message, { code, timedOut });
988
+ }
989
+ this.fallbackByExec.delete(input.verb);
990
+ this.lastError = null;
991
+ return {
992
+ state: "scoutd",
993
+ value: response.value,
994
+ daemonVersion: readString(response.daemonVersion) ?? capabilities.daemonVersion
995
+ };
996
+ } catch (error) {
997
+ if (error instanceof ScoutdExecResponseError) {
998
+ throw error;
999
+ }
1000
+ this.capabilities = null;
1001
+ this.lastCapabilityCheckAt = null;
1002
+ this.lastError = fallbackMessage(error);
1003
+ return this.execFallback(input.verb, this.lastError);
1004
+ }
1005
+ }
1006
+ diagnostics() {
1007
+ const socketPath = resolveScoutdProbesSocketPath(this.env);
1008
+ return {
1009
+ socketPath,
1010
+ socketExists: existsSync2(socketPath),
1011
+ daemonObserved: this.daemonObserved,
1012
+ daemonVersion: this.capabilities?.daemonVersion ?? null,
1013
+ supportedProbeIds: this.capabilities ? [...this.capabilities.families.keys()].sort() : [],
1014
+ supportedExecVerbs: this.capabilities ? [...this.capabilities.verbs.keys()].sort() : [],
1015
+ lastCapabilityCheckAt: this.lastCapabilityCheckAt,
1016
+ lastError: this.lastError
1017
+ };
1018
+ }
1019
+ resetForTests() {
1020
+ this.capabilities = null;
1021
+ this.lastCapabilityCheckAt = null;
1022
+ this.lastError = null;
1023
+ this.daemonObserved = false;
1024
+ this.fallbackByProbe.clear();
1025
+ this.fallbackByExec.clear();
1026
+ }
1027
+ async ensureCapabilities(socketPath, timeoutMs = SOCKET_TIMEOUT_MS) {
1028
+ const now = Date.now();
1029
+ if (this.capabilities && this.lastCapabilityCheckAt !== null && now - this.lastCapabilityCheckAt < CAPABILITY_RECHECK_MS) {
1030
+ return this.capabilities;
1031
+ }
1032
+ this.lastCapabilityCheckAt = now;
1033
+ try {
1034
+ const response = await requestJson(socketPath, { schema: CAPABILITIES_SCHEMA }, { timeoutMs });
1035
+ if (!isRecord2(response) || response.schema !== CAPABILITIES_SCHEMA) {
1036
+ throw new Error("scoutd returned an invalid capabilities envelope");
1037
+ }
1038
+ const daemonVersion = readString(response.daemonVersion) ?? "unknown";
1039
+ const families = new Map;
1040
+ if (Array.isArray(response.families)) {
1041
+ for (const entry of response.families) {
1042
+ if (!isRecord2(entry))
1043
+ continue;
1044
+ const probeId = readString(entry.probeId);
1045
+ const schemaVersion = readNumber(entry.schemaVersion);
1046
+ const ttlMs = readNumber(entry.ttlMs);
1047
+ if (probeId && schemaVersion !== null && ttlMs !== null) {
1048
+ families.set(probeId, { probeId, schemaVersion, ttlMs });
1049
+ }
1050
+ }
1051
+ }
1052
+ const verbs = new Map;
1053
+ if (Array.isArray(response.verbs)) {
1054
+ for (const entry of response.verbs) {
1055
+ if (!isRecord2(entry))
1056
+ continue;
1057
+ const verb = readString(entry.verb);
1058
+ const schemaVersion = readNumber(entry.schemaVersion);
1059
+ if (verb && schemaVersion !== null) {
1060
+ verbs.set(verb, { verb, schemaVersion });
1061
+ }
1062
+ }
1063
+ }
1064
+ this.capabilities = { daemonVersion, families, verbs };
1065
+ this.daemonObserved = true;
1066
+ this.lastError = null;
1067
+ return this.capabilities;
1068
+ } catch (error) {
1069
+ this.capabilities = null;
1070
+ this.lastError = fallbackMessage(error);
1071
+ return null;
1072
+ }
1073
+ }
1074
+ fallback(probeId, key, reason) {
1075
+ const id = fallbackKey(probeId, key);
1076
+ let state = this.fallbackByProbe.get(id);
1077
+ if (!state || state.reason !== reason) {
1078
+ state = { since: Date.now(), reason };
1079
+ this.fallbackByProbe.set(id, state);
1080
+ }
1081
+ return {
1082
+ state: "local",
1083
+ fallbackSince: state.since,
1084
+ fallbackReason: state.reason
1085
+ };
1086
+ }
1087
+ execFallback(verb, reason) {
1088
+ let state = this.fallbackByExec.get(verb);
1089
+ if (!state || state.reason !== reason) {
1090
+ state = { since: Date.now(), reason };
1091
+ this.fallbackByExec.set(verb, state);
1092
+ }
1093
+ return {
1094
+ state: "local",
1095
+ fallbackSince: state.since,
1096
+ fallbackReason: state.reason
1097
+ };
1098
+ }
1099
+ }
1100
+ var singletonClient = null;
1101
+ function getScoutdProbeClient() {
1102
+ singletonClient ??= new ScoutdProbeClient;
1103
+ return singletonClient;
1104
+ }
1105
+ async function runWithScoutdFallback(input) {
1106
+ const client = getScoutdProbeClient();
1107
+ const scoutd = await client.requestProbe({
1108
+ probeId: input.probeId,
1109
+ key: input.key,
1110
+ maxAgeMs: input.ctx.maxAgeMs,
1111
+ timeoutMs: input.ctx.timeoutMs
1112
+ });
1113
+ if (scoutd.state === "scoutd") {
1114
+ return probeRunOutput(scoutd.value, {
1115
+ backend: "scoutd",
1116
+ generatedAt: scoutd.generatedAt
1117
+ });
1118
+ }
1119
+ const metadata = scoutd.fallbackSince ? {
1120
+ backend: "local-fallback",
1121
+ fallbackSince: scoutd.fallbackSince,
1122
+ fallbackReason: scoutd.fallbackReason ?? "scoutd unavailable"
1123
+ } : { backend: "local" };
1124
+ try {
1125
+ const local = await input.local();
1126
+ return probeRunOutput(local, metadata);
1127
+ } catch (error) {
1128
+ throw new ProbeBackendError(error instanceof Error ? error.message : String(error), metadata, error);
1129
+ }
1130
+ }
1131
+ function probeSocketTimeoutMs(timeoutMs) {
1132
+ if (timeoutMs === undefined || !Number.isFinite(timeoutMs)) {
1133
+ return SOCKET_TIMEOUT_MS;
1134
+ }
1135
+ return Math.max(SOCKET_TIMEOUT_MS, Math.min(Math.max(0, timeoutMs) + 1000, 31000));
1136
+ }
1137
+ function requestOpTimeoutMs(timeoutMs) {
1138
+ if (timeoutMs === undefined || !Number.isFinite(timeoutMs)) {
1139
+ return;
1140
+ }
1141
+ return Math.max(0, Math.floor(timeoutMs));
1142
+ }
1143
+ function execSocketTimeoutMs(args) {
1144
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) ? Math.max(0, args.timeoutMs) : 5000;
1145
+ return Math.max(SOCKET_TIMEOUT_MS, Math.min(timeoutMs + 1000, 31000));
1146
+ }
1147
+ async function requestJson(socketPath, payload, options = {}) {
1148
+ const bun = globalThis.Bun;
1149
+ if (bun?.connect) {
1150
+ return await requestJsonWithBun(bun.connect, socketPath, payload, options);
1151
+ }
1152
+ return await new Promise((resolve, reject) => {
1153
+ const socket = new Socket;
1154
+ let response = "";
1155
+ let settled = false;
1156
+ const finish = (error, value) => {
1157
+ if (settled)
1158
+ return;
1159
+ settled = true;
1160
+ clearTimeout(timer);
1161
+ socket.removeAllListeners();
1162
+ socket.destroy();
1163
+ if (error) {
1164
+ reject(error);
1165
+ } else {
1166
+ resolve(value);
1167
+ }
1168
+ };
1169
+ const timeoutMs = options.timeoutMs ?? SOCKET_TIMEOUT_MS;
1170
+ const timer = setTimeout(() => {
1171
+ finish(new Error(`scoutd probe socket timed out after ${timeoutMs}ms`));
1172
+ }, timeoutMs);
1173
+ timer.unref?.();
1174
+ const finishFromResponse = () => {
1175
+ if (settled)
1176
+ return;
1177
+ try {
1178
+ const parsed = JSON.parse(response);
1179
+ if (isRecord2(parsed) && parsed.schema === ERROR_SCHEMA) {
1180
+ const error = isRecord2(parsed.error) ? parsed.error : {};
1181
+ const message = readString(error.message) ?? readString(error.code) ?? "scoutd probe request failed";
1182
+ finish(new Error(message));
1183
+ return;
1184
+ }
1185
+ finish(null, parsed);
1186
+ } catch (error) {
1187
+ finish(new Error(`scoutd probe response was not JSON: ${fallbackMessage(error)}`));
1188
+ }
1189
+ };
1190
+ socket.setEncoding("utf8");
1191
+ socket.on("connect", () => {
1192
+ socket.write(`${JSON.stringify(payload)}
1193
+ `);
1194
+ });
1195
+ socket.on("data", (chunk) => {
1196
+ response += chunk;
1197
+ if (Buffer.byteLength(response, "utf8") > MAX_RESPONSE_BYTES) {
1198
+ finish(new Error("scoutd probe response exceeded output limit"));
1199
+ }
1200
+ });
1201
+ socket.on("error", (error) => finish(error));
1202
+ socket.on("end", finishFromResponse);
1203
+ socket.on("close", () => {
1204
+ if (!settled) {
1205
+ if (response.length === 0) {
1206
+ finish(new Error("scoutd probe socket closed without a response"));
1207
+ } else {
1208
+ finishFromResponse();
1209
+ }
1210
+ }
1211
+ });
1212
+ socket.connect(socketPath);
1213
+ });
1214
+ }
1215
+ async function requestJsonWithBun(connect, socketPath, payload, options = {}) {
1216
+ return await new Promise((resolve, reject) => {
1217
+ let response = "";
1218
+ let settled = false;
1219
+ let socketRef = null;
1220
+ const decoder = new TextDecoder;
1221
+ const finish = (error, value) => {
1222
+ if (settled)
1223
+ return;
1224
+ settled = true;
1225
+ clearTimeout(timer);
1226
+ try {
1227
+ socketRef?.end?.();
1228
+ } catch {}
1229
+ if (error) {
1230
+ reject(error);
1231
+ } else {
1232
+ resolve(value);
1233
+ }
1234
+ };
1235
+ const finishFromResponse = () => {
1236
+ if (settled)
1237
+ return;
1238
+ try {
1239
+ const parsed = JSON.parse(response);
1240
+ if (isRecord2(parsed) && parsed.schema === ERROR_SCHEMA) {
1241
+ const error = isRecord2(parsed.error) ? parsed.error : {};
1242
+ const message = readString(error.message) ?? readString(error.code) ?? "scoutd probe request failed";
1243
+ finish(new Error(message));
1244
+ return;
1245
+ }
1246
+ finish(null, parsed);
1247
+ } catch (error) {
1248
+ finish(new Error(`scoutd probe response was not JSON: ${fallbackMessage(error)}`));
1249
+ }
1250
+ };
1251
+ const timeoutMs = options.timeoutMs ?? SOCKET_TIMEOUT_MS;
1252
+ const timer = setTimeout(() => {
1253
+ finish(new Error(`scoutd probe socket timed out after ${timeoutMs}ms`));
1254
+ }, timeoutMs);
1255
+ timer.unref?.();
1256
+ connect({
1257
+ unix: socketPath,
1258
+ socket: {
1259
+ open(socket) {
1260
+ socketRef = socket;
1261
+ socket.write(`${JSON.stringify(payload)}
1262
+ `);
1263
+ },
1264
+ data(_socket, data) {
1265
+ response += decoder.decode(data, { stream: true });
1266
+ if (Buffer.byteLength(response, "utf8") > MAX_RESPONSE_BYTES) {
1267
+ finish(new Error("scoutd probe response exceeded output limit"));
1268
+ }
1269
+ },
1270
+ close() {
1271
+ if (response.length === 0) {
1272
+ finish(new Error("scoutd probe socket closed without a response"));
1273
+ } else {
1274
+ response += decoder.decode();
1275
+ finishFromResponse();
1276
+ }
1277
+ },
1278
+ error(_socket, error) {
1279
+ finish(error);
1280
+ }
1281
+ }
1282
+ }).then((socket) => {
1283
+ socketRef = socket;
1284
+ }, (error) => {
1285
+ finish(error instanceof Error ? error : new Error(String(error)));
1286
+ });
1287
+ });
1288
+ }
1289
+ function fallbackKey(probeId, key) {
1290
+ return `${probeId}\x00${key ?? ""}`;
1291
+ }
1292
+
1293
+ // packages/runtime/src/system-probes/exec.ts
1294
+ class ProbeCommandError extends Error {
1295
+ code;
1296
+ exitCode;
1297
+ signal;
1298
+ constructor(message, options) {
1299
+ super(message);
1300
+ this.name = "ProbeCommandError";
1301
+ this.code = options.code;
1302
+ this.exitCode = options.exitCode;
1303
+ this.signal = options.signal;
1304
+ }
1305
+ }
1306
+ var DEFAULT_STDOUT_CAP_BYTES = 1024 * 1024;
1307
+ var DEFAULT_STDERR_CAP_BYTES = 128 * 1024;
1308
+ var SIGKILL_DELAY_MS = 500;
1309
+ var EXEC_SYSTEM_TRANSPORT_METADATA = Symbol.for("openscout.execSystem.transport");
1310
+ var spawnProcess = spawn;
1311
+ function bufferByteLength(chunks) {
1312
+ return chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
1313
+ }
1314
+ function abortMessage(ctx) {
1315
+ const reason = ctx.signal.reason;
1316
+ if (typeof reason === "object" && reason !== null && "message" in reason) {
1317
+ const message = reason.message;
1318
+ if (typeof message === "string" && message.trim()) {
1319
+ return message;
1320
+ }
1321
+ }
1322
+ return `Probe ${ctx.probeId} aborted`;
1323
+ }
1324
+ async function execProbeFile(ctx, file, args, options = {}) {
1325
+ const maxStdoutBytes = options.maxStdoutBytes ?? DEFAULT_STDOUT_CAP_BYTES;
1326
+ const maxStderrBytes = options.maxStderrBytes ?? DEFAULT_STDERR_CAP_BYTES;
1327
+ return await new Promise((resolve, reject) => {
1328
+ if (ctx.signal.aborted) {
1329
+ reject(new ProbeCommandError(abortMessage(ctx), { code: "aborted" }));
1330
+ return;
1331
+ }
1332
+ const stdoutChunks = [];
1333
+ const stderrChunks = [];
1334
+ let settled = false;
1335
+ let killTimer = null;
1336
+ const child = spawnProcess(file, [...args], {
1337
+ cwd: options.cwd,
1338
+ env: options.env,
1339
+ stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
1340
+ windowsHide: true
1341
+ });
1342
+ const cleanup = () => {
1343
+ ctx.signal.removeEventListener("abort", onAbort);
1344
+ if (killTimer) {
1345
+ clearTimeout(killTimer);
1346
+ killTimer = null;
1347
+ }
1348
+ };
1349
+ const fail = (error) => {
1350
+ if (settled) {
1351
+ return;
1352
+ }
1353
+ settled = true;
1354
+ cleanup();
1355
+ if (!child.killed) {
1356
+ child.kill("SIGTERM");
1357
+ }
1358
+ reject(error);
1359
+ };
1360
+ const onAbort = () => {
1361
+ if (settled) {
1362
+ return;
1363
+ }
1364
+ settled = true;
1365
+ ctx.signal.removeEventListener("abort", onAbort);
1366
+ child.kill("SIGTERM");
1367
+ killTimer = setTimeout(() => {
1368
+ child.kill("SIGKILL");
1369
+ }, SIGKILL_DELAY_MS);
1370
+ reject(new ProbeCommandError(abortMessage(ctx), { code: "timeout" }));
1371
+ };
1372
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
1373
+ if (options.input !== undefined) {
1374
+ child.stdin?.end(options.input);
1375
+ }
1376
+ child.stdout?.on("data", (chunk) => {
1377
+ if (settled) {
1378
+ return;
1379
+ }
1380
+ stdoutChunks.push(chunk);
1381
+ if (bufferByteLength(stdoutChunks) > maxStdoutBytes) {
1382
+ fail(new ProbeCommandError(`Probe ${ctx.probeId} stdout exceeded ${maxStdoutBytes} bytes`, { code: "output_cap" }));
1383
+ }
1384
+ });
1385
+ child.stderr?.on("data", (chunk) => {
1386
+ if (settled) {
1387
+ return;
1388
+ }
1389
+ stderrChunks.push(chunk);
1390
+ if (bufferByteLength(stderrChunks) > maxStderrBytes) {
1391
+ fail(new ProbeCommandError(`Probe ${ctx.probeId} stderr exceeded ${maxStderrBytes} bytes`, { code: "output_cap" }));
1392
+ }
1393
+ });
1394
+ child.once("error", (error) => {
1395
+ if (settled) {
1396
+ return;
1397
+ }
1398
+ settled = true;
1399
+ cleanup();
1400
+ const code = typeof error.code === "string" ? String(error.code) : "spawn";
1401
+ reject(new ProbeCommandError(error.message, { code }));
1402
+ });
1403
+ child.once("close", (exitCode, signal) => {
1404
+ if (settled) {
1405
+ cleanup();
1406
+ return;
1407
+ }
1408
+ settled = true;
1409
+ cleanup();
1410
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
1411
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
1412
+ if (exitCode === 0) {
1413
+ resolve({ stdout, stderr, exitCode });
1414
+ return;
1415
+ }
1416
+ reject(new ProbeCommandError(`${file} exited with ${exitCode ?? signal ?? "unknown status"}`, { code: "exit", exitCode, signal }));
1417
+ });
1418
+ });
1419
+ }
1420
+
1421
+ // packages/runtime/src/system-probes/tailscale-status.ts
1422
+ var DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS = 1500;
1423
+ function parseTailscaleStatusJson(raw) {
1424
+ return JSON.parse(raw);
1425
+ }
1426
+ function parsePeers(status) {
1427
+ const peers = Object.entries(status.Peer ?? {});
1428
+ return peers.map(([fallbackId, peer]) => ({
1429
+ id: peer.ID ?? fallbackId,
1430
+ name: peer.HostName ?? peer.DNSName ?? fallbackId,
1431
+ dnsName: peer.DNSName,
1432
+ addresses: peer.TailscaleIPs ?? [],
1433
+ online: peer.Online ?? false,
1434
+ hostName: peer.HostName,
1435
+ os: peer.OS,
1436
+ tags: peer.Tags ?? []
1437
+ }));
1438
+ }
1439
+ function parseSelf(status) {
1440
+ const self = status.Self;
1441
+ if (!self) {
1442
+ return null;
1443
+ }
1444
+ return {
1445
+ id: self.ID ?? self.DNSName ?? self.HostName ?? "self",
1446
+ name: self.HostName ?? self.DNSName ?? "self",
1447
+ dnsName: self.DNSName,
1448
+ addresses: self.TailscaleIPs ?? [],
1449
+ online: self.Online ?? true,
1450
+ hostName: self.HostName,
1451
+ os: self.OS,
1452
+ tailnetName: status.CurrentTailnet?.Name,
1453
+ magicDnsSuffix: status.CurrentTailnet?.MagicDNSSuffix
1454
+ };
1455
+ }
1456
+ function isTailscaleBackendRunning(status) {
1457
+ return (status.BackendState ?? "").trim().toLowerCase() === "running";
1458
+ }
1459
+ function normalizeHost(value) {
1460
+ const normalized = value?.trim().replace(/\.$/, "").toLowerCase();
1461
+ if (!normalized || normalized.includes("/") || /\s/.test(normalized)) {
1462
+ return null;
1463
+ }
1464
+ return normalized;
1465
+ }
1466
+ function uniq(values) {
1467
+ const seen = new Set;
1468
+ const out = [];
1469
+ for (const value of values) {
1470
+ const normalized = normalizeHost(value ?? undefined);
1471
+ if (!normalized || seen.has(normalized)) {
1472
+ continue;
1473
+ }
1474
+ seen.add(normalized);
1475
+ out.push(normalized);
1476
+ }
1477
+ return out;
1478
+ }
1479
+ function tailscaleSelfWebHosts(self) {
1480
+ if (!self) {
1481
+ return [];
1482
+ }
1483
+ return uniq([
1484
+ self.dnsName,
1485
+ self.hostName && self.magicDnsSuffix ? `${self.hostName}.${self.magicDnsSuffix}` : undefined,
1486
+ ...self.addresses
1487
+ ]);
1488
+ }
1489
+ function summarizeTailscaleStatus(status) {
1490
+ return {
1491
+ backendState: status.BackendState ?? null,
1492
+ running: isTailscaleBackendRunning(status),
1493
+ health: status.Health ?? [],
1494
+ peers: parsePeers(status),
1495
+ self: parseSelf(status)
149
1496
  };
150
1497
  }
151
- async function requestScoutBrokerJsonWithTrace(baseUrl, path, options = {}) {
152
- const { response, trace } = await requestBrokerWire(baseUrl, path, options);
153
- let parsed;
154
- let parsedJson = false;
155
- if (response.text.length > 0) {
1498
+ function statusTimeoutMs(env) {
1499
+ const parsed = Number.parseInt(env.OPENSCOUT_TAILSCALE_STATUS_TIMEOUT_MS ?? "", 10);
1500
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS;
1501
+ }
1502
+ async function readStatusJsonFromFile(filePath) {
1503
+ try {
1504
+ const raw = await readFile(filePath, "utf8");
1505
+ return parseTailscaleStatusJson(raw);
1506
+ } catch {
1507
+ return null;
1508
+ }
1509
+ }
1510
+ function isDomainUnavailableError(error) {
1511
+ if (!(error instanceof ProbeCommandError)) {
1512
+ return false;
1513
+ }
1514
+ return error.code === "ENOENT" || error.code === "spawn" || error.code === "exit";
1515
+ }
1516
+ async function readTailscaleStatusSummaryLocal(ctx) {
1517
+ const fixturePath = process.env.OPENSCOUT_TAILSCALE_STATUS_JSON;
1518
+ if (fixturePath) {
1519
+ const status = await readStatusJsonFromFile(fixturePath);
1520
+ return status ? summarizeTailscaleStatus(status) : null;
1521
+ }
1522
+ const tailscaleBin = process.env.OPENSCOUT_TAILSCALE_BIN ?? "tailscale";
1523
+ try {
1524
+ const { stdout } = await execProbeFile(ctx, tailscaleBin, ["status", "--json"], {
1525
+ maxStdoutBytes: 4 * 1024 * 1024,
1526
+ maxStderrBytes: 256 * 1024
1527
+ });
1528
+ return summarizeTailscaleStatus(parseTailscaleStatusJson(stdout));
1529
+ } catch (error) {
1530
+ if (isDomainUnavailableError(error)) {
1531
+ return null;
1532
+ }
1533
+ throw error;
1534
+ }
1535
+ }
1536
+ var tailscaleStatusProbe = defineProbe({
1537
+ id: "tailscale.status",
1538
+ ttlMs: 30000,
1539
+ timeoutMs: statusTimeoutMs(process.env),
1540
+ run: (ctx) => runWithScoutdFallback({
1541
+ probeId: "tailscale.status",
1542
+ ctx,
1543
+ local: () => readTailscaleStatusSummaryLocal(ctx)
1544
+ })
1545
+ });
1546
+
1547
+ // packages/runtime/src/tailscale.ts
1548
+ var DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS2 = 1500;
1549
+ function readStatusJsonFromFileSync(filePath) {
1550
+ const raw = readFileSync2(filePath, "utf8");
1551
+ return parseTailscaleStatusJson(raw);
1552
+ }
1553
+ function statusTimeoutMs2(env) {
1554
+ const parsed = Number.parseInt(env.OPENSCOUT_TAILSCALE_STATUS_TIMEOUT_MS ?? "", 10);
1555
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS2;
1556
+ }
1557
+ function readStatusJsonSync(env = process.env) {
1558
+ if (env.OPENSCOUT_TAILSCALE_AUTO_HOSTS === "0") {
1559
+ return null;
1560
+ }
1561
+ const fixturePath = env.OPENSCOUT_TAILSCALE_STATUS_JSON;
1562
+ if (fixturePath) {
156
1563
  try {
157
- parsed = JSON.parse(response.text);
158
- parsedJson = true;
1564
+ return readStatusJsonFromFileSync(fixturePath);
159
1565
  } catch {
160
- parsedJson = false;
1566
+ return null;
161
1567
  }
162
1568
  }
163
- if (!response.ok) {
164
- if (parsedJson && options.acceptErrorJson?.(parsed)) {
165
- return { value: parsed, trace };
166
- }
167
- throw new Error(`${path} returned ${response.status}: ${response.text}`);
1569
+ const tailscaleBin = env.OPENSCOUT_TAILSCALE_BIN ?? (env === process.env ? "tailscale" : undefined);
1570
+ if (!tailscaleBin) {
1571
+ return null;
168
1572
  }
169
- if (parsedJson) {
170
- return { value: parsed, trace };
1573
+ try {
1574
+ const stdout = execFileSync(tailscaleBin, ["status", "--json"], {
1575
+ encoding: "utf8",
1576
+ stdio: ["ignore", "pipe", "pipe"],
1577
+ timeout: statusTimeoutMs2(env),
1578
+ windowsHide: true
1579
+ });
1580
+ return parseTailscaleStatusJson(stdout);
1581
+ } catch {
1582
+ return null;
171
1583
  }
172
- return { value: undefined, trace };
1584
+ }
1585
+ function readTailscaleSelfWebHostsSync(env = process.env) {
1586
+ const status = readStatusJsonSync(env);
1587
+ if (!status || !isTailscaleBackendRunning(status)) {
1588
+ return [];
1589
+ }
1590
+ return tailscaleSelfWebHosts(summarizeTailscaleStatus(status).self);
173
1591
  }
174
1592
 
175
- // packages/runtime/src/support-paths.ts
176
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
177
- import { homedir } from "os";
178
- import { join } from "path";
179
- var OPENSCOUT_RPC_CUTOVER_MARKER = "rpc-runtime-cutover-v1";
180
- function resolveOpenScoutSupportPaths() {
181
- const home = homedir();
182
- const supportDirectory = process.env.OPENSCOUT_SUPPORT_DIRECTORY ?? join(home, "Library", "Application Support", "OpenScout");
183
- const logsDirectory = join(supportDirectory, "logs");
184
- const runtimeDirectory = join(supportDirectory, "runtime");
185
- const catalogDirectory = join(supportDirectory, "catalog");
186
- const controlHome = process.env.OPENSCOUT_CONTROL_HOME ?? join(home, ".openscout", "control-plane");
187
- const knowledgeDirectory = join(controlHome, "knowledge");
1593
+ // packages/runtime/src/local-config.ts
1594
+ import {
1595
+ existsSync as existsSync3,
1596
+ mkdirSync,
1597
+ readFileSync as readFileSync3,
1598
+ renameSync,
1599
+ writeFileSync
1600
+ } from "fs";
1601
+ import { homedir as homedir3, hostname as osHostname } from "os";
1602
+ import { dirname, join as join3 } from "path";
1603
+ var LOCAL_CONFIG_VERSION = 1;
1604
+ var DEFAULT_SCOUT_WEB_PORTAL_HOST = "scout.local";
1605
+ var DEFAULT_SCOUT_WEB_DEV_HOST = `dev.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
1606
+ var OPENSCOUT_PORTS = {
1607
+ broker: 43110,
1608
+ web: 43120,
1609
+ webTerminalRelay: 43121,
1610
+ vite: 43122,
1611
+ pairingBridge: 43130,
1612
+ pairingRelay: 43131,
1613
+ pairingFileServer: 43132,
1614
+ studio: 43140
1615
+ };
1616
+ var DEFAULT_LOCAL_CONFIG = {
1617
+ version: LOCAL_CONFIG_VERSION,
1618
+ host: "127.0.0.1",
1619
+ webLocalName: undefined,
1620
+ ports: {
1621
+ broker: OPENSCOUT_PORTS.broker,
1622
+ web: OPENSCOUT_PORTS.web,
1623
+ pairing: OPENSCOUT_PORTS.pairingBridge
1624
+ }
1625
+ };
1626
+ function localConfigHome() {
1627
+ return process.env.OPENSCOUT_HOME ?? join3(homedir3(), ".openscout");
1628
+ }
1629
+ function localConfigPath() {
1630
+ return join3(localConfigHome(), "config.json");
1631
+ }
1632
+ function loadLocalConfig() {
1633
+ const configPath = localConfigPath();
1634
+ if (!existsSync3(configPath))
1635
+ return { version: LOCAL_CONFIG_VERSION };
1636
+ try {
1637
+ return validateLocalConfig(JSON.parse(readFileSync3(configPath, "utf8")));
1638
+ } catch {
1639
+ return { version: LOCAL_CONFIG_VERSION };
1640
+ }
1641
+ }
1642
+ function validateLocalConfig(input) {
1643
+ if (!input || typeof input !== "object")
1644
+ return { version: LOCAL_CONFIG_VERSION };
1645
+ const raw = input;
1646
+ const out = { version: LOCAL_CONFIG_VERSION };
1647
+ if (typeof raw.host === "string" && raw.host.trim().length > 0) {
1648
+ out.host = raw.host.trim();
1649
+ }
1650
+ if (typeof raw.webLocalName === "string" && raw.webLocalName.trim().length > 0) {
1651
+ out.webLocalName = raw.webLocalName.trim();
1652
+ }
1653
+ if (raw.ports && typeof raw.ports === "object") {
1654
+ const ports = raw.ports;
1655
+ const p = {};
1656
+ if (isValidPort(ports.broker))
1657
+ p.broker = ports.broker;
1658
+ if (isValidPort(ports.web))
1659
+ p.web = ports.web;
1660
+ if (isValidPort(ports.pairing))
1661
+ p.pairing = ports.pairing;
1662
+ if (Object.keys(p).length > 0)
1663
+ out.ports = p;
1664
+ }
1665
+ return out;
1666
+ }
1667
+ function isValidPort(value) {
1668
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536;
1669
+ }
1670
+ function parseEnvPort(name) {
1671
+ const raw = process.env[name]?.trim();
1672
+ if (!raw) {
1673
+ return;
1674
+ }
1675
+ const parsed = Number.parseInt(raw, 10);
1676
+ return isValidPort(parsed) ? parsed : undefined;
1677
+ }
1678
+ function resolveEffectiveLocalConfig() {
1679
+ const file = loadLocalConfig();
1680
+ const host = process.env.OPENSCOUT_BROKER_HOST?.trim() || process.env.OPENSCOUT_HOST?.trim() || file.host;
1681
+ const ports = {
1682
+ broker: parseEnvPort("OPENSCOUT_BROKER_PORT") ?? file.ports?.broker,
1683
+ web: parseEnvPort("OPENSCOUT_WEB_PORT") ?? parseEnvPort("SCOUT_WEB_PORT") ?? file.ports?.web,
1684
+ pairing: parseEnvPort("OPENSCOUT_PAIRING_PORT") ?? file.ports?.pairing
1685
+ };
188
1686
  return {
189
- supportDirectory,
190
- logsDirectory,
191
- appLogsDirectory: join(logsDirectory, "app"),
192
- brokerLogsDirectory: join(logsDirectory, "broker"),
193
- runtimeDirectory,
194
- catalogDirectory,
195
- relayAgentsDirectory: join(runtimeDirectory, "agents"),
196
- settingsPath: join(supportDirectory, "settings.json"),
197
- harnessCatalogPath: join(catalogDirectory, "harness-catalog.json"),
198
- relayAgentsRegistryPath: join(supportDirectory, "relay-agents.json"),
199
- managedInstallsPath: join(supportDirectory, "managed-installs.json"),
200
- relayHubDirectory: process.env.OPENSCOUT_RELAY_HUB ?? join(home, ".openscout", "relay"),
201
- controlHome,
202
- knowledgeDirectory,
203
- knowledgeQmdDirectory: join(knowledgeDirectory, "qmd"),
204
- knowledgeSqlitePath: join(knowledgeDirectory, "knowledge.sqlite"),
205
- desktopStatusPath: join(supportDirectory, "agent-status.json"),
206
- workspaceStatePath: join(supportDirectory, "workspace-state.json"),
207
- cutoverMarkerPath: join(supportDirectory, OPENSCOUT_RPC_CUTOVER_MARKER)
1687
+ version: LOCAL_CONFIG_VERSION,
1688
+ ...host ? { host } : {},
1689
+ ...file.webLocalName ? { webLocalName: file.webLocalName } : {},
1690
+ ...ports.broker || ports.web || ports.pairing ? { ports } : {}
208
1691
  };
209
1692
  }
210
- function ensureOpenScoutCleanSlateSync() {
211
- const paths = resolveOpenScoutSupportPaths();
212
- if (existsSync(paths.cutoverMarkerPath)) {
213
- return;
1693
+ function localBrokerControlHost(host) {
1694
+ const trimmed = host.trim();
1695
+ if (!trimmed || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]") {
1696
+ return DEFAULT_LOCAL_CONFIG.host;
1697
+ }
1698
+ return trimmed;
1699
+ }
1700
+ function resolveBrokerControlUrl(config = resolveEffectiveLocalConfig()) {
1701
+ const injected = process.env.OPENSCOUT_BROKER_INTERNAL_URL?.trim();
1702
+ if (injected) {
1703
+ return injected;
214
1704
  }
215
- rmSync(paths.supportDirectory, { recursive: true, force: true });
216
- rmSync(paths.controlHome, { recursive: true, force: true });
217
- rmSync(paths.relayHubDirectory, { recursive: true, force: true });
218
- mkdirSync(paths.supportDirectory, { recursive: true });
219
- writeFileSync(paths.cutoverMarkerPath, `${Date.now()}
220
- `, "utf8");
1705
+ const host = config.host ?? DEFAULT_LOCAL_CONFIG.host;
1706
+ const port = config.ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker;
1707
+ return `http://${localBrokerControlHost(host)}:${port}`;
1708
+ }
1709
+ function resolveBrokerPort() {
1710
+ return resolveEffectiveLocalConfig().ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker;
221
1711
  }
222
1712
 
223
1713
  // packages/runtime/src/tool-resolution.ts
224
- import { accessSync, constants, existsSync as existsSync2 } from "fs";
225
- import { homedir as homedir2 } from "os";
226
- import { basename, dirname, join as join2, resolve } from "path";
1714
+ import { accessSync, constants, existsSync as existsSync4 } from "fs";
1715
+ import { homedir as homedir4 } from "os";
1716
+ import { basename, dirname as dirname2, join as join4, resolve } from "path";
227
1717
  var DEFAULT_COMMON_EXECUTABLE_DIRECTORIES = [
228
- join2(homedir2(), ".bun", "bin"),
1718
+ join4(homedir4(), ".bun", "bin"),
229
1719
  "/opt/homebrew/bin",
230
1720
  "/usr/local/bin"
231
1721
  ];
232
1722
  function expandHomePath(value, env = process.env) {
233
- const home = env.HOME ?? homedir2();
1723
+ const home = env.HOME ?? homedir4();
234
1724
  if (value === "~") {
235
1725
  return home;
236
1726
  }
237
1727
  if (value.startsWith("~/")) {
238
- return join2(home, value.slice(2));
1728
+ return join4(home, value.slice(2));
239
1729
  }
240
1730
  return value;
241
1731
  }
@@ -292,7 +1782,7 @@ function resolveExecutableFromSearch(options) {
292
1782
  ], env);
293
1783
  for (const directory of searchDirectories) {
294
1784
  for (const name of options.names) {
295
- const candidate = join2(directory, name);
1785
+ const candidate = join4(directory, name);
296
1786
  if (isExecutablePath(candidate)) {
297
1787
  return {
298
1788
  path: candidate,
@@ -316,7 +1806,7 @@ function findExecutableOnSearchPath(name, env) {
316
1806
  ...DEFAULT_COMMON_EXECUTABLE_DIRECTORIES
317
1807
  ], env);
318
1808
  for (const directory of searchDirectories) {
319
- const candidate = join2(directory, name);
1809
+ const candidate = join4(directory, name);
320
1810
  if (isExecutablePath(candidate)) {
321
1811
  return { path: candidate, source: "path" };
322
1812
  }
@@ -324,25 +1814,68 @@ function findExecutableOnSearchPath(name, env) {
324
1814
  return null;
325
1815
  }
326
1816
 
1817
+ // packages/runtime/src/runtime-adapters.ts
1818
+ function normalizeRuntimeHost(value) {
1819
+ const normalized = value?.trim().toLowerCase();
1820
+ if (normalized === "bun")
1821
+ return "bun";
1822
+ if (normalized === "node")
1823
+ return "node";
1824
+ return null;
1825
+ }
1826
+ function normalizeRuntimeServiceAdapter(value) {
1827
+ const normalized = value?.trim().toLowerCase().replaceAll("_", "-");
1828
+ if (normalized === "macos-scoutd" || normalized === "macos-launchd")
1829
+ return "macos-scoutd";
1830
+ if (normalized === "linux-systemd-user" || normalized === "systemd-user")
1831
+ return "linux-systemd-user";
1832
+ if (normalized === "headless-foreground" || normalized === "foreground" || normalized === "headless") {
1833
+ return "headless-foreground";
1834
+ }
1835
+ if (normalized === "windows-service")
1836
+ return "windows-service";
1837
+ return null;
1838
+ }
1839
+ function defaultServiceAdapterForPlatform(platform = process.platform, env = process.env) {
1840
+ const explicit = normalizeRuntimeServiceAdapter(env.OPENSCOUT_SERVICE_ADAPTER);
1841
+ if (explicit)
1842
+ return explicit;
1843
+ if (normalizeRuntimeHost(env.OPENSCOUT_RUNTIME_HOST) === "node") {
1844
+ return "headless-foreground";
1845
+ }
1846
+ if (platform === "darwin")
1847
+ return "macos-scoutd";
1848
+ if (platform === "linux")
1849
+ return "headless-foreground";
1850
+ if (platform === "win32")
1851
+ return "windows-service";
1852
+ return "headless-foreground";
1853
+ }
1854
+
327
1855
  // packages/runtime/src/broker-process-manager.ts
328
1856
  function isTmpPath(p) {
329
1857
  return /^\/(?:private\/)?tmp\//.test(p);
330
1858
  }
331
1859
  var DEFAULT_BROKER_HOST = "127.0.0.1";
332
1860
  var DEFAULT_BROKER_HOST_MESH = "0.0.0.0";
333
- var DEFAULT_BROKER_PORT = 65535;
1861
+ var DEFAULT_BROKER_PORT = OPENSCOUT_PORTS.broker;
334
1862
  var DEFAULT_ADVERTISE_SCOPE = "local";
335
- function resolveAdvertiseScope() {
336
- const raw = (process.env.OPENSCOUT_ADVERTISE_SCOPE ?? "").trim().toLowerCase();
1863
+ function resolveAdvertiseScope(env = process.env) {
1864
+ if (openScoutNetworkDiscoveryEnabled(env))
1865
+ return "mesh";
1866
+ const raw = (env.OPENSCOUT_ADVERTISE_SCOPE ?? "").trim().toLowerCase();
337
1867
  if (raw === "mesh")
338
1868
  return "mesh";
339
1869
  if (raw === "local")
340
1870
  return "local";
341
1871
  return DEFAULT_ADVERTISE_SCOPE;
342
1872
  }
343
- function resolveBrokerHost(scope = resolveAdvertiseScope()) {
344
- const explicit = process.env.OPENSCOUT_BROKER_HOST;
345
- if (typeof explicit === "string" && explicit.trim().length > 0) {
1873
+ function resolveBrokerHost(scope = resolveAdvertiseScope(), env = process.env) {
1874
+ const explicit = env.OPENSCOUT_BROKER_HOST?.trim();
1875
+ if (explicit) {
1876
+ if (scope === "mesh" && isLoopbackHost(explicit)) {
1877
+ return DEFAULT_BROKER_HOST_MESH;
1878
+ }
346
1879
  return explicit;
347
1880
  }
348
1881
  return scope === "mesh" ? DEFAULT_BROKER_HOST_MESH : DEFAULT_BROKER_HOST;
@@ -351,14 +1884,46 @@ function isLoopbackHost(host) {
351
1884
  const trimmed = host.trim();
352
1885
  return trimmed === "127.0.0.1" || trimmed === "::1" || trimmed === "localhost";
353
1886
  }
354
- var BROKER_SERVICE_POLL_INTERVAL_MS = 100;
355
- var DEFAULT_BROKER_START_TIMEOUT_MS = 15000;
356
- var DEFAULT_BROKER_STOP_TIMEOUT_MS = 20000;
1887
+ function localBrokerControlHost2(host) {
1888
+ const trimmed = host.trim();
1889
+ if (!trimmed || isWildcardHost(trimmed)) {
1890
+ return DEFAULT_BROKER_HOST;
1891
+ }
1892
+ return trimmed;
1893
+ }
357
1894
  function buildDefaultBrokerUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
358
1895
  return `http://${host}:${port}`;
359
1896
  }
1897
+ function resolveBrokerUrl(host, port, scope, env = process.env) {
1898
+ const explicit = env.OPENSCOUT_BROKER_URL?.trim();
1899
+ if (explicit && !(scope === "mesh" && isUnreachableMeshBrokerUrl(explicit))) {
1900
+ return explicit;
1901
+ }
1902
+ if (scope === "mesh") {
1903
+ const tailnetHost = readTailscaleSelfWebHostsSync(env)[0];
1904
+ if (tailnetHost) {
1905
+ return buildDefaultBrokerUrl(tailnetHost, port);
1906
+ }
1907
+ }
1908
+ return buildDefaultBrokerUrl(host, port);
1909
+ }
1910
+ function isUnreachableMeshBrokerUrl(value) {
1911
+ try {
1912
+ const host = new URL(value).hostname;
1913
+ return isLoopbackHost(host) || isWildcardHost(host);
1914
+ } catch {
1915
+ return false;
1916
+ }
1917
+ }
1918
+ function isWildcardHost(host) {
1919
+ const trimmed = host.trim();
1920
+ return trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]";
1921
+ }
1922
+ function buildLocalBrokerControlUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
1923
+ return buildDefaultBrokerUrl(localBrokerControlHost2(host), port);
1924
+ }
360
1925
  function buildDefaultBrokerSocketPath(runtimeDirectory) {
361
- return join3(runtimeDirectory, "broker.sock");
1926
+ return join5(runtimeDirectory, "broker.sock");
362
1927
  }
363
1928
  var DEFAULT_BROKER_URL = buildDefaultBrokerUrl();
364
1929
  function normalizeBrokerUrl(value) {
@@ -368,8 +1933,15 @@ function normalizeBrokerUrl(value) {
368
1933
  return value.trim();
369
1934
  }
370
1935
  }
1936
+ function resolveScoutBrokerControlUrl(_config = resolveBrokerServiceConfig()) {
1937
+ return resolveBrokerControlUrl();
1938
+ }
371
1939
  function resolveBrokerSocketPathForBaseUrl(baseUrl, config = resolveBrokerServiceConfig()) {
372
- return normalizeBrokerUrl(baseUrl) === normalizeBrokerUrl(config.brokerUrl) ? config.brokerSocketPath : null;
1940
+ const normalized = normalizeBrokerUrl(baseUrl);
1941
+ if (normalized === normalizeBrokerUrl(config.brokerUrl) || normalized === normalizeBrokerUrl(resolveScoutBrokerControlUrl(config))) {
1942
+ return config.brokerSocketPath;
1943
+ }
1944
+ return null;
373
1945
  }
374
1946
  function runtimePackageDir() {
375
1947
  const explicit = process.env.OPENSCOUT_RUNTIME_PACKAGE_DIR?.trim();
@@ -384,17 +1956,17 @@ function runtimePackageDir() {
384
1956
  const fromGlobal = findGlobalRuntimeDir();
385
1957
  if (fromGlobal)
386
1958
  return fromGlobal;
387
- const moduleDir = dirname2(fileURLToPath(import.meta.url));
1959
+ const moduleDir = dirname3(fileURLToPath(import.meta.url));
388
1960
  return resolve2(moduleDir, "..");
389
1961
  }
390
1962
  function isInstalledRuntimePackageDir(candidate) {
391
- return existsSync3(join3(candidate, "package.json")) && existsSync3(join3(candidate, "bin", "openscout-runtime.mjs"));
1963
+ return existsSync5(join5(candidate, "package.json")) && existsSync5(join5(candidate, "bin", "openscout-runtime.mjs"));
392
1964
  }
393
1965
  function findGlobalRuntimeDir() {
394
1966
  const candidates = [
395
- join3(homedir3(), ".bun", "node_modules", "@openscout", "runtime"),
396
- join3(homedir3(), ".bun", "install", "global", "node_modules", "@openscout", "runtime"),
397
- join3(homedir3(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime")
1967
+ join5(homedir5(), ".bun", "node_modules", "@openscout", "runtime"),
1968
+ join5(homedir5(), ".bun", "install", "global", "node_modules", "@openscout", "runtime"),
1969
+ join5(homedir5(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime")
398
1970
  ];
399
1971
  for (const c of candidates) {
400
1972
  if (isInstalledRuntimePackageDir(c))
@@ -407,7 +1979,7 @@ function findGlobalRuntimeDir() {
407
1979
  const scoutPkg = resolve2(scoutBin, "..", "..");
408
1980
  if (isInstalledRuntimePackageDir(scoutPkg))
409
1981
  return scoutPkg;
410
- const nested = join3(scoutPkg, "node_modules", "@openscout", "runtime");
1982
+ const nested = join5(scoutPkg, "node_modules", "@openscout", "runtime");
411
1983
  if (isInstalledRuntimePackageDir(nested))
412
1984
  return nested;
413
1985
  const sibling = resolve2(scoutPkg, "..", "runtime");
@@ -418,30 +1990,43 @@ function findGlobalRuntimeDir() {
418
1990
  return null;
419
1991
  }
420
1992
  function findBundledRuntimeDir() {
421
- const moduleDir = dirname2(fileURLToPath(import.meta.url));
422
- const candidate = resolve2(moduleDir, "..");
423
- return isInstalledRuntimePackageDir(candidate) ? candidate : null;
1993
+ const moduleDir = dirname3(fileURLToPath(import.meta.url));
1994
+ return resolveBundledRuntimeDirFromModuleDir(moduleDir);
1995
+ }
1996
+ function resolveBundledRuntimeDirFromModuleDir(moduleDir) {
1997
+ const candidates = [
1998
+ resolve2(moduleDir, ".."),
1999
+ resolve2(moduleDir, "..", "..")
2000
+ ];
2001
+ for (const candidate of candidates) {
2002
+ if (isInstalledRuntimePackageDir(candidate))
2003
+ return candidate;
2004
+ }
2005
+ return null;
424
2006
  }
425
2007
  function findWorkspaceRuntimeDir(startDir) {
426
2008
  let current = resolve2(startDir);
427
2009
  while (true) {
428
- const candidate = join3(current, "packages", "runtime");
429
- if (existsSync3(join3(candidate, "package.json")) && existsSync3(join3(candidate, "src"))) {
2010
+ const candidate = join5(current, "packages", "runtime");
2011
+ if (existsSync5(join5(candidate, "package.json")) && existsSync5(join5(candidate, "src"))) {
430
2012
  return candidate;
431
2013
  }
432
- const parent = dirname2(current);
2014
+ const parent = dirname3(current);
433
2015
  if (parent === current)
434
2016
  return null;
435
2017
  current = parent;
436
2018
  }
437
2019
  }
438
2020
  function resolveBunExecutable2() {
439
- const bun = resolveBunExecutable(process.env);
2021
+ const bun = resolveOptionalBunExecutable();
440
2022
  if (bun) {
441
- return bun.path;
2023
+ return bun;
442
2024
  }
443
2025
  throw new Error("Unable to locate Bun for broker service management. Install Bun or set OPENSCOUT_BUN_BIN.");
444
2026
  }
2027
+ function resolveOptionalBunExecutable() {
2028
+ return resolveBunExecutable(process.env)?.path ?? null;
2029
+ }
445
2030
  function resolveBrokerServiceMode() {
446
2031
  const explicit = (process.env.OPENSCOUT_BROKER_SERVICE_MODE ?? "").trim().toLowerCase();
447
2032
  if (explicit === "prod" || explicit === "production") {
@@ -452,20 +2037,6 @@ function resolveBrokerServiceMode() {
452
2037
  }
453
2038
  return "dev";
454
2039
  }
455
- function resolveBrokerStartTimeoutMs() {
456
- const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_START_TIMEOUT_MS ?? "", 10);
457
- if (Number.isFinite(explicit) && explicit > 0) {
458
- return Math.max(explicit, BROKER_SERVICE_POLL_INTERVAL_MS);
459
- }
460
- return DEFAULT_BROKER_START_TIMEOUT_MS;
461
- }
462
- function resolveBrokerStopTimeoutMs() {
463
- const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_STOP_TIMEOUT_MS ?? "", 10);
464
- if (Number.isFinite(explicit) && explicit > 0) {
465
- return Math.max(explicit, BROKER_SERVICE_POLL_INTERVAL_MS);
466
- }
467
- return DEFAULT_BROKER_STOP_TIMEOUT_MS;
468
- }
469
2040
  function resolveBrokerServiceLabel(mode) {
470
2041
  const explicit = process.env.OPENSCOUT_SERVICE_LABEL?.trim() || process.env.OPENSCOUT_BROKER_SERVICE_LABEL?.trim();
471
2042
  if (explicit) {
@@ -481,33 +2052,23 @@ function resolveBrokerServiceLabel(mode) {
481
2052
  return "dev.openscout";
482
2053
  }
483
2054
  }
484
- function legacyBrokerServiceLabel(mode) {
485
- switch (mode) {
486
- case "prod":
487
- return "com.openscout.broker";
488
- case "custom":
489
- return "com.openscout.broker.custom";
490
- case "dev":
491
- default:
492
- return "dev.openscout.broker";
493
- }
494
- }
495
2055
  function resolveBrokerServiceConfig() {
496
2056
  const mode = resolveBrokerServiceMode();
497
2057
  const label = resolveBrokerServiceLabel(mode);
2058
+ const serviceAdapter = resolveBrokerServiceAdapter();
498
2059
  const uid = typeof process.getuid === "function" ? process.getuid() : Number.parseInt(process.env.UID ?? "0", 10);
499
2060
  const supportPaths = resolveOpenScoutSupportPaths();
500
- const defaultSupportDir = join3(homedir3(), "Library", "Application Support", "OpenScout");
2061
+ const defaultSupportDir = join5(homedir5(), "Library", "Application Support", "OpenScout");
501
2062
  const supportDirectory = isTmpPath(supportPaths.supportDirectory) ? defaultSupportDir : supportPaths.supportDirectory;
502
- const runtimeDirectory = join3(supportDirectory, "runtime");
503
- const logsDirectory = join3(supportDirectory, "logs", "broker");
504
- const controlHome = isTmpPath(supportPaths.controlHome) ? join3(homedir3(), ".openscout", "control-plane") : supportPaths.controlHome;
2063
+ const runtimeDirectory = join5(supportDirectory, "runtime");
2064
+ const logsDirectory = join5(supportDirectory, "logs", "broker");
2065
+ const controlHome = isTmpPath(supportPaths.controlHome) ? join5(homedir5(), ".openscout", "control-plane") : supportPaths.controlHome;
505
2066
  const advertiseScope = resolveAdvertiseScope();
506
2067
  const brokerHost = resolveBrokerHost(advertiseScope);
507
- const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(DEFAULT_BROKER_PORT), 10);
508
- const brokerUrl = process.env.OPENSCOUT_BROKER_URL ?? buildDefaultBrokerUrl(brokerHost, brokerPort);
2068
+ const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(resolveBrokerPort()), 10);
2069
+ const brokerUrl = resolveBrokerUrl(brokerHost, brokerPort, advertiseScope);
509
2070
  const brokerSocketPath = process.env.OPENSCOUT_BROKER_SOCKET_PATH ?? buildDefaultBrokerSocketPath(runtimeDirectory);
510
- const launchAgentPath = join3(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
2071
+ const launchAgentPath = join5(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
511
2072
  return {
512
2073
  label,
513
2074
  mode,
@@ -518,11 +2079,11 @@ function resolveBrokerServiceConfig() {
518
2079
  supportDirectory,
519
2080
  runtimeDirectory,
520
2081
  logsDirectory,
521
- stdoutLogPath: join3(logsDirectory, "stdout.log"),
522
- stderrLogPath: join3(logsDirectory, "stderr.log"),
2082
+ stdoutLogPath: join5(logsDirectory, "stdout.log"),
2083
+ stderrLogPath: join5(logsDirectory, "stderr.log"),
523
2084
  controlHome,
524
2085
  runtimePackageDir: runtimePackageDir(),
525
- bunExecutable: resolveBunExecutable2(),
2086
+ bunExecutable: serviceAdapter === "macos-scoutd" ? resolveBunExecutable2() : resolveOptionalBunExecutable(),
526
2087
  brokerHost,
527
2088
  brokerPort,
528
2089
  brokerUrl,
@@ -531,86 +2092,259 @@ function resolveBrokerServiceConfig() {
531
2092
  coreAgents: readCoreAgentsSync()
532
2093
  };
533
2094
  }
534
- function renderLaunchAgentPlist(config) {
535
- const launchPath = resolveLaunchAgentPATH();
536
- const coreAgentsValue = (config.coreAgents ?? []).join(",");
537
- const envEntries = {
2095
+ function resolveBrokerServiceAdapter(env = process.env, platform = process.platform) {
2096
+ return defaultServiceAdapterForPlatform(platform, env);
2097
+ }
2098
+ function executableCandidate(path) {
2099
+ return isExecutablePath(path) ? path : null;
2100
+ }
2101
+ function resolveExecutableName(name) {
2102
+ return resolveExecutableFromSearch({ names: [name] })?.path ?? null;
2103
+ }
2104
+ function resolveEnvExecutable(value) {
2105
+ const trimmed = value?.trim();
2106
+ if (!trimmed)
2107
+ return null;
2108
+ if (trimmed.includes("/") || trimmed.startsWith(".")) {
2109
+ const expanded = resolve2(expandHomePath(trimmed));
2110
+ return executableCandidate(expanded);
2111
+ }
2112
+ return resolveExecutableName(trimmed);
2113
+ }
2114
+ function findWorkspaceRootFromRuntimeDir(runtimePackageDir2) {
2115
+ let current = resolve2(runtimePackageDir2);
2116
+ while (true) {
2117
+ if (existsSync5(join5(current, "Cargo.toml")) && existsSync5(join5(current, "crates", "scoutd", "Cargo.toml"))) {
2118
+ return current;
2119
+ }
2120
+ const parent = dirname3(current);
2121
+ if (parent === current)
2122
+ return null;
2123
+ current = parent;
2124
+ }
2125
+ }
2126
+ function workspaceScoutdAllowed() {
2127
+ const raw = process.env.OPENSCOUT_ALLOW_WORKSPACE_SCOUTD?.trim().toLowerCase();
2128
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
2129
+ }
2130
+ function resolveScoutdCommand(config = resolveBrokerServiceConfig()) {
2131
+ const explicit = resolveEnvExecutable(process.env.OPENSCOUT_SCOUTD_BIN);
2132
+ if (explicit) {
2133
+ return { path: explicit, source: "env" };
2134
+ }
2135
+ const workspaceRoot = findWorkspaceRootFromRuntimeDir(config.runtimePackageDir);
2136
+ const packageCandidates = [
2137
+ join5(config.runtimePackageDir, "bin", "scoutd"),
2138
+ join5(config.runtimePackageDir, "native", "scoutd"),
2139
+ join5(config.runtimePackageDir, "scoutd"),
2140
+ workspaceRoot ? join5(workspaceRoot, "packages", "cli", "bin", "scoutd") : null,
2141
+ workspaceRoot ? join5(workspaceRoot, "packages", "runtime", "bin", "scoutd") : null,
2142
+ join5(config.runtimeDirectory, "scoutd"),
2143
+ join5(dirname3(config.runtimePackageDir), "scout", "bin", "scoutd")
2144
+ ];
2145
+ for (const candidate of packageCandidates) {
2146
+ const resolved = executableCandidate(candidate);
2147
+ if (resolved) {
2148
+ return { path: resolved, source: "package" };
2149
+ }
2150
+ }
2151
+ const fromPath = resolveExecutableName("scoutd");
2152
+ if (fromPath) {
2153
+ return { path: fromPath, source: "path" };
2154
+ }
2155
+ if (workspaceRoot && workspaceScoutdAllowed()) {
2156
+ for (const candidate of [
2157
+ join5(workspaceRoot, "target", "release", "scoutd"),
2158
+ join5(workspaceRoot, "target", "debug", "scoutd")
2159
+ ]) {
2160
+ const resolved = executableCandidate(candidate);
2161
+ if (resolved) {
2162
+ return { path: resolved, source: "workspace" };
2163
+ }
2164
+ }
2165
+ }
2166
+ return null;
2167
+ }
2168
+ function nativeServiceEnvironment(config, scoutdPath) {
2169
+ const env = {
2170
+ ...process.env,
2171
+ OPENSCOUT_SCOUTD_BIN: scoutdPath,
2172
+ OPENSCOUT_RUNTIME_PACKAGE_DIR: config.runtimePackageDir,
2173
+ OPENSCOUT_SUPPORT_DIRECTORY: config.supportDirectory,
2174
+ OPENSCOUT_CONTROL_HOME: config.controlHome,
538
2175
  OPENSCOUT_BROKER_HOST: config.brokerHost,
539
2176
  OPENSCOUT_BROKER_PORT: String(config.brokerPort),
540
2177
  OPENSCOUT_BROKER_URL: config.brokerUrl,
541
2178
  OPENSCOUT_BROKER_SOCKET_PATH: config.brokerSocketPath,
542
- OPENSCOUT_CONTROL_HOME: config.controlHome,
543
2179
  OPENSCOUT_BROKER_SERVICE_MODE: config.mode,
544
2180
  OPENSCOUT_BROKER_SERVICE_LABEL: config.label,
545
2181
  OPENSCOUT_SERVICE_LABEL: config.label,
546
- OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope,
547
- HOME: homedir3(),
548
- PATH: launchPath,
549
- ...collectOptionalEnvVars([
550
- "OPENSCOUT_MESH_ID",
551
- "OPENSCOUT_MESH_SEEDS",
552
- "OPENSCOUT_MESH_DISCOVERY_INTERVAL_MS",
553
- "OPENSCOUT_NODE_NAME",
554
- "OPENSCOUT_NODE_ID",
555
- "OPENSCOUT_NODE_QUALIFIER",
556
- "OPENSCOUT_TAILSCALE_BIN",
557
- "OPENSCOUT_TAILSCALE_STATUS_JSON",
558
- "OPENSCOUT_SSE_KEEPALIVE_MS",
559
- "OPENSCOUT_WEB_EDGE_SCHEME",
560
- "OPENSCOUT_WEB_PUBLIC_ORIGIN",
561
- "OPENSCOUT_WEB_PORTAL_HOST",
562
- "OPENSCOUT_WEB_LOCAL_NAME",
563
- "OPENSCOUT_WEB_ADVERTISED_HOST",
564
- "OPENSCOUT_WEB_TRUSTED_HOSTS",
565
- "OPENSCOUT_WEB_TRUSTED_ORIGINS",
566
- "OPENSCOUT_WEB_PORT",
567
- "OPENSCOUT_WEB_FLAG_BUNDLE",
568
- "OPENSCOUT_WEB_EXPERIENCE",
569
- "OPENSCOUT_WEB_AB_VARIANT"
570
- ])
2182
+ OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
2183
+ };
2184
+ if (config.bunExecutable) {
2185
+ env.OPENSCOUT_BUN_BIN = config.bunExecutable;
2186
+ }
2187
+ if (config.coreAgents.length > 0) {
2188
+ env.OPENSCOUT_CORE_AGENTS = config.coreAgents.join(",");
2189
+ }
2190
+ return env;
2191
+ }
2192
+ function isRecord3(value) {
2193
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2194
+ }
2195
+ function readString2(value) {
2196
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
2197
+ }
2198
+ function readBoolean(value) {
2199
+ return typeof value === "boolean" ? value : undefined;
2200
+ }
2201
+ function readNumber2(value) {
2202
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
2203
+ }
2204
+ function readHealthTransport(value) {
2205
+ return value === "unix_socket" || value === "http" || value === "in_process" ? value : undefined;
2206
+ }
2207
+ function normalizeNativeServiceStatus(input, config) {
2208
+ const healthRecord = isRecord3(input.health) ? input.health : {};
2209
+ const healthReachable = readBoolean(healthRecord.reachable) ?? readBoolean(input.reachable) ?? false;
2210
+ const healthOk = readBoolean(healthRecord.ok) ?? (typeof input.health === "boolean" ? input.health : undefined) ?? false;
2211
+ const healthError = readString2(healthRecord.error) ?? readString2(input.healthError);
2212
+ const healthTransport = readHealthTransport(healthRecord.transport) ?? readHealthTransport(input.healthTransport);
2213
+ const healthNodeId = readString2(healthRecord.nodeId);
2214
+ const healthMeshId = readString2(healthRecord.meshId);
2215
+ const healthSocketFallbackError = readString2(healthRecord.socketFallbackError);
2216
+ const healthCounts = isRecord3(healthRecord.counts) ? healthRecord.counts : undefined;
2217
+ const installed = readBoolean(input.installed) ?? existsSync5(config.launchAgentPath);
2218
+ const loaded = readBoolean(input.loaded) ?? false;
2219
+ const stdoutLogPath = readString2(input.stdoutLogPath) ?? config.stdoutLogPath;
2220
+ const stderrLogPath = readString2(input.stderrLogPath) ?? config.stderrLogPath;
2221
+ const lastLogLine = readString2(input.lastLogLine) ?? (healthReachable ? readLastLogLine([stdoutLogPath, stderrLogPath]) : readLastLogLine([stderrLogPath, stdoutLogPath]));
2222
+ return {
2223
+ label: readString2(input.label) ?? config.label,
2224
+ mode: readString2(input.mode) ?? config.mode,
2225
+ launchAgentPath: readString2(input.launchAgentPath) ?? config.launchAgentPath,
2226
+ bootoutCommand: readString2(input.bootoutCommand) ?? bootoutCommand(config),
2227
+ brokerUrl: readString2(input.brokerUrl) ?? config.brokerUrl,
2228
+ brokerSocketPath: readString2(input.brokerSocketPath) ?? config.brokerSocketPath,
2229
+ supportDirectory: readString2(input.supportDirectory) ?? config.supportDirectory,
2230
+ runtimeDirectory: readString2(input.runtimeDirectory) ?? config.runtimeDirectory,
2231
+ controlHome: readString2(input.controlHome) ?? config.controlHome,
2232
+ stdoutLogPath,
2233
+ stderrLogPath,
2234
+ installed,
2235
+ loaded,
2236
+ pid: readNumber2(input.pid) ?? null,
2237
+ launchdState: readString2(input.launchdState) ?? null,
2238
+ lastExitStatus: readNumber2(input.lastExitStatus) ?? null,
2239
+ usesLaunchAgent: readBoolean(input.usesLaunchAgent) ?? (installed || loaded),
2240
+ reachable: healthReachable,
2241
+ serviceAdapter: "macos-scoutd",
2242
+ health: {
2243
+ reachable: healthReachable,
2244
+ ok: healthOk,
2245
+ checkedAt: readNumber2(healthRecord.checkedAt) ?? Date.now(),
2246
+ transport: healthTransport,
2247
+ socketPath: config.brokerSocketPath,
2248
+ ...healthSocketFallbackError ? { socketFallbackError: healthSocketFallbackError } : {},
2249
+ ...healthNodeId ? { nodeId: healthNodeId } : {},
2250
+ ...healthMeshId ? { meshId: healthMeshId } : {},
2251
+ ...healthCounts ? { counts: healthCounts } : {},
2252
+ ...isRecord3(healthRecord.build) ? { build: healthRecord.build } : {},
2253
+ ...isRecord3(healthRecord.services) ? { services: healthRecord.services } : {},
2254
+ error: healthError
2255
+ },
2256
+ lastLogLine
571
2257
  };
572
- if (coreAgentsValue) {
573
- envEntries["OPENSCOUT_CORE_AGENTS"] = coreAgentsValue;
574
- }
575
- const envBlock = Object.entries(envEntries).map(([key, value]) => `
576
- <key>${xmlEscape(key)}</key>
577
- <string>${xmlEscape(value)}</string>`).join("");
578
- return `<?xml version="1.0" encoding="UTF-8"?>
579
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
580
- <plist version="1.0">
581
- <dict>
582
- <key>Label</key>
583
- <string>${xmlEscape(config.label)}</string>
584
- <key>ProgramArguments</key>
585
- <array>
586
- <string>${xmlEscape(config.bunExecutable)}</string>
587
- <string>${xmlEscape(join3(config.runtimePackageDir, "bin", "openscout-runtime.mjs"))}</string>
588
- <string>base</string>
589
- </array>
590
- <key>WorkingDirectory</key>
591
- <string>${xmlEscape(config.runtimePackageDir)}</string>
592
- <key>RunAtLoad</key>
593
- <true/>
594
- <key>KeepAlive</key>
595
- <dict>
596
- <key>SuccessfulExit</key>
597
- <false/>
598
- </dict>
599
- <key>StandardOutPath</key>
600
- <string>${xmlEscape(config.stdoutLogPath)}</string>
601
- <key>StandardErrorPath</key>
602
- <string>${xmlEscape(config.stderrLogPath)}</string>
603
- <key>EnvironmentVariables</key>
604
- <dict>${envBlock}
605
- </dict>
606
- </dict>
607
- </plist>
608
- `;
2258
+ }
2259
+ var SCOUTD_MAX_BUFFER = 2 * 1024 * 1024;
2260
+ var SCOUTD_DEFAULT_TIMEOUT_MS = 45000;
2261
+ var SCOUTD_KILL_GRACE_MS = 250;
2262
+ function spawnScoutdJson(scoutdPath, command, env, timeoutMs) {
2263
+ return new Promise((resolvePromise, reject) => {
2264
+ const child = spawn2(scoutdPath, [command, "--json"], {
2265
+ env,
2266
+ stdio: ["ignore", "pipe", "pipe"]
2267
+ });
2268
+ let stdout = "";
2269
+ let stderr = "";
2270
+ let settled = false;
2271
+ let timedOut = false;
2272
+ const killTimer = setTimeout(() => {
2273
+ timedOut = true;
2274
+ terminate();
2275
+ fail(new Error(`scoutd ${command} timed out after ${timeoutMs}ms`));
2276
+ }, timeoutMs);
2277
+ killTimer.unref?.();
2278
+ function terminate() {
2279
+ child.kill("SIGTERM");
2280
+ const hardKillTimer = setTimeout(() => child.kill("SIGKILL"), SCOUTD_KILL_GRACE_MS);
2281
+ hardKillTimer.unref?.();
2282
+ }
2283
+ function cleanup() {
2284
+ clearTimeout(killTimer);
2285
+ }
2286
+ function fail(error) {
2287
+ if (settled)
2288
+ return;
2289
+ settled = true;
2290
+ cleanup();
2291
+ reject(error);
2292
+ }
2293
+ function succeed(output) {
2294
+ if (settled)
2295
+ return;
2296
+ settled = true;
2297
+ cleanup();
2298
+ resolvePromise(output);
2299
+ }
2300
+ function append(kind, chunk) {
2301
+ const text = typeof chunk === "string" ? chunk : String(chunk);
2302
+ if (kind === "stdout")
2303
+ stdout += text;
2304
+ else
2305
+ stderr += text;
2306
+ if (stdout.length + stderr.length > SCOUTD_MAX_BUFFER) {
2307
+ terminate();
2308
+ fail(new Error(`scoutd ${command} exceeded output limit`));
2309
+ }
2310
+ }
2311
+ child.stdout.setEncoding("utf8");
2312
+ child.stderr.setEncoding("utf8");
2313
+ child.stdout.on("data", (chunk) => append("stdout", chunk));
2314
+ child.stderr.on("data", (chunk) => append("stderr", chunk));
2315
+ child.on("error", (error) => fail(new Error(`scoutd ${command} failed: ${error.message}`)));
2316
+ child.on("close", (code, signal) => {
2317
+ if (settled || timedOut)
2318
+ return;
2319
+ const trimmedStdout = stdout.trim();
2320
+ const trimmedStderr = stderr.trim();
2321
+ if ((code ?? 1) !== 0) {
2322
+ const detail = trimmedStderr || trimmedStdout || `exit ${signal ?? code ?? "unknown status"}`;
2323
+ fail(new Error(`scoutd ${command} failed: ${detail}`));
2324
+ return;
2325
+ }
2326
+ succeed(trimmedStdout);
2327
+ });
2328
+ });
2329
+ }
2330
+ async function runScoutdServiceCommand(command, config, timeoutMs = SCOUTD_DEFAULT_TIMEOUT_MS, runScoutdJson = spawnScoutdJson) {
2331
+ const scoutd = resolveScoutdCommand(config);
2332
+ if (!scoutd) {
2333
+ throw new Error("Unable to locate scoutd for broker service management. Build scoutd with `npm run scoutd:build`, install a package that includes scoutd, or set OPENSCOUT_SCOUTD_BIN.");
2334
+ }
2335
+ const stdout = await runScoutdJson(scoutd.path, command, nativeServiceEnvironment(config, scoutd.path), timeoutMs);
2336
+ let parsed;
2337
+ try {
2338
+ parsed = JSON.parse(stdout);
2339
+ } catch {
2340
+ throw new Error(`scoutd ${command} returned non-JSON stdout: ${stdout.slice(0, 400)}`);
2341
+ }
2342
+ return normalizeNativeServiceStatus(parsed, config);
609
2343
  }
610
2344
  function readCoreAgentsSync() {
611
2345
  try {
612
2346
  const settingsPath = resolveOpenScoutSupportPaths().settingsPath;
613
- const raw = readFileSync(settingsPath, "utf8");
2347
+ const raw = readFileSync4(settingsPath, "utf8");
614
2348
  const settings = JSON.parse(raw);
615
2349
  const raw_agents = settings?.agents?.coreAgents;
616
2350
  if (Array.isArray(raw_agents)) {
@@ -619,88 +2353,14 @@ function readCoreAgentsSync() {
619
2353
  } catch {}
620
2354
  return [];
621
2355
  }
622
- function collectOptionalEnvVars(keys) {
623
- const entries = {};
624
- for (const key of keys) {
625
- const value = process.env[key];
626
- if (typeof value === "string" && value.trim().length > 0) {
627
- entries[key] = value;
628
- }
629
- }
630
- return entries;
631
- }
632
- function resolveLaunchAgentPATH() {
633
- const entries = [
634
- join3(homedir3(), ".bun", "bin"),
635
- ...(process.env.PATH ?? "").split(":").filter(Boolean),
636
- "/opt/homebrew/bin",
637
- "/usr/local/bin",
638
- "/usr/bin",
639
- "/bin",
640
- "/usr/sbin",
641
- "/sbin"
642
- ];
643
- return Array.from(new Set(entries)).filter((e) => !isTmpPath(e)).join(":");
644
- }
645
- function xmlEscape(value) {
646
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
647
- }
648
- function ensureParentDirectory(filePath) {
649
- mkdirSync2(dirname2(filePath), { recursive: true });
650
- }
651
- function ensureServiceDirectories(config) {
652
- ensureOpenScoutCleanSlateSync();
653
- mkdirSync2(config.supportDirectory, { recursive: true });
654
- mkdirSync2(config.runtimeDirectory, { recursive: true, mode: 448 });
655
- chmodSync(config.runtimeDirectory, 448);
656
- mkdirSync2(config.logsDirectory, { recursive: true });
657
- mkdirSync2(config.controlHome, { recursive: true });
658
- ensureParentDirectory(config.launchAgentPath);
659
- }
660
- function writeLaunchAgentPlist(config) {
661
- ensureServiceDirectories(config);
662
- writeFileSync2(config.launchAgentPath, renderLaunchAgentPlist(config), "utf8");
663
- }
664
2356
  function bootoutCommand(config) {
665
- return `${launchctlPath()} bootout ${config.serviceTarget}`;
666
- }
667
- function legacyLaunchAgentPath(config) {
668
- return join3(homedir3(), "Library", "LaunchAgents", `${legacyBrokerServiceLabel(config.mode)}.plist`);
669
- }
670
- function legacyServiceTarget(config) {
671
- return `${config.domainTarget}/${legacyBrokerServiceLabel(config.mode)}`;
672
- }
673
- function bootoutLegacyBrokerService(config) {
674
- const legacyTarget = legacyServiceTarget(config);
675
- if (legacyTarget === config.serviceTarget) {
676
- return;
677
- }
678
- runCommand(launchctlPath(), ["bootout", legacyTarget], { allowFailure: true });
679
- }
680
- function runCommand(command, args, options) {
681
- const result = spawnSync(command, args, {
682
- env: {
683
- ...process.env,
684
- ...options?.env ?? {}
685
- },
686
- encoding: "utf8"
687
- });
688
- const stdout = (result.stdout ?? "").trim();
689
- const stderr = (result.stderr ?? "").trim();
690
- const exitCode = result.status ?? 1;
691
- if (exitCode !== 0 && !options?.allowFailure) {
692
- throw new Error(stderr || stdout || `${command} exited with status ${exitCode}`);
693
- }
694
- return { exitCode, stdout, stderr };
695
- }
696
- function launchctlPath() {
697
- return "/bin/launchctl";
2357
+ return `/bin/launchctl bootout ${config.serviceTarget}`;
698
2358
  }
699
2359
  function readLogLines(path) {
700
- if (!existsSync3(path)) {
2360
+ if (!existsSync5(path)) {
701
2361
  return [];
702
2362
  }
703
- return readFileSync(path, "utf8").split(`
2363
+ return readFileSync4(path, "utf8").split(`
704
2364
  `).map((line) => line.trim()).filter(Boolean);
705
2365
  }
706
2366
  function isPackageScriptBanner(line) {
@@ -733,170 +2393,120 @@ function readLastLogLine(paths) {
733
2393
  }
734
2394
  return fallback;
735
2395
  }
736
- function parseLaunchctlPrint(output) {
737
- const pidMatch = output.match(/\bpid = (\d+)/);
738
- const stateMatch = output.match(/\bstate = ([^\n]+)/);
739
- const lastExitMatch = output.match(/\blast exit code = (-?\d+)/i) || output.match(/\blast exit status = (-?\d+)/i);
2396
+ async function readHeadlessBrokerHealth(config) {
2397
+ const result = await requestScoutBrokerJsonWithTrace(config.brokerUrl, "/health", { socketPath: config.brokerSocketPath });
740
2398
  return {
741
- pid: pidMatch ? Number.parseInt(pidMatch[1] ?? "0", 10) : null,
742
- launchdState: stateMatch?.[1]?.trim() ?? null,
743
- lastExitStatus: lastExitMatch ? Number.parseInt(lastExitMatch[1] ?? "0", 10) : null
2399
+ health: result.value,
2400
+ trace: result.trace
744
2401
  };
745
2402
  }
746
- function inspectLaunchctl(config) {
747
- const printResult = runCommand(launchctlPath(), ["print", config.serviceTarget], { allowFailure: true });
748
- if (printResult.exitCode !== 0) {
749
- return {
750
- loaded: false,
751
- pid: null,
752
- launchdState: null,
753
- lastExitStatus: null,
754
- raw: printResult.stderr || printResult.stdout
755
- };
756
- }
757
- return {
758
- loaded: true,
759
- raw: printResult.stdout,
760
- ...parseLaunchctlPrint(printResult.stdout)
761
- };
2403
+ function headlessLifecycleError(command) {
2404
+ return new Error(`Next step: run \`openscout-runtime broker\` in this shell or under your process manager. ` + `The headless service adapter leaves broker ${command} to that foreground process.`);
762
2405
  }
763
- async function fetchHealthSnapshot(config) {
764
- const controller = new AbortController;
765
- const timeout = setTimeout(() => controller.abort(), 1000);
2406
+ async function runHeadlessForegroundServiceCommand(command, config, readHealth = readHeadlessBrokerHealth) {
2407
+ if (command !== "status") {
2408
+ throw headlessLifecycleError(command);
2409
+ }
766
2410
  const checkedAt = Date.now();
767
2411
  try {
768
- const { value: payload, trace } = await requestScoutBrokerJsonWithTrace(config.brokerUrl, "/health", {
769
- signal: controller.signal,
770
- socketPath: config.brokerSocketPath
771
- });
2412
+ const { health, trace } = await readHealth(config);
2413
+ const reachable = true;
772
2414
  return {
773
- reachable: true,
774
- ok: Boolean(payload.ok),
775
- checkedAt,
776
- transport: trace.transport,
777
- socketPath: trace.socketPath,
778
- socketFallbackError: trace.socketFallbackError,
779
- nodeId: payload.nodeId,
780
- meshId: payload.meshId,
781
- counts: payload.counts ? {
782
- nodes: payload.counts.nodes ?? 0,
783
- actors: payload.counts.actors ?? 0,
784
- agents: payload.counts.agents ?? 0,
785
- agentRecords: payload.counts.agentRecords,
786
- rawAgentRecords: payload.counts.rawAgentRecords,
787
- configuredAgents: payload.counts.configuredAgents,
788
- scoutManagedAgents: payload.counts.scoutManagedAgents,
789
- currentAgentRegistrations: payload.counts.currentAgentRegistrations,
790
- localAgentRegistrations: payload.counts.localAgentRegistrations,
791
- remoteAgentRegistrations: payload.counts.remoteAgentRegistrations,
792
- staleAgentRegistrations: payload.counts.staleAgentRegistrations,
793
- retiredAgentRegistrations: payload.counts.retiredAgentRegistrations,
794
- oneTimeAgentCards: payload.counts.oneTimeAgentCards,
795
- persistentAgentCards: payload.counts.persistentAgentCards,
796
- conversations: payload.counts.conversations ?? 0,
797
- messages: payload.counts.messages ?? 0,
798
- flights: payload.counts.flights ?? 0,
799
- collaborationRecords: payload.counts.collaborationRecords ?? 0
800
- } : undefined
2415
+ serviceAdapter: "headless-foreground",
2416
+ label: config.label,
2417
+ mode: config.mode,
2418
+ launchAgentPath: config.launchAgentPath,
2419
+ bootoutCommand: "headless-foreground does not use launchd",
2420
+ brokerUrl: config.brokerUrl,
2421
+ brokerSocketPath: config.brokerSocketPath,
2422
+ supportDirectory: config.supportDirectory,
2423
+ runtimeDirectory: config.runtimeDirectory,
2424
+ controlHome: config.controlHome,
2425
+ stdoutLogPath: config.stdoutLogPath,
2426
+ stderrLogPath: config.stderrLogPath,
2427
+ installed: false,
2428
+ loaded: reachable,
2429
+ pid: null,
2430
+ launchdState: null,
2431
+ lastExitStatus: null,
2432
+ usesLaunchAgent: false,
2433
+ reachable,
2434
+ health: {
2435
+ reachable,
2436
+ ok: Boolean(health.ok),
2437
+ checkedAt,
2438
+ transport: trace.transport,
2439
+ socketPath: trace.socketPath,
2440
+ socketFallbackError: trace.socketFallbackError,
2441
+ nodeId: health.nodeId ?? undefined,
2442
+ meshId: health.meshId ?? undefined,
2443
+ counts: health.counts ?? undefined,
2444
+ build: health.build,
2445
+ services: health.services
2446
+ },
2447
+ lastLogLine: readLastLogLine([config.stderrLogPath, config.stdoutLogPath])
801
2448
  };
802
2449
  } catch (error) {
803
2450
  return {
2451
+ serviceAdapter: "headless-foreground",
2452
+ label: config.label,
2453
+ mode: config.mode,
2454
+ launchAgentPath: config.launchAgentPath,
2455
+ bootoutCommand: "headless-foreground does not use launchd",
2456
+ brokerUrl: config.brokerUrl,
2457
+ brokerSocketPath: config.brokerSocketPath,
2458
+ supportDirectory: config.supportDirectory,
2459
+ runtimeDirectory: config.runtimeDirectory,
2460
+ controlHome: config.controlHome,
2461
+ stdoutLogPath: config.stdoutLogPath,
2462
+ stderrLogPath: config.stderrLogPath,
2463
+ installed: false,
2464
+ loaded: false,
2465
+ pid: null,
2466
+ launchdState: null,
2467
+ lastExitStatus: null,
2468
+ usesLaunchAgent: false,
804
2469
  reachable: false,
805
- ok: false,
806
- checkedAt,
807
- socketPath: config.brokerSocketPath,
808
- error: error instanceof Error ? error.message : String(error)
2470
+ health: {
2471
+ reachable: false,
2472
+ ok: false,
2473
+ checkedAt,
2474
+ error: error instanceof Error ? error.message : String(error)
2475
+ },
2476
+ lastLogLine: readLastLogLine([config.stderrLogPath, config.stdoutLogPath])
809
2477
  };
810
- } finally {
811
- clearTimeout(timeout);
2478
+ }
2479
+ }
2480
+ async function runBrokerServiceCommand(command, config) {
2481
+ const adapter = resolveBrokerServiceAdapter();
2482
+ switch (adapter) {
2483
+ case "macos-scoutd":
2484
+ return await runScoutdServiceCommand(command, config);
2485
+ case "headless-foreground":
2486
+ return await runHeadlessForegroundServiceCommand(command, config);
2487
+ case "linux-systemd-user":
2488
+ throw new Error("The linux-systemd-user service adapter is not implemented yet. " + "Use OPENSCOUT_SERVICE_ADAPTER=headless-foreground and run `openscout-runtime broker` under your process manager.");
2489
+ case "windows-service":
2490
+ throw new Error("The windows-service adapter is not implemented yet. " + "Use OPENSCOUT_SERVICE_ADAPTER=headless-foreground and run `openscout-runtime broker` from a shell.");
812
2491
  }
813
2492
  }
814
2493
  async function brokerServiceStatus(config = resolveBrokerServiceConfig()) {
815
- ensureServiceDirectories(config);
816
- const launchctl = inspectLaunchctl(config);
817
- const health = await fetchHealthSnapshot(config);
818
- const installed = existsSync3(config.launchAgentPath);
819
- const lastLogLine = health.reachable ? readLastLogLine([config.stdoutLogPath, config.stderrLogPath]) : readLastLogLine([config.stderrLogPath, config.stdoutLogPath]);
820
- return {
821
- label: config.label,
822
- mode: config.mode,
823
- launchAgentPath: config.launchAgentPath,
824
- bootoutCommand: bootoutCommand(config),
825
- brokerUrl: config.brokerUrl,
826
- brokerSocketPath: config.brokerSocketPath,
827
- supportDirectory: config.supportDirectory,
828
- runtimeDirectory: config.runtimeDirectory,
829
- controlHome: config.controlHome,
830
- stdoutLogPath: config.stdoutLogPath,
831
- stderrLogPath: config.stderrLogPath,
832
- installed,
833
- loaded: launchctl.loaded,
834
- pid: launchctl.pid,
835
- launchdState: launchctl.launchdState,
836
- lastExitStatus: launchctl.lastExitStatus,
837
- usesLaunchAgent: installed || launchctl.loaded,
838
- reachable: health.reachable,
839
- health,
840
- lastLogLine
841
- };
2494
+ return runBrokerServiceCommand("status", config);
842
2495
  }
843
2496
  async function installBrokerService(config = resolveBrokerServiceConfig()) {
844
- bootoutLegacyBrokerService(config);
845
- writeLaunchAgentPlist(config);
846
- return brokerServiceStatus(config);
2497
+ return runBrokerServiceCommand("install", config);
847
2498
  }
848
2499
  async function startBrokerService(config = resolveBrokerServiceConfig()) {
849
- bootoutLegacyBrokerService(config);
850
- writeLaunchAgentPlist(config);
851
- runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
852
- await waitForBrokerServiceStopped(config);
853
- runCommand(launchctlPath(), ["bootstrap", config.domainTarget, config.launchAgentPath], { allowFailure: true });
854
- runCommand(launchctlPath(), ["kickstart", "-k", config.serviceTarget], { allowFailure: true });
855
- const attempts = Math.ceil(resolveBrokerStartTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
856
- for (let attempt = 0;attempt < attempts; attempt += 1) {
857
- const status2 = await brokerServiceStatus(config);
858
- if (status2.health.reachable) {
859
- return status2;
860
- }
861
- await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
862
- }
863
- const status = await brokerServiceStatus(config);
864
- throw new Error(status.lastLogLine ?? status.health.error ?? "Broker service did not become healthy.");
865
- }
866
- function sleep(ms) {
867
- return new Promise((resolve3) => setTimeout(resolve3, ms));
868
- }
869
- async function waitForBrokerServiceStopped(config) {
870
- let status = await brokerServiceStatus(config);
871
- const attempts = Math.ceil(resolveBrokerStopTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
872
- for (let attempt = 0;attempt < attempts; attempt += 1) {
873
- status = await brokerServiceStatus(config);
874
- if (!status.loaded && !status.health.reachable) {
875
- return status;
876
- }
877
- await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
878
- }
879
- return status;
2500
+ return runBrokerServiceCommand("start", config);
880
2501
  }
881
2502
  async function stopBrokerService(config = resolveBrokerServiceConfig()) {
882
- runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
883
- return waitForBrokerServiceStopped(config);
2503
+ return runBrokerServiceCommand("stop", config);
884
2504
  }
885
2505
  async function restartBrokerService(config = resolveBrokerServiceConfig()) {
886
- await stopBrokerService(config);
887
- return startBrokerService(config);
2506
+ return runBrokerServiceCommand("restart", config);
888
2507
  }
889
2508
  async function uninstallBrokerService(config = resolveBrokerServiceConfig()) {
890
- await stopBrokerService(config);
891
- if (existsSync3(config.launchAgentPath)) {
892
- rmSync2(config.launchAgentPath, { force: true });
893
- }
894
- const legacyPath = legacyLaunchAgentPath(config);
895
- bootoutLegacyBrokerService(config);
896
- if (existsSync3(legacyPath)) {
897
- rmSync2(legacyPath, { force: true });
898
- }
899
- return brokerServiceStatus(config);
2509
+ return runBrokerServiceCommand("uninstall", config);
900
2510
  }
901
2511
  async function main() {
902
2512
  const command = process.argv[2] ?? "status";
@@ -934,6 +2544,7 @@ async function main() {
934
2544
  }
935
2545
  function formatBrokerServiceStatus(status) {
936
2546
  const lines = [
2547
+ `service adapter: ${status.serviceAdapter ?? "unknown"}`,
937
2548
  `label: ${status.label}`,
938
2549
  `mode: ${status.mode}`,
939
2550
  `launch agent: ${status.installed ? status.launchAgentPath : "not installed"}`,
@@ -968,15 +2579,21 @@ export {
968
2579
  stopBrokerService,
969
2580
  startBrokerService,
970
2581
  selectLastRelevantLogLine,
2582
+ runScoutdServiceCommand,
2583
+ runHeadlessForegroundServiceCommand,
971
2584
  restartBrokerService,
2585
+ resolveScoutdCommand,
2586
+ resolveScoutBrokerControlUrl,
2587
+ resolveBundledRuntimeDirFromModuleDir,
2588
+ resolveBrokerUrl,
972
2589
  resolveBrokerSocketPathForBaseUrl,
973
2590
  resolveBrokerServiceConfig,
2591
+ resolveBrokerServiceAdapter,
974
2592
  resolveBrokerHost,
975
2593
  resolveAdvertiseScope,
976
- renderLaunchAgentPlist,
977
- parseLaunchctlPrint,
978
2594
  isLoopbackHost,
979
2595
  installBrokerService,
2596
+ buildLocalBrokerControlUrl,
980
2597
  buildDefaultBrokerUrl,
981
2598
  buildDefaultBrokerSocketPath,
982
2599
  brokerServiceStatus,