@openscout/protocol 0.2.98 → 0.2.104

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 (63) hide show
  1. package/dist/actors.d.ts +2 -2
  2. package/dist/actors.d.ts.map +1 -1
  3. package/dist/actors.js +1 -0
  4. package/dist/broker-routes.d.ts +8 -0
  5. package/dist/broker-routes.d.ts.map +1 -1
  6. package/dist/broker-routes.js +15 -0
  7. package/dist/channel-identity.d.ts +77 -0
  8. package/dist/channel-identity.d.ts.map +1 -1
  9. package/dist/channel-identity.js +134 -0
  10. package/dist/channel-invites.d.ts +179 -0
  11. package/dist/channel-invites.d.ts.map +1 -0
  12. package/dist/channel-invites.js +166 -0
  13. package/dist/channel-reception.d.ts +78 -0
  14. package/dist/channel-reception.d.ts.map +1 -0
  15. package/dist/channel-reception.js +219 -0
  16. package/dist/common.d.ts +1 -1
  17. package/dist/common.d.ts.map +1 -1
  18. package/dist/conversation-projection.d.ts +1 -1
  19. package/dist/conversation-projection.js +1 -1
  20. package/dist/herdr-workspace-digest.d.ts +151 -0
  21. package/dist/herdr-workspace-digest.d.ts.map +1 -0
  22. package/dist/herdr-workspace-digest.js +343 -0
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +4 -0
  26. package/dist/machines.d.ts +233 -0
  27. package/dist/machines.d.ts.map +1 -0
  28. package/dist/machines.js +550 -0
  29. package/dist/mesh.d.ts +8 -0
  30. package/dist/mesh.d.ts.map +1 -1
  31. package/dist/runtime-catalog-contract.d.ts +1 -1
  32. package/dist/runtime-catalog-contract.d.ts.map +1 -1
  33. package/dist/runtime-catalog-contract.js +1 -1
  34. package/dist/runtime-catalog.generated.d.ts +34 -15
  35. package/dist/runtime-catalog.generated.d.ts.map +1 -1
  36. package/dist/runtime-catalog.generated.js +46 -19
  37. package/dist/runtime-execution.d.ts +1 -1
  38. package/dist/runtime-execution.d.ts.map +1 -1
  39. package/dist/scout-delivery.d.ts +1 -0
  40. package/dist/scout-delivery.d.ts.map +1 -1
  41. package/dist/terminal-host-topology.d.ts +20 -1
  42. package/dist/terminal-host-topology.d.ts.map +1 -1
  43. package/dist/terminal-host-topology.js +8 -2
  44. package/dist/transports.d.ts +39 -1
  45. package/dist/transports.d.ts.map +1 -1
  46. package/package.json +2 -2
  47. package/src/actors.ts +2 -0
  48. package/src/broker-routes.ts +17 -0
  49. package/src/channel-identity.ts +151 -0
  50. package/src/channel-invites.ts +343 -0
  51. package/src/channel-reception.ts +308 -0
  52. package/src/common.ts +1 -0
  53. package/src/conversation-projection.ts +1 -1
  54. package/src/herdr-workspace-digest.ts +458 -0
  55. package/src/index.ts +4 -0
  56. package/src/machines.ts +743 -0
  57. package/src/mesh.ts +8 -0
  58. package/src/runtime-catalog-contract.ts +1 -1
  59. package/src/runtime-catalog.generated.ts +46 -19
  60. package/src/runtime-catalog.v1.json +18 -8
  61. package/src/scout-delivery.ts +1 -0
  62. package/src/terminal-host-topology.ts +32 -2
  63. package/src/transports.ts +48 -0
@@ -0,0 +1,550 @@
1
+ /**
2
+ * Machines — the physical (or virtual) boxes Scout knows about.
3
+ *
4
+ * A machine is NOT a node. A node is a Scout broker runtime; a machine is the
5
+ * computer it runs on. The distinction matters because most machines Scout can
6
+ * see are not running Scout at all: a tailnet peer, a Mac advertising itself on
7
+ * the LAN, a Linux box that only answers ARP. Modelling them as nodes would
8
+ * either invent fake nodes or make them invisible, and both were wrong.
9
+ *
10
+ * A machine record is assembled from **evidence**, never asserted. Four
11
+ * channels report independently and none of them is authoritative:
12
+ *
13
+ * | channel | tells us |
14
+ * | --------- | ----------------------------------------------- |
15
+ * | `scout` | a broker node lives here (id, mesh, urls) |
16
+ * | `tailnet` | reachable over Tailscale (stable peer id, IPs) |
17
+ * | `lan` | seen on this network (mDNS advert, ARP neighbor)|
18
+ * | `host` | terminal hosts present (herdr, tmux, zellij) |
19
+ *
20
+ * The join between them is `machineIdentityKeys` + `groupMachineEvidence`.
21
+ * Everything else in this module is a pure derivation over a group, so the
22
+ * whole model is testable without a network, a broker, or a database.
23
+ */
24
+ export const MACHINE_ID_PREFIX = "mach-";
25
+ const EMPTY_KEYS = { durable: [], transient: [] };
26
+ /** mDNS service types Scout browses, and what each one proves. */
27
+ export const MACHINE_SERVICE_CAPABILITIES = {
28
+ "_openscout._tcp": "scout-broker",
29
+ "_oscout-pair._tcp": "scout-pairing",
30
+ "_ssh._tcp": "ssh",
31
+ "_sftp-ssh._tcp": "ssh",
32
+ "_rfb._tcp": "vnc",
33
+ "_smb._tcp": "smb",
34
+ "_http._tcp": "http",
35
+ };
36
+ /** Browsed for inventory. `_workstation._tcp` proves nothing but presence. */
37
+ export const MACHINE_LAN_SERVICE_TYPES = [
38
+ "_openscout._tcp",
39
+ "_oscout-pair._tcp",
40
+ "_ssh._tcp",
41
+ "_workstation._tcp",
42
+ "_rfb._tcp",
43
+ "_smb._tcp",
44
+ ];
45
+ /* ── Normalization ── */
46
+ /**
47
+ * Reduce a host label to the machine's short name: `mini.local.` → `mini`,
48
+ * `mini.tail1234.ts.net` → `mini`.
49
+ *
50
+ * Bonjour renames a colliding instance to `mini (2)` or `mini-2.local`, and
51
+ * both must fold back to `mini` or one machine becomes two. The numeric form
52
+ * is only stripped for a `.local` name — a machine genuinely called `node-1`
53
+ * must keep its digit, which is the same rule
54
+ * `stripBonjourCollisionSuffix` follows in the runtime.
55
+ */
56
+ /**
57
+ * Host names that every machine can answer to and therefore identify none of
58
+ * them. iOS devices report `localhost` as their Tailscale host name, so
59
+ * treating this as identity fuses an iPhone and an iPad into one record.
60
+ */
61
+ const NON_IDENTIFYING_HOST_NAMES = new Set(["localhost", "ip6-localhost", "unknown", "android"]);
62
+ export function normalizeMachineHostName(value) {
63
+ const raw = (value ?? "").trim().toLowerCase();
64
+ if (!raw)
65
+ return "";
66
+ const withoutTrailingDot = raw.replace(/\.+$/, "");
67
+ const isMdnsName = /\.local$/.test(withoutTrailingDot);
68
+ const firstLabel = (withoutTrailingDot.split(".")[0] ?? "").replace(/\s*\(\d+\)$/, "").trim();
69
+ // A machine that has re-registered hundreds of times carries a suffix to
70
+ // match: `Arts-Mac-mini-769.local` and `Arts-Mac-mini-612.local` are one Mac,
71
+ // and capping at two digits left 25 of them as 25 machines.
72
+ const name = isMdnsName ? firstLabel.replace(/-\d{1,4}$/, "") : firstLabel;
73
+ return NON_IDENTIFYING_HOST_NAMES.has(name) ? "" : name;
74
+ }
75
+ /**
76
+ * Lowercase colon-form MAC. Returns "" for entries that identify nothing —
77
+ * broadcast, all-zero, or an incomplete ARP row.
78
+ *
79
+ * BSD `arp` drops leading zeros (`3c:22:fb:1:2:3`), so octets are padded
80
+ * rather than the string being scrubbed of separators: scrubbing turns that
81
+ * nine-character form into a rejected MAC instead of a valid one.
82
+ */
83
+ export function normalizeMacAddress(value) {
84
+ const raw = (value ?? "").trim().toLowerCase();
85
+ if (!raw)
86
+ return "";
87
+ const groups = raw.split(/[:-]/);
88
+ const octets = groups.length === 6
89
+ ? groups.map((group) => (/^[0-9a-f]{1,2}$/.test(group) ? group.padStart(2, "0") : ""))
90
+ : (raw.replace(/[^0-9a-f]/g, "").match(/.{2}/g) ?? []);
91
+ if (octets.length !== 6 || octets.some((octet) => !/^[0-9a-f]{2}$/.test(octet)))
92
+ return "";
93
+ const joined = octets.join(":");
94
+ if (joined === "00:00:00:00:00:00")
95
+ return "";
96
+ // The low bit of the first octet is the I/G bit: when it is set the address
97
+ // is a group (multicast or broadcast) destination, never a station address.
98
+ // Real ARP tables are full of these — `01:00:5e:00:00:fb` is mDNS, not a
99
+ // machine — and treating one as identity would mint a phantom neighbour on
100
+ // every network Scout ever sees.
101
+ if ((Number.parseInt(octets[0] ?? "0", 16) & 0x01) === 1)
102
+ return "";
103
+ return joined;
104
+ }
105
+ export function normalizeMachineAddress(value) {
106
+ const raw = (value ?? "").trim().toLowerCase();
107
+ if (!raw)
108
+ return "";
109
+ return raw.replace(/^\[/, "").replace(/\]$/, "").replace(/%.*$/, "");
110
+ }
111
+ export function isLoopbackAddress(value) {
112
+ const address = normalizeMachineAddress(value);
113
+ return address === "127.0.0.1" || address === "::1" || address === "localhost";
114
+ }
115
+ /** Tailscale's CGNAT range (100.64.0.0/10) and its IPv6 ULA prefix. */
116
+ export function isTailnetAddress(value) {
117
+ const address = normalizeMachineAddress(value);
118
+ if (address.startsWith("fd7a:115c:a1e0"))
119
+ return true;
120
+ const octets = address.split(".");
121
+ if (octets.length !== 4)
122
+ return false;
123
+ const first = Number.parseInt(octets[0] ?? "", 10);
124
+ const second = Number.parseInt(octets[1] ?? "", 10);
125
+ return first === 100 && second >= 64 && second <= 127;
126
+ }
127
+ /* ── Identity ── */
128
+ function pushUnique(target, value) {
129
+ if (value === null || value === undefined || value === "")
130
+ return;
131
+ if (!target.includes(value))
132
+ target.push(value);
133
+ }
134
+ const pushKey = (pushUnique);
135
+ /**
136
+ * The keys one piece of evidence claims. Two pieces of evidence describe the
137
+ * same machine when they share any key — see `groupMachineEvidence`.
138
+ */
139
+ export function machineIdentityKeys(evidence) {
140
+ const durable = [];
141
+ const transient = [];
142
+ switch (evidence.kind) {
143
+ case "scout": {
144
+ pushKey(durable, `node:${evidence.nodeId}`);
145
+ const host = normalizeMachineHostName(evidence.hostName)
146
+ || normalizeMachineHostName(evidence.nodeName);
147
+ if (host)
148
+ pushKey(durable, `host:${host}`);
149
+ return { durable, transient };
150
+ }
151
+ case "tailnet": {
152
+ pushKey(durable, `tailnet:${evidence.peerId}`);
153
+ const host = normalizeMachineHostName(evidence.hostName)
154
+ || normalizeMachineHostName(evidence.dnsName);
155
+ if (host)
156
+ pushKey(durable, `host:${host}`);
157
+ // A tailnet address is assigned by the coordination server and stays with
158
+ // the node, so unlike a LAN address it is safe to keep.
159
+ for (const address of evidence.addresses) {
160
+ const normalized = normalizeMachineAddress(address);
161
+ if (!normalized)
162
+ continue;
163
+ if (isTailnetAddress(normalized))
164
+ pushKey(durable, `ip:${normalized}`);
165
+ else
166
+ pushKey(transient, `ip:${normalized}`);
167
+ }
168
+ return { durable, transient };
169
+ }
170
+ case "lan": {
171
+ const mac = normalizeMacAddress(evidence.macAddress);
172
+ if (mac)
173
+ pushKey(durable, `mac:${mac}`);
174
+ const host = normalizeMachineHostName(evidence.hostName)
175
+ || normalizeMachineHostName(evidence.instanceName);
176
+ if (host)
177
+ pushKey(durable, `host:${host}`);
178
+ for (const address of evidence.addresses) {
179
+ const normalized = normalizeMachineAddress(address);
180
+ if (!normalized || isLoopbackAddress(normalized))
181
+ continue;
182
+ if (isTailnetAddress(normalized))
183
+ pushKey(durable, `ip:${normalized}`);
184
+ else
185
+ pushKey(transient, `ip:${normalized}`);
186
+ }
187
+ return { durable, transient };
188
+ }
189
+ case "host": {
190
+ if (evidence.nodeId)
191
+ pushKey(durable, `node:${evidence.nodeId}`);
192
+ const host = normalizeMachineHostName(evidence.hostName);
193
+ if (host)
194
+ pushKey(durable, `host:${host}`);
195
+ return { durable, transient };
196
+ }
197
+ default:
198
+ return EMPTY_KEYS;
199
+ }
200
+ }
201
+ /**
202
+ * Union-find over identity keys. Evidence lands in the same group when it
203
+ * shares any key, directly or through a chain — an ARP row and an mDNS advert
204
+ * share an IP; the advert and a tailnet peer share a hostname; so all three
205
+ * describe one machine even though ARP and Tailscale share nothing at all.
206
+ *
207
+ * `seeds` are the durable key sets of machines already on record. They enter
208
+ * the union as members, which is what lets a machine seen only by ARP today
209
+ * and only by Tailscale tomorrow stay one record: the seed bridges the passes.
210
+ */
211
+ export function groupMachineEvidence(evidence, seeds = []) {
212
+ const parent = [];
213
+ const find = (index) => {
214
+ let root = index;
215
+ while (parent[root] !== root)
216
+ root = parent[root];
217
+ let cursor = index;
218
+ while (parent[cursor] !== root) {
219
+ const next = parent[cursor];
220
+ parent[cursor] = root;
221
+ cursor = next;
222
+ }
223
+ return root;
224
+ };
225
+ const union = (a, b) => {
226
+ const rootA = find(a);
227
+ const rootB = find(b);
228
+ if (rootA !== rootB)
229
+ parent[rootB] = rootA;
230
+ };
231
+ // Members: seeds first (so a seed's slot index is its own), then evidence.
232
+ const memberKeys = [];
233
+ for (const seed of seeds) {
234
+ parent.push(parent.length);
235
+ memberKeys.push({ durable: [...seed], transient: [] });
236
+ }
237
+ const evidenceOffset = memberKeys.length;
238
+ for (const item of evidence) {
239
+ parent.push(parent.length);
240
+ memberKeys.push(machineIdentityKeys(item));
241
+ }
242
+ const owners = new Map();
243
+ memberKeys.forEach((keys, index) => {
244
+ for (const key of [...keys.durable, ...keys.transient]) {
245
+ const existing = owners.get(key);
246
+ if (existing === undefined)
247
+ owners.set(key, index);
248
+ else
249
+ union(existing, index);
250
+ }
251
+ });
252
+ const groups = new Map();
253
+ const groupOrder = [];
254
+ const ensureGroup = (root) => {
255
+ const existing = groups.get(root);
256
+ if (existing)
257
+ return existing;
258
+ const created = { identityKeys: [], evidence: [], seedIndices: [] };
259
+ groups.set(root, created);
260
+ groupOrder.push(root);
261
+ return created;
262
+ };
263
+ memberKeys.forEach((keys, index) => {
264
+ const group = ensureGroup(find(index));
265
+ for (const key of keys.durable)
266
+ pushKey(group.identityKeys, key);
267
+ if (index < evidenceOffset)
268
+ group.seedIndices.push(index);
269
+ else
270
+ group.evidence.push(evidence[index - evidenceOffset]);
271
+ });
272
+ return groupOrder
273
+ .map((root) => groups.get(root))
274
+ // A seed with no fresh evidence is a machine nobody saw this pass. It stays
275
+ // on record (the caller keeps it), but it is not a result of this scan.
276
+ .filter((group) => group.evidence.length > 0)
277
+ .map((group) => ({
278
+ ...group,
279
+ identityKeys: [...group.identityKeys].sort(),
280
+ // Ascending, so the lowest-indexed record is the stable survivor of a merge.
281
+ seedIndices: [...group.seedIndices].sort((a, b) => a - b),
282
+ }));
283
+ }
284
+ /**
285
+ * Deterministic id from a group's durable keys, so two brokers observing the
286
+ * same machine mint the same id and a restart never renumbers the fleet.
287
+ * FNV-1a ×2, matching `stableChannelId`.
288
+ */
289
+ export function stableMachineId(identityKeys) {
290
+ const normalized = [...identityKeys].map((key) => key.trim().toLowerCase()).sort().join("|");
291
+ const hash = (seed) => {
292
+ let value = seed;
293
+ for (let index = 0; index < normalized.length; index += 1) {
294
+ value ^= BigInt(normalized.charCodeAt(index));
295
+ value = BigInt.asUintN(64, value * 0x100000001b3n);
296
+ }
297
+ return value.toString(16).padStart(16, "0");
298
+ };
299
+ return `${MACHINE_ID_PREFIX}${hash(0xcbf29ce484222325n)}${hash(0x84222325cbf29ce4n)}`;
300
+ }
301
+ /* ── Derivations over a group ── */
302
+ function evidenceOf(evidence, kind) {
303
+ return evidence.filter((item) => item.kind === kind);
304
+ }
305
+ /**
306
+ * The name to show. Scout's own name for the node wins because the operator
307
+ * chose it; then the tailnet hostname, then whatever the LAN advertised. A
308
+ * machine known only by MAC gets its MAC, never a fabricated label.
309
+ */
310
+ export function deriveMachineName(evidence) {
311
+ for (const scout of evidenceOf(evidence, "scout")) {
312
+ // A node name is a host name — `Arts-Mac-mini-534.local`, suffix and all —
313
+ // so it goes through the same normalizer rather than straight to the UI.
314
+ const name = normalizeMachineHostName(scout.nodeName) || normalizeMachineHostName(scout.hostName);
315
+ if (name)
316
+ return name;
317
+ }
318
+ const tailnet = evidenceOf(evidence, "tailnet")
319
+ .sort((a, b) => Number(b.online) - Number(a.online))[0];
320
+ const tailnetName = normalizeMachineHostName(tailnet?.hostName)
321
+ || normalizeMachineHostName(tailnet?.dnsName);
322
+ if (tailnetName)
323
+ return tailnetName;
324
+ for (const lan of evidenceOf(evidence, "lan")) {
325
+ const name = normalizeMachineHostName(lan.hostName) || normalizeMachineHostName(lan.instanceName);
326
+ if (name)
327
+ return name;
328
+ }
329
+ // A terminal host names the machine it was probed on. Weaker than a node or
330
+ // a tailnet peer, but far better than falling through to a bare MAC for a
331
+ // box we are literally running sessions on.
332
+ for (const host of evidenceOf(evidence, "host")) {
333
+ const name = normalizeMachineHostName(host.hostName);
334
+ if (name)
335
+ return name;
336
+ }
337
+ const mac = evidenceOf(evidence, "lan")
338
+ .map((lan) => normalizeMacAddress(lan.macAddress))
339
+ .find(Boolean);
340
+ if (mac)
341
+ return mac;
342
+ const address = evidenceOf(evidence, "lan")
343
+ .flatMap((lan) => lan.addresses.map(normalizeMachineAddress))
344
+ .find((value) => value && !isLoopbackAddress(value));
345
+ return address || "unknown";
346
+ }
347
+ const TAILSCALE_OS_PLATFORMS = {
348
+ macos: "macos",
349
+ macOS: "macos",
350
+ darwin: "macos",
351
+ linux: "linux",
352
+ windows: "windows",
353
+ ios: "ios",
354
+ iOS: "ios",
355
+ android: "android",
356
+ };
357
+ export function deriveMachinePlatform(evidence) {
358
+ for (const tailnet of evidenceOf(evidence, "tailnet")) {
359
+ const os = (tailnet.os ?? "").trim();
360
+ const platform = TAILSCALE_OS_PLATFORMS[os] ?? TAILSCALE_OS_PLATFORMS[os.toLowerCase()];
361
+ if (platform)
362
+ return platform;
363
+ }
364
+ // Only Apple platforms publish `_rfb._tcp` alongside `_ssh._tcp` by default
365
+ // on a stock install, but that is a guess, not proof — keep it to the
366
+ // Bonjour `model` TXT key, which Apple devices set and others do not.
367
+ for (const lan of evidenceOf(evidence, "lan")) {
368
+ const model = String(lan.txt?.model ?? "").trim();
369
+ if (/^(mac|imac|macbook|macmini|macpro|macstudio)/i.test(model))
370
+ return "macos";
371
+ }
372
+ return "unknown";
373
+ }
374
+ export function deriveMachineCapabilities(evidence) {
375
+ const capabilities = [];
376
+ for (const scout of evidenceOf(evidence, "scout")) {
377
+ if (scout.brokerUrl)
378
+ pushUnique(capabilities, "scout-broker");
379
+ if (scout.webUrl)
380
+ pushUnique(capabilities, "scout-web");
381
+ }
382
+ for (const lan of evidenceOf(evidence, "lan")) {
383
+ pushUnique(capabilities, lan.serviceType ? MACHINE_SERVICE_CAPABILITIES[lan.serviceType] : null);
384
+ }
385
+ for (const host of evidenceOf(evidence, "host")) {
386
+ pushUnique(capabilities, host.host);
387
+ }
388
+ return capabilities.sort();
389
+ }
390
+ /**
391
+ * Routes, best first: loopback (it's this machine), then LAN (no hop), then
392
+ * tailnet, then whatever the mesh advertised. This is the order a caller
393
+ * should dial, and it mirrors the LAN-first preference iOS already uses.
394
+ */
395
+ /**
396
+ * Every way we know to reach this machine, best first. `isSelf` decides whether
397
+ * loopback belongs at all: a peer advertising `http://127.0.0.1:43120` is
398
+ * describing its own loopback, which is not a route to it from here.
399
+ */
400
+ export function deriveMachineRoutes(evidence, isSelf = false) {
401
+ const routes = [];
402
+ const seen = new Set();
403
+ const add = (route) => {
404
+ const key = `${route.kind}|${route.host}|${route.port ?? ""}`;
405
+ if (seen.has(key))
406
+ return;
407
+ seen.add(key);
408
+ routes.push(route);
409
+ };
410
+ for (const scout of evidenceOf(evidence, "scout")) {
411
+ for (const url of [scout.brokerUrl, scout.webUrl]) {
412
+ if (!url)
413
+ continue;
414
+ try {
415
+ const parsed = new URL(url);
416
+ const host = normalizeMachineAddress(parsed.hostname);
417
+ const port = parsed.port ? Number.parseInt(parsed.port, 10) : undefined;
418
+ const kind = isLoopbackAddress(host)
419
+ ? "loopback"
420
+ : isTailnetAddress(host)
421
+ ? "tailnet"
422
+ : "mesh";
423
+ add({ kind, host, ...(port ? { port } : {}), url, lastSeenAt: scout.observedAt });
424
+ }
425
+ catch {
426
+ // A malformed advertised URL is the peer's problem, not a reason to
427
+ // drop every other route this machine has.
428
+ }
429
+ }
430
+ }
431
+ for (const lan of evidenceOf(evidence, "lan")) {
432
+ for (const address of lan.addresses) {
433
+ const host = normalizeMachineAddress(address);
434
+ if (!host || isLoopbackAddress(host))
435
+ continue;
436
+ add({
437
+ kind: isTailnetAddress(host) ? "tailnet" : "lan",
438
+ host,
439
+ ...(lan.port ? { port: lan.port } : {}),
440
+ lastSeenAt: lan.observedAt,
441
+ });
442
+ }
443
+ }
444
+ for (const tailnet of evidenceOf(evidence, "tailnet")) {
445
+ for (const address of tailnet.addresses) {
446
+ const host = normalizeMachineAddress(address);
447
+ if (!host)
448
+ continue;
449
+ add({ kind: "tailnet", host, lastSeenAt: tailnet.observedAt });
450
+ }
451
+ const dnsName = (tailnet.dnsName ?? "").trim().replace(/\.+$/, "");
452
+ if (dnsName)
453
+ add({ kind: "tailnet", host: dnsName, lastSeenAt: tailnet.observedAt });
454
+ }
455
+ const order = {
456
+ lan: 0,
457
+ tailnet: 1,
458
+ mesh: 2,
459
+ relay: 3,
460
+ // Last, not first: `127.0.0.1` only reaches the machine you are already on.
461
+ // A dozen peers each advertising `http://127.0.0.1:43120` made loopback the
462
+ // headline address for machines it cannot reach at all.
463
+ loopback: 4,
464
+ };
465
+ return routes
466
+ .filter((route) => isSelf || route.kind !== "loopback")
467
+ .sort((a, b) => order[a.kind] - order[b.kind]);
468
+ }
469
+ /** Anything seen inside this window counts as present. */
470
+ export const MACHINE_ONLINE_WINDOW_MS = 2 * 60_000;
471
+ export const MACHINE_RECENT_WINDOW_MS = 60 * 60_000;
472
+ export function machineLabel(machine) {
473
+ return machine.displayName?.trim() || machine.name;
474
+ }
475
+ export function machinePresence(machine, now = Date.now()) {
476
+ const age = now - machine.lastSeenAt;
477
+ if (age <= MACHINE_ONLINE_WINDOW_MS)
478
+ return "online";
479
+ if (age <= MACHINE_RECENT_WINDOW_MS)
480
+ return "recent";
481
+ return "offline";
482
+ }
483
+ /**
484
+ * Build (or rebuild) a record from a group. `previous` carries the parts an
485
+ * operator owns — id, chosen name, notes, pin, first-seen — which no amount of
486
+ * fresh evidence may overwrite.
487
+ */
488
+ export function buildMachineRecord(group, previous) {
489
+ const evidence = [...group.evidence].sort((a, b) => a.observedAt - b.observedAt);
490
+ const observedAt = evidence.reduce((latest, item) => Math.max(latest, item.observedAt), 0);
491
+ const scout = evidenceOf(evidence, "scout")[0];
492
+ const tailnet = evidenceOf(evidence, "tailnet")
493
+ .sort((a, b) => Number(b.online) - Number(a.online))[0];
494
+ const hostNames = [];
495
+ const addresses = [];
496
+ const macAddresses = [];
497
+ for (const key of group.identityKeys) {
498
+ const [prefix, ...rest] = key.split(":");
499
+ const value = rest.join(":");
500
+ if (prefix === "host")
501
+ pushKey(hostNames, value);
502
+ else if (prefix === "ip")
503
+ pushKey(addresses, value);
504
+ else if (prefix === "mac")
505
+ pushKey(macAddresses, value);
506
+ }
507
+ for (const item of evidence) {
508
+ if (item.kind === "lan" || item.kind === "tailnet") {
509
+ for (const address of item.addresses) {
510
+ const normalized = normalizeMachineAddress(address);
511
+ if (normalized && !isLoopbackAddress(normalized))
512
+ pushKey(addresses, normalized);
513
+ }
514
+ }
515
+ }
516
+ const isSelf = evidence.some((item) => ((item.kind === "scout" || item.kind === "tailnet") && item.isSelf === true));
517
+ return {
518
+ id: previous?.id ?? stableMachineId(group.identityKeys),
519
+ displayName: previous?.displayName ?? null,
520
+ name: deriveMachineName(evidence),
521
+ platform: deriveMachinePlatform(evidence),
522
+ identityKeys: group.identityKeys,
523
+ isSelf,
524
+ ...(scout?.nodeId ? { scoutNodeId: scout.nodeId } : {}),
525
+ ...(scout?.meshId ? { meshId: scout.meshId } : {}),
526
+ ...(tailnet?.peerId ? { tailnetId: tailnet.peerId } : {}),
527
+ ...(tailnet?.tailnetName ?? scout?.tailnetName
528
+ ? { tailnetName: tailnet?.tailnetName ?? scout?.tailnetName }
529
+ : {}),
530
+ hostNames,
531
+ addresses,
532
+ macAddresses,
533
+ capabilities: deriveMachineCapabilities(evidence),
534
+ routes: deriveMachineRoutes(evidence, isSelf),
535
+ evidence,
536
+ pinned: previous?.pinned ?? false,
537
+ ...(previous?.notes ? { notes: previous.notes } : {}),
538
+ firstSeenAt: previous?.firstSeenAt ?? observedAt,
539
+ lastSeenAt: Math.max(observedAt, previous?.lastSeenAt ?? 0),
540
+ ...(previous?.metadata ? { metadata: previous.metadata } : {}),
541
+ };
542
+ }
543
+ /** Sort for display: this machine, then present ones, then by name. */
544
+ export function compareMachines(a, b) {
545
+ if (a.isSelf !== b.isSelf)
546
+ return a.isSelf ? -1 : 1;
547
+ if (a.lastSeenAt !== b.lastSeenAt)
548
+ return b.lastSeenAt - a.lastSeenAt;
549
+ return machineLabel(a).localeCompare(machineLabel(b));
550
+ }
package/dist/mesh.d.ts CHANGED
@@ -72,6 +72,14 @@ export interface NodeDefinition {
72
72
  advertiseScope: AdvertiseScope;
73
73
  brokerUrl?: string;
74
74
  webUrl?: string;
75
+ /**
76
+ * The node's named web doorway (e.g. `m1.scout.local`), matching the edge's
77
+ * advertisedHost. `*.scout.local` names resolve to 127.0.0.1 on every LAN
78
+ * machine; the local edge recognizes them as peer doorways and proxies to
79
+ * this node's reachable route, so the name is a stable way to open a peer's
80
+ * Scout regardless of its current address.
81
+ */
82
+ webHost?: string;
75
83
  meshEntrypoints?: NodeMeshEntrypoint[];
76
84
  tailnetName?: string;
77
85
  capabilities?: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"mesh.d.ts","sourceRoot":"","sources":["../src/mesh.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mesh.d.ts","sourceRoot":"","sources":["../src/mesh.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /** Harness ids Scout can select for a new execution session. */
2
- export declare const SCOUT_LAUNCHABLE_HARNESSES: readonly ["claude", "codex", "grok", "grok-acp", "kimi", "flue", "cursor", "opencode", "pi"];
2
+ export declare const SCOUT_LAUNCHABLE_HARNESSES: readonly ["claude", "codex", "grok", "grok-acp", "kimi", "flue", "cursor", "opencode", "pi", "devin"];
3
3
  export type ScoutLaunchableHarness = typeof SCOUT_LAUNCHABLE_HARNESSES[number];
4
4
  export declare const SCOUT_REASONING_EFFORTS: readonly ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
5
5
  export type ScoutReasoningEffort = typeof SCOUT_REASONING_EFFORTS[number];
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-catalog-contract.d.ts","sourceRoot":"","sources":["../src/runtime-catalog-contract.ts"],"names":[],"mappings":"AAEA,gEAAgE;AAChE,eAAO,MAAM,0BAA0B,8FAEK,CAAC;AAE7C,MAAM,MAAM,sBAAsB,GAAG,OAAO,0BAA0B,CAAC,MAAM,CAAC,CAAC;AAE/E,eAAO,MAAM,uBAAuB,gFAE1B,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAAG,OAAO,uBAAuB,CAAC,MAAM,CAAC,CAAC;AAE1E,eAAO,MAAM,6BAA6B,EAAE,QAAQ,CAAC,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAC,CASxF,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,SAAS,oBAAoB,EAAE,GAAG,IAAI,CAAC;IAC1D,sBAAsB,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,sBAAsB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,SAAS,oBAAoB,EAAE,GAAG,IAAI,CAAC;IACzD,sBAAsB,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACrD,MAAM,EAAE,SAAS,sBAAsB,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,8BAA8B,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,SAAS,wBAAwB,EAAE,CAAC;CAChD"}
1
+ {"version":3,"file":"runtime-catalog-contract.d.ts","sourceRoot":"","sources":["../src/runtime-catalog-contract.ts"],"names":[],"mappings":"AAEA,gEAAgE;AAChE,eAAO,MAAM,0BAA0B,uGAEK,CAAC;AAE7C,MAAM,MAAM,sBAAsB,GAAG,OAAO,0BAA0B,CAAC,MAAM,CAAC,CAAC;AAE/E,eAAO,MAAM,uBAAuB,gFAE1B,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAAG,OAAO,uBAAuB,CAAC,MAAM,CAAC,CAAC;AAE1E,eAAO,MAAM,6BAA6B,EAAE,QAAQ,CAAC,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAC,CASxF,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,SAAS,oBAAoB,EAAE,GAAG,IAAI,CAAC;IAC1D,sBAAsB,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,sBAAsB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,SAAS,oBAAoB,EAAE,GAAG,IAAI,CAAC;IACzD,sBAAsB,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACrD,MAAM,EAAE,SAAS,sBAAsB,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,8BAA8B,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,SAAS,wBAAwB,EAAE,CAAC;CAChD"}
@@ -1,6 +1,6 @@
1
1
  /** Harness ids Scout can select for a new execution session. */
2
2
  export const SCOUT_LAUNCHABLE_HARNESSES = [
3
- "claude", "codex", "grok", "grok-acp", "kimi", "flue", "cursor", "opencode", "pi",
3
+ "claude", "codex", "grok", "grok-acp", "kimi", "flue", "cursor", "opencode", "pi", "devin",
4
4
  ];
5
5
  export const SCOUT_REASONING_EFFORTS = [
6
6
  "none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra",