infinicode 2.7.1 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -311,10 +311,7 @@ program
311
311
  .option('--lan-port <port>', 'UDP discovery port for --lan (default 47915)')
312
312
  .option('--tag <tag>', 'only discover peers carrying this tag (Tailscale ACL tag or LAN beacon tag)')
313
313
  .option('--console', 'interactive slash-command console on the node (/nodes, /build, /activity …); on by default in a TTY')
314
- .option('--dashboard', 'serve the RoboPark control web UI at http://<host>:<port>/ (fleet map, live activity, actions)')
315
- .option('--scheduler-url <url>', 'link a RoboPark scheduler so the dashboard shows sessions + telemetry (e.g. http://localhost:8080)')
316
- .option('--scheduler-token <token>', 'bearer token to present to the scheduler on proxied requests')
317
- .option('--tls-cert <path>', 'PEM cert file — serve the dashboard/mesh over HTTPS')
314
+ .option('--tls-cert <path>', 'PEM cert file serve the mesh over HTTPS')
318
315
  .option('--tls-key <path>', 'PEM private key file for --tls-cert')
319
316
  .option('--gateway <url>', 'link to an InfiniBot gateway (ws://host:18789) — appear on its neural-mesh map')
320
317
  .option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
@@ -3,4 +3,6 @@ import type Conf from 'conf';
3
3
  import type { InfinicodeConfig } from '../kernel/config-schema.js';
4
4
  /** Attach the robopark verbs to a Commander command (the `robopark` program in
5
5
  * the standalone bin, or the `infinicode robopark` group). */
6
- export declare function attachRoboparkSubcommands(cmd: Command, config: Conf<InfinicodeConfig>): void;
6
+ export declare function attachRoboparkSubcommands(cmd: Command, config: Conf<InfinicodeConfig>, opts?: {
7
+ skipSetup?: boolean;
8
+ }): void;
@@ -48,7 +48,7 @@ function unreachable(url, err) {
48
48
  return [
49
49
  chalk.red(` ✗ no mesh node reachable at ${url}`),
50
50
  chalk.dim(` ${msg}`),
51
- chalk.dim(' start one with infinicode serve --dashboard or robopark setup --start'),
51
+ chalk.dim(' start one with infinicode serve --peer --token <token> or robopark setup --control --start'),
52
52
  ].join('\n');
53
53
  }
54
54
  // ── loadbar / formatting helpers ───────────────────────────────────────────────
@@ -111,10 +111,10 @@ function renderFleet(snap) {
111
111
  return lines.join('\n');
112
112
  }
113
113
  const ROLE_PRESETS = {
114
- scheduler: { mesh: 'hub', blurb: 'on-site hub (LiveKit Server 1) — bridges tailnet ↔ LAN, serves the dashboard', dashboard: true, lan: true, tailscale: true },
115
- robot: { mesh: 'satellite', blurb: 'a robot Pi — accept-only, LAN-discovered, never dials out', dashboard: false, lan: true, tailscale: false },
116
- laptop: { mesh: 'peer', blurb: 'your control station — drives the fleet over the mesh', dashboard: false, lan: false, tailscale: true },
117
- relay: { mesh: 'relay', blurb: 'a relay hop between two networks', dashboard: false, lan: true, tailscale: true },
114
+ scheduler: { mesh: 'hub', blurb: 'on-site hub (LiveKit Server 1) — bridges tailnet ↔ LAN, runs robopark serve', lan: true, tailscale: true },
115
+ robot: { mesh: 'satellite', blurb: 'a robot Pi — accept-only, LAN-discovered, never dials out', lan: true, tailscale: false },
116
+ laptop: { mesh: 'peer', blurb: 'your control station — drives the fleet over the mesh', lan: false, tailscale: true },
117
+ relay: { mesh: 'relay', blurb: 'a relay hop between two networks', lan: true, tailscale: true },
118
118
  };
119
119
  // Power users can pass raw mesh roles too; fold them onto the matching preset.
120
120
  const MESH_ALIASES = { hub: 'scheduler', satellite: 'robot', peer: 'laptop' };
@@ -192,11 +192,6 @@ async function roboparkSetup(config, opts) {
192
192
  const { label, preset } = resolved;
193
193
  const meshRole = preset.mesh;
194
194
  // Preset defaults, overridable by explicit flags.
195
- const dashboard = opts.dashboard ?? preset.dashboard;
196
- // The RoboPark scheduler API runs on the same box as the on-site hub, so the
197
- // scheduler role links it by default → the dashboard's Sessions & Telemetry
198
- // tab is live out of the box. Other roles don't proxy a scheduler.
199
- const schedulerUrl = opts.schedulerUrl ?? (label === 'scheduler' ? 'http://127.0.0.1:8080' : undefined);
200
195
  const lan = opts.lan ?? preset.lan;
201
196
  const tailscale = opts.tailscale ?? (preset.tailscale && !opts.hub); // seed URL replaces discovery
202
197
  const port = opts.port ? parseInt(opts.port, 10) : DEFAULT_PORT;
@@ -232,8 +227,6 @@ async function roboparkSetup(config, opts) {
232
227
  console.log(` lan: ${chalk.green('auto-discovery on')}`);
233
228
  if (tailscale)
234
229
  console.log(` tailnet:${chalk.green(' discovery on')}${opts.tag ? chalk.dim(' · tag ' + opts.tag) : ''}`);
235
- if (dashboard)
236
- console.log(` ui: ${chalk.green('dashboard on')}`);
237
230
  console.log();
238
231
  const serveOpts = {
239
232
  role: meshRole,
@@ -244,33 +237,29 @@ async function roboparkSetup(config, opts) {
244
237
  tailscale,
245
238
  lan,
246
239
  tag: opts.tag,
247
- dashboard,
248
- schedulerUrl,
249
- schedulerToken: opts.schedulerToken,
250
240
  gateway: opts.gateway,
251
241
  gatewayToken: opts.gatewayToken,
252
242
  };
253
- if (opts.start) {
254
- console.log(chalk.dim(' starting node…\n'));
255
- await serve(config, serveOpts);
256
- return;
257
- }
258
- const startCmd = [
243
+ const infinicodeCmd = [
259
244
  'infinicode serve',
260
245
  `--role ${meshRole}`,
261
- dashboard ? '--dashboard' : '',
262
- schedulerUrl ? `--scheduler-url ${schedulerUrl}` : '',
263
246
  lan ? '--lan' : '',
264
247
  tailscale ? '--tailscale' : '',
265
248
  opts.tag ? `--tag ${opts.tag}` : '',
266
249
  opts.hub ? `--seed ${opts.hub}` : '',
267
250
  fed.token ? `--token ${fed.token}` : '',
268
251
  opts.gateway ? `--gateway ${opts.gateway}` : '',
269
- ]
270
- .filter(Boolean)
271
- .join(' ');
252
+ ].filter(Boolean).join(' ');
253
+ if (opts.start) {
254
+ console.log(chalk.dim(' starting node…\n'));
255
+ await serve(config, serveOpts);
256
+ return;
257
+ }
272
258
  console.log(chalk.dim(' saved. start it now with: ') + chalk.cyan('robopark setup --' + label + ' --start'));
273
- console.log(chalk.dim(' or run the node directly: ') + chalk.cyan(startCmd));
259
+ console.log(chalk.dim(' or run the node directly: ') + chalk.cyan(infinicodeCmd));
260
+ if (label === 'scheduler') {
261
+ console.log(chalk.dim(' and for the RoboPark UI: ') + chalk.cyan('robopark serve --port 8080'));
262
+ }
274
263
  console.log();
275
264
  }
276
265
  async function roboparkFleet(config, opts) {
@@ -367,17 +356,13 @@ function openBrowser(url) {
367
356
  }
368
357
  }
369
358
  async function roboparkDashboard(config, opts) {
370
- const base = nodeUrl(config, opts.url);
359
+ // The RoboPark dashboard is now served by `robopark serve`, not the mesh node.
360
+ // Default to the scheduler URL if none is given, falling back to the local node.
361
+ const base = (opts.url ?? 'http://127.0.0.1:8080').replace(/\/+$/, '');
371
362
  const token = authToken(config, opts.token);
372
363
  const link = `${base}/${token ? `?token=${encodeURIComponent(token)}` : ''}`;
373
- // Confirm the node is actually serving the dashboard before pointing there.
374
364
  try {
375
- const res = await fetch(link, { headers: headers(token), signal: AbortSignal.timeout(5000) });
376
- if (res.status === 404) {
377
- console.log(chalk.yellow(` ⚠ node at ${base} is up but the dashboard is off.`));
378
- console.log(chalk.dim(' restart it with infinicode serve --dashboard'));
379
- return;
380
- }
365
+ const res = await fetch(`${base}/api/settings`, { headers: headers(token), signal: AbortSignal.timeout(5000) });
381
366
  if (!res.ok)
382
367
  throw new Error(`HTTP ${res.status}`);
383
368
  }
@@ -393,33 +378,32 @@ async function roboparkDashboard(config, opts) {
393
378
  // ── wiring ─────────────────────────────────────────────────────────────────────
394
379
  /** Attach the robopark verbs to a Commander command (the `robopark` program in
395
380
  * the standalone bin, or the `infinicode robopark` group). */
396
- export function attachRoboparkSubcommands(cmd, config) {
397
- cmd
398
- .command('setup')
399
- .description('Set up this device in one pass. Run bare for a guided wizard, or pass a role to skip it.')
400
- .option('--scheduler', 'this device is the on-site hub (LiveKit Server 1)')
401
- .option('--robot', 'this device is a talking-robot Raspberry Pi')
402
- .option('--laptop', 'this device is your control station')
403
- .option('--role <role>', 'explicit role: scheduler | robot | laptop | relay (or raw mesh role hub/peer/satellite)')
404
- .option('--site <site>', 'physical location for fleet grouping, e.g. "Tel Aviv"')
405
- .option('--name <name>', 'friendly device name (defaults to hostname)')
406
- .option('--hub <url>', 'hub base URL to join, e.g. http://100.x.y.z:47913 (robots/laptops)')
407
- .option('--gateway <url>', 'InfiniBot gateway ws:// URL to surface the fleet in InfiniBot (scheduler)')
408
- .option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
409
- .option('--token <token>', 'shared bearer token every device must present')
410
- .option('--port <port>', 'mesh listen port (default 47913)')
411
- .option('--tag <tag>', 'only discover peers carrying this tag (e.g. tag:robopark)')
412
- .option('--architecture <text>', "one-line brief of this device's job in the wider system")
413
- .option('--lan', 'force zero-config LAN auto-discovery (on by default for scheduler/robot)')
414
- .option('--tailscale', 'force Tailscale peer discovery (on by default for scheduler/laptop)')
415
- .option('--dashboard', 'force the web control UI (on by default for scheduler)')
416
- .option('--scheduler-url <url>', 'RoboPark scheduler API to link for the dashboard Sessions tab (default http://127.0.0.1:8080 for the scheduler role)')
417
- .option('--scheduler-token <token>', 'bearer token presented to the scheduler on proxied requests')
418
- .option('--start', 'launch the node now instead of just writing config')
419
- .option('--yes', 'skip the guided wizard even with no role (non-interactive)')
420
- .action(async (opts) => {
421
- await roboparkSetup(config, opts);
422
- });
381
+ export function attachRoboparkSubcommands(cmd, config, opts) {
382
+ if (!opts?.skipSetup) {
383
+ cmd
384
+ .command('setup')
385
+ .description('Set up this device in one pass. Run bare for a guided wizard, or pass a role to skip it.')
386
+ .option('--scheduler', 'this device is the on-site hub (LiveKit Server 1)')
387
+ .option('--robot', 'this device is a talking-robot Raspberry Pi')
388
+ .option('--laptop', 'this device is your control station')
389
+ .option('--role <role>', 'explicit role: scheduler | robot | laptop | relay (or raw mesh role hub/peer/satellite)')
390
+ .option('--site <site>', 'physical location for fleet grouping, e.g. "Tel Aviv"')
391
+ .option('--name <name>', 'friendly device name (defaults to hostname)')
392
+ .option('--hub <url>', 'hub base URL to join, e.g. http://100.x.y.z:47913 (robots/laptops)')
393
+ .option('--gateway <url>', 'InfiniBot gateway ws:// URL to surface the fleet in InfiniBot (scheduler)')
394
+ .option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
395
+ .option('--token <token>', 'shared bearer token every device must present')
396
+ .option('--port <port>', 'mesh listen port (default 47913)')
397
+ .option('--tag <tag>', 'only discover peers carrying this tag (e.g. tag:robopark)')
398
+ .option('--architecture <text>', "one-line brief of this device's job in the wider system")
399
+ .option('--lan', 'force zero-config LAN auto-discovery (on by default for scheduler/robot)')
400
+ .option('--tailscale', 'force Tailscale peer discovery (on by default for scheduler/laptop)')
401
+ .option('--start', 'launch the node now instead of just writing config')
402
+ .option('--yes', 'skip the guided wizard even with no role (non-interactive)')
403
+ .action(async (opts) => {
404
+ await roboparkSetup(config, opts);
405
+ });
406
+ }
423
407
  cmd
424
408
  .command('fleet')
425
409
  .alias('status')
@@ -17,9 +17,6 @@ export interface ServeOptions {
17
17
  gateway?: string;
18
18
  gatewayToken?: string;
19
19
  gatewayPassword?: string;
20
- dashboard?: boolean;
21
- schedulerUrl?: string;
22
- schedulerToken?: string;
23
20
  tlsCert?: string;
24
21
  tlsKey?: string;
25
22
  supervised?: boolean;
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import chalk from 'chalk';
13
13
  import { readFileSync } from 'node:fs';
14
- import { randomBytes } from 'node:crypto';
14
+ import crypto from 'node:crypto';
15
15
  import { fileURLToPath } from 'node:url';
16
16
  import { dirname, join } from 'node:path';
17
17
  import { buildKernelFromConfig, registerCloudProviders } from '../kernel/setup.js';
@@ -60,18 +60,20 @@ export async function serve(config, opts) {
60
60
  // performSelfUpdate via the environment.
61
61
  if (opts.supervised ?? fed.supervised)
62
62
  process.env.INFINICODE_SUPERVISED = '1';
63
- // Secure-by-default: a control dashboard exposed on a routable interface with
64
- // no token is a wide-open remote. If the operator asked for the dashboard but
65
- // set no token, mint a session token so the surface isn't unauthenticated.
66
- if (opts.dashboard && !token && !isLoopback(bindHost)) {
67
- token = randomBytes(9).toString('base64url');
68
- console.log(chalk.yellow(` ⚠ dashboard exposed on ${bindHost} without --token — generated a session token.`));
63
+ // Mesh nodes should always present a token on a routable interface. If none
64
+ // is provided, generate one so remote peers can authenticate.
65
+ if (!token && !isLoopback(bindHost)) {
66
+ token = crypto.randomBytes(9).toString('base64url');
67
+ console.log(chalk.yellow(` ⚠ mesh exposed on ${bindHost} without --token generated a session token.`));
69
68
  console.log(chalk.yellow(` peers must now connect with --token ${token}`));
70
69
  }
71
70
  // Keep a network-exposed, tokenless node read-only: it will answer status/map
72
71
  // queries but refuse control commands until a token is set. Loopback-only or
73
72
  // token-protected nodes are trusted and run everything.
74
73
  const trusted = Boolean(token) || isLoopback(bindHost);
74
+ // commandGate is the mesh-side gate only. The RoboPark product UI is now
75
+ // served exclusively by `robopark serve`, so control writes there are no
76
+ // longer in scope for `infinicode serve`.
75
77
  const commandGate = (input) => {
76
78
  if (trusted)
77
79
  return { ok: true };
@@ -158,13 +160,9 @@ export async function serve(config, opts) {
158
160
  autoUpdate: opts.autoUpdate ?? fed.autoUpdate,
159
161
  applyConfig,
160
162
  autoPullConfig: role === 'satellite',
161
- dashboard: opts.dashboard,
162
163
  onApply,
163
164
  commandGate,
164
165
  tls,
165
- schedulerUrl: opts.schedulerUrl ?? fed.schedulerUrl,
166
- schedulerToken: opts.schedulerToken,
167
- schedulerReadOnly: !trusted,
168
166
  },
169
167
  });
170
168
  // Let the kernel's slash-command surface reach the mesh (/nodes, /node, …).
@@ -184,18 +182,10 @@ export async function serve(config, opts) {
184
182
  console.log(` auth: ${chalk.green('token required')}`);
185
183
  console.log();
186
184
  await federation.start();
187
- // Control dashboard link prefer a reachable host over the 0.0.0.0 wildcard.
188
- if (opts.dashboard) {
189
- const uiHost = isLoopback(bindHost) || bindHost === '0.0.0.0' ? 'localhost' : bindHost;
190
- const scheme = tls ? 'https' : 'http';
191
- const url = `${scheme}://${uiHost}:${port}/${token ? `?token=${token}` : ''}`;
192
- console.log(chalk.bold(` dashboard: ${chalk.cyan(url)}`));
193
- if (opts.schedulerUrl)
194
- console.log(chalk.dim(` scheduler linked: ${opts.schedulerUrl} (sessions + telemetry tabs live)`));
195
- if (!token)
196
- console.log(chalk.dim(' (no token — anyone who can reach this port can control the fleet; add --token to lock it)'));
197
- console.log();
198
- }
185
+ // Mesh-only node. The RoboPark dashboard + scheduler UI are served by
186
+ // `robopark serve` on the hub box, not by `infinicode serve`.
187
+ console.log(chalk.dim(' (use `robopark serve` on the hub for the fleet dashboard + scheduler UI)'));
188
+ console.log();
199
189
  // Connect to seeds. Satellites auto-source shared config from any hub/relay
200
190
  // they connect to (handled inside federation.connect()).
201
191
  for (const url of seeds) {
@@ -63,28 +63,19 @@ export interface FederationOptions {
63
63
  /** Coder profiles + rotation config (from node config `coder`). Merged over
64
64
  * built-in defaults, so the mesh rotates across backends out of the box. */
65
65
  coder?: Parameters<typeof loadCoderConfig>[0];
66
- /** Serve the web control dashboard at GET / and /ui (off unless enabled). */
67
- dashboard?: boolean;
68
- /** Publish this node's shared config to the fleet (backs POST /fed/apply +
69
- * the dashboard's "Apply config to fleet" button). */
66
+ /** Publish this node's shared config to the fleet (backs POST /fed/apply). */
70
67
  onApply?: () => Promise<unknown> | unknown;
71
- /** Gate remote/dashboard slash-commands. Returns {ok:false,reason} to reject —
72
- * used to keep an unauthenticated, network-exposed node read-only. */
68
+ /** Gate remote slash-commands. Returns {ok:false,reason} to reject — used to
69
+ * keep an unauthenticated, network-exposed node read-only. */
73
70
  commandGate?: (input: string) => {
74
71
  ok: boolean;
75
72
  reason?: string;
76
73
  };
77
- /** TLS material — when set, the node + dashboard are served over HTTPS. */
74
+ /** TLS material — when set, the mesh is served over HTTPS. */
78
75
  tls?: {
79
76
  cert: Buffer;
80
77
  key: Buffer;
81
78
  };
82
- /** RoboPark scheduler base URL — proxied under /robopark/* for the dashboard. */
83
- schedulerUrl?: string;
84
- /** Bearer token presented to the scheduler on proxied requests. */
85
- schedulerToken?: string;
86
- /** Refuse write actions through the scheduler proxy (read-only console). */
87
- schedulerReadOnly?: boolean;
88
79
  }
89
80
  /** Consolidated fleet view served at GET /fed/status. */
90
81
  export interface FleetStatus {
@@ -98,11 +89,9 @@ export interface FleetStatus {
98
89
  peerId: string;
99
90
  f: StreamFrame;
100
91
  }>;
101
- /** Console capabilities for the dashboard: whether control writes are allowed
102
- * on this node and whether a RoboPark scheduler is linked. */
92
+ /** Console capabilities: whether control writes are allowed on this node. */
103
93
  control: {
104
94
  readonly: boolean;
105
- schedulerLinked: boolean;
106
95
  };
107
96
  ts: number;
108
97
  }
@@ -1,5 +1,4 @@
1
1
  import { MeshServer, MeshClient } from './transport-http.js';
2
- import { DASHBOARD_HTML } from './dashboard-html.js';
3
2
  import { PeerMesh } from './peer-mesh.js';
4
3
  import { EventBridge } from './event-bridge.js';
5
4
  import { ConfigSync } from './config-sync.js';
@@ -89,11 +88,7 @@ export class Federation {
89
88
  runCommand: (input) => this.deps.kernel.command(input),
90
89
  commandGate: options.commandGate,
91
90
  onApply: options.onApply,
92
- dashboardHtml: options.dashboard ? DASHBOARD_HTML : undefined,
93
91
  tls: options.tls,
94
- schedulerUrl: options.schedulerUrl,
95
- schedulerToken: options.schedulerToken,
96
- schedulerReadOnly: options.schedulerReadOnly,
97
92
  onRpc: (env, remote) => this.onRpc(env, remote),
98
93
  });
99
94
  await this.server.start();
@@ -694,8 +689,7 @@ export class Federation {
694
689
  agents: this.localAgents(),
695
690
  activity: this.recentFrames(60),
696
691
  control: {
697
- readonly: Boolean(this.deps.options.schedulerReadOnly),
698
- schedulerLinked: Boolean(this.deps.options.schedulerUrl),
692
+ readonly: Boolean(this.deps.options.commandGate && !this.deps.options.commandGate('/status').ok),
699
693
  },
700
694
  ts: Date.now(),
701
695
  };
@@ -23,23 +23,11 @@ export interface MeshServerOptions {
23
23
  };
24
24
  /** Optional: publish this node's shared config to the fleet (POST /fed/apply). */
25
25
  onApply?: () => Promise<unknown> | unknown;
26
- /** Optional: static HTML for the control dashboard, served at GET / and /ui. */
27
- dashboardHtml?: string;
28
26
  /** Optional TLS — when set, the node serves HTTPS/WSS instead of plain HTTP. */
29
27
  tls?: {
30
28
  cert: Buffer;
31
29
  key: Buffer;
32
30
  };
33
- /** Optional: base URL of the RoboPark scheduler. When set, the node proxies
34
- * GET/POST /robopark/* to it so the dashboard can show sessions + telemetry
35
- * (product data) alongside the mesh. */
36
- schedulerUrl?: string;
37
- /** Optional bearer token to present to the scheduler on proxied requests. */
38
- schedulerToken?: string;
39
- /** When true, only GET/HEAD are proxied to the scheduler — write actions
40
- * (end-session, production_mode toggle, trigger) are refused. Set for a
41
- * network-exposed, tokenless node so the operator console stays read-only. */
42
- schedulerReadOnly?: boolean;
43
31
  onRpc: RpcHandler;
44
32
  logger: Logger;
45
33
  }
@@ -51,8 +39,6 @@ export declare class MeshServer {
51
39
  constructor(opts: MeshServerOptions);
52
40
  get streamCount(): number;
53
41
  start(): Promise<void>;
54
- /** Proxy a dashboard request to the RoboPark scheduler (product data). */
55
- private proxyToScheduler;
56
42
  /** Push a frame to every connected stream subscriber. */
57
43
  broadcast(frame: StreamFrame): void;
58
44
  /** Token presented on a request — Bearer header (peers/CLI) OR ?token= query
@@ -16,28 +16,7 @@
16
16
  import { createServer } from 'node:http';
17
17
  import { createServer as createHttpsServer } from 'node:https';
18
18
  import { timingSafeEqual } from 'node:crypto';
19
- import { readFileSync } from 'node:fs';
20
- import { createRequire } from 'node:module';
21
19
  import { frameToSSE, parseSSE } from './protocol.js';
22
- /** Read the livekit-client UMD bundle from the installed dependency, once.
23
- * Returns null (cached) if the package isn't present — the dashboard then
24
- * falls back to an idle camera panel instead of erroring. */
25
- let _lkUmd;
26
- function loadLiveKitUmd() {
27
- if (_lkUmd !== undefined)
28
- return _lkUmd;
29
- try {
30
- const require = createRequire(import.meta.url);
31
- // The package's exports map blocks deep subpaths, but its main/require entry
32
- // IS the UMD bundle — so a bare resolve returns dist/livekit-client.umd.js.
33
- const file = require.resolve('livekit-client');
34
- _lkUmd = readFileSync(file, 'utf8');
35
- }
36
- catch {
37
- _lkUmd = null;
38
- }
39
- return _lkUmd;
40
- }
41
20
  /** Constant-time string compare — avoids leaking the token via response timing. */
42
21
  function safeEqual(a, b) {
43
22
  const ab = Buffer.from(a);
@@ -72,32 +51,6 @@ export class MeshServer {
72
51
  const scheme = this.opts.tls ? 'https' : 'http';
73
52
  this.opts.logger.info(`[federation] mesh listening on ${scheme}://${this.opts.host ?? '0.0.0.0'}:${this.opts.port}`);
74
53
  }
75
- /** Proxy a dashboard request to the RoboPark scheduler (product data). */
76
- proxyToScheduler(req, res, path) {
77
- const base = this.opts.schedulerUrl.replace(/\/+$/, '');
78
- // strip the /robopark prefix; forward the rest (incl. query) to the scheduler
79
- const rest = path.replace(/^\/robopark/, '') || '/';
80
- const target = `${base}${rest}`;
81
- this.readBody(req)
82
- .then(async (body) => {
83
- const headers = { 'content-type': req.headers['content-type'] ?? 'application/json' };
84
- if (this.opts.schedulerToken)
85
- headers['authorization'] = `Bearer ${this.opts.schedulerToken}`;
86
- const upstream = await fetch(target, {
87
- method: req.method,
88
- headers,
89
- body: req.method === 'GET' || req.method === 'HEAD' ? undefined : body,
90
- signal: AbortSignal.timeout(10_000),
91
- });
92
- const text = await upstream.text();
93
- res.writeHead(upstream.status, { 'content-type': upstream.headers.get('content-type') ?? 'application/json' });
94
- res.end(text);
95
- })
96
- .catch(err => {
97
- res.writeHead(502, { 'content-type': 'application/json' });
98
- res.end(JSON.stringify({ ok: false, error: `scheduler unreachable: ${err instanceof Error ? err.message : String(err)}` }));
99
- });
100
- }
101
54
  /** Push a frame to every connected stream subscriber. */
102
55
  broadcast(frame) {
103
56
  if (this.sse.size === 0)
@@ -141,29 +94,10 @@ export class MeshServer {
141
94
  return;
142
95
  }
143
96
  const remote = req.socket.remoteAddress ?? 'unknown';
144
- // Control dashboard (single self-contained page). Same-origin fetches carry
145
- // the ?token= through, so no CORS is opened.
97
+ // Mesh root: redirect to /fed/status so browsers and probes get useful data.
146
98
  if (req.method === 'GET' && (path === '/' || path === '/ui' || path === '/dashboard')) {
147
- if (!this.opts.dashboardHtml) {
148
- res.writeHead(404).end('dashboard not enabled (start with --dashboard)');
149
- return;
150
- }
151
- res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
152
- res.end(this.opts.dashboardHtml);
153
- return;
154
- }
155
- // LiveKit browser SDK, served from the installed dependency so the Park
156
- // tab can subscribe to a robot's live camera track. Loaded by the dashboard
157
- // with the ?token= appended (so it passes the auth gate above). Optional:
158
- // if livekit-client isn't installed, the dashboard just shows an idle cam.
159
- if (req.method === 'GET' && path === '/vendor/livekit-client.umd.js') {
160
- const js = loadLiveKitUmd();
161
- if (!js) {
162
- res.writeHead(404).end('livekit-client not installed');
163
- return;
164
- }
165
- res.writeHead(200, { 'content-type': 'application/javascript; charset=utf-8', 'cache-control': 'public, max-age=86400' });
166
- res.end(js);
99
+ res.writeHead(302, { location: '/fed/status' });
100
+ res.end();
167
101
  return;
168
102
  }
169
103
  if (req.method === 'GET' && path === '/fed/status') {
@@ -171,25 +105,6 @@ export class MeshServer {
171
105
  res.end(JSON.stringify(this.opts.getStatus?.() ?? {}));
172
106
  return;
173
107
  }
174
- // RoboPark product data (sessions, telemetry, robots) — proxied to the
175
- // scheduler so the dashboard is one pane of glass. Already token-gated above.
176
- if (path.startsWith('/robopark/')) {
177
- if (!this.opts.schedulerUrl) {
178
- res.writeHead(200, { 'content-type': 'application/json' });
179
- res.end(JSON.stringify({ ok: false, disabled: true, error: 'no scheduler linked — start with --scheduler-url' }));
180
- return;
181
- }
182
- // Keep a network-exposed, tokenless node read-only: mesh writes are gated
183
- // by commandGate; the scheduler proxy is the product-side equivalent.
184
- const method = (req.method ?? 'GET').toUpperCase();
185
- if (this.opts.schedulerReadOnly && method !== 'GET' && method !== 'HEAD') {
186
- res.writeHead(403, { 'content-type': 'application/json' });
187
- res.end(JSON.stringify({ ok: false, readonly: true, error: 'this node is network-exposed without a token — control actions are disabled. Restart with --token <secret> to enable them.' }));
188
- return;
189
- }
190
- this.proxyToScheduler(req, res, path);
191
- return;
192
- }
193
108
  if (req.method === 'POST' && path.startsWith('/fed/apply')) {
194
109
  if (!this.opts.onApply) {
195
110
  res.writeHead(404).end('no apply handler');
@@ -0,0 +1,16 @@
1
+ export interface ServiceArgs {
2
+ role: 'hub' | 'robot' | 'control' | 'hub-scheduler';
3
+ name: string;
4
+ command: string;
5
+ args: string[];
6
+ workingDir?: string;
7
+ env?: Record<string, string>;
8
+ }
9
+ export declare function registerAutoStart(service: ServiceArgs): Promise<{
10
+ ok: boolean;
11
+ message: string;
12
+ }>;
13
+ export declare function unregisterAutoStart(role: string, name: string): {
14
+ ok: boolean;
15
+ message: string;
16
+ };