infinicode 2.8.4 → 2.8.6

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,7 +311,10 @@ 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('--tls-cert <path>', 'PEM cert file serve the mesh over HTTPS')
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')
315
318
  .option('--tls-key <path>', 'PEM private key file for --tls-cert')
316
319
  .option('--gateway <url>', 'link to an InfiniBot gateway (ws://host:18789) — appear on its neural-mesh map')
317
320
  .option('--gateway-token <token>', 'auth token for the InfiniBot gateway')
@@ -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, 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 },
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 },
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,6 +192,11 @@ 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);
195
200
  const lan = opts.lan ?? preset.lan;
196
201
  const tailscale = opts.tailscale ?? (preset.tailscale && !opts.hub); // seed URL replaces discovery
197
202
  const port = opts.port ? parseInt(opts.port, 10) : DEFAULT_PORT;
@@ -237,29 +242,33 @@ async function roboparkSetup(config, opts) {
237
242
  tailscale,
238
243
  lan,
239
244
  tag: opts.tag,
245
+ dashboard,
246
+ schedulerUrl,
247
+ schedulerToken: opts.schedulerToken,
240
248
  gateway: opts.gateway,
241
249
  gatewayToken: opts.gatewayToken,
242
250
  };
243
- const infinicodeCmd = [
251
+ if (opts.start) {
252
+ console.log(chalk.dim(' starting node…\n'));
253
+ await serve(config, serveOpts);
254
+ return;
255
+ }
256
+ const startCmd = [
244
257
  'infinicode serve',
245
258
  `--role ${meshRole}`,
259
+ dashboard ? '--dashboard' : '',
260
+ schedulerUrl ? `--scheduler-url ${schedulerUrl}` : '',
246
261
  lan ? '--lan' : '',
247
262
  tailscale ? '--tailscale' : '',
248
263
  opts.tag ? `--tag ${opts.tag}` : '',
249
264
  opts.hub ? `--seed ${opts.hub}` : '',
250
265
  fed.token ? `--token ${fed.token}` : '',
251
266
  opts.gateway ? `--gateway ${opts.gateway}` : '',
252
- ].filter(Boolean).join(' ');
253
- if (opts.start) {
254
- console.log(chalk.dim(' starting node…\n'));
255
- await serve(config, serveOpts);
256
- return;
257
- }
267
+ ]
268
+ .filter(Boolean)
269
+ .join(' ');
258
270
  console.log(chalk.dim(' saved. start it now with: ') + chalk.cyan('robopark setup --' + label + ' --start'));
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
- }
271
+ console.log(chalk.dim(' or run the node directly: ') + chalk.cyan(startCmd));
263
272
  console.log();
264
273
  }
265
274
  async function roboparkFleet(config, opts) {
@@ -356,13 +365,17 @@ function openBrowser(url) {
356
365
  }
357
366
  }
358
367
  async function roboparkDashboard(config, opts) {
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(/\/+$/, '');
368
+ const base = nodeUrl(config, opts.url);
362
369
  const token = authToken(config, opts.token);
363
370
  const link = `${base}/${token ? `?token=${encodeURIComponent(token)}` : ''}`;
371
+ // Confirm the node is actually serving the dashboard before pointing there.
364
372
  try {
365
- const res = await fetch(`${base}/api/settings`, { headers: headers(token), signal: AbortSignal.timeout(5000) });
373
+ const res = await fetch(link, { headers: headers(token), signal: AbortSignal.timeout(5000) });
374
+ if (res.status === 404) {
375
+ console.log(chalk.yellow(` ⚠ node at ${base} is up but the dashboard is off.`));
376
+ console.log(chalk.dim(' restart it with infinicode serve --dashboard'));
377
+ return;
378
+ }
366
379
  if (!res.ok)
367
380
  throw new Error(`HTTP ${res.status}`);
368
381
  }
@@ -398,6 +411,9 @@ export function attachRoboparkSubcommands(cmd, config, opts) {
398
411
  .option('--architecture <text>', "one-line brief of this device's job in the wider system")
399
412
  .option('--lan', 'force zero-config LAN auto-discovery (on by default for scheduler/robot)')
400
413
  .option('--tailscale', 'force Tailscale peer discovery (on by default for scheduler/laptop)')
414
+ .option('--dashboard', 'force the web control UI (on by default for scheduler)')
415
+ .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)')
416
+ .option('--scheduler-token <token>', 'bearer token presented to the scheduler on proxied requests')
401
417
  .option('--start', 'launch the node now instead of just writing config')
402
418
  .option('--yes', 'skip the guided wizard even with no role (non-interactive)')
403
419
  .action(async (opts) => {
@@ -17,6 +17,9 @@ export interface ServeOptions {
17
17
  gateway?: string;
18
18
  gatewayToken?: string;
19
19
  gatewayPassword?: string;
20
+ dashboard?: boolean;
21
+ schedulerUrl?: string;
22
+ schedulerToken?: string;
20
23
  tlsCert?: string;
21
24
  tlsKey?: string;
22
25
  supervised?: boolean;
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import chalk from 'chalk';
13
13
  import { readFileSync } from 'node:fs';
14
- import crypto from 'node:crypto';
14
+ import { randomBytes } 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,20 +60,18 @@ 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
- // 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.`));
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.`));
68
69
  console.log(chalk.yellow(` peers must now connect with --token ${token}`));
69
70
  }
70
71
  // Keep a network-exposed, tokenless node read-only: it will answer status/map
71
72
  // queries but refuse control commands until a token is set. Loopback-only or
72
73
  // token-protected nodes are trusted and run everything.
73
74
  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`.
77
75
  const commandGate = (input) => {
78
76
  if (trusted)
79
77
  return { ok: true };
@@ -160,9 +158,13 @@ export async function serve(config, opts) {
160
158
  autoUpdate: opts.autoUpdate ?? fed.autoUpdate,
161
159
  applyConfig,
162
160
  autoPullConfig: role === 'satellite',
161
+ dashboard: opts.dashboard,
163
162
  onApply,
164
163
  commandGate,
165
164
  tls,
165
+ schedulerUrl: opts.schedulerUrl ?? fed.schedulerUrl,
166
+ schedulerToken: opts.schedulerToken,
167
+ schedulerReadOnly: !trusted,
166
168
  },
167
169
  });
168
170
  // Let the kernel's slash-command surface reach the mesh (/nodes, /node, …).
@@ -182,10 +184,18 @@ export async function serve(config, opts) {
182
184
  console.log(` auth: ${chalk.green('token required')}`);
183
185
  console.log();
184
186
  await federation.start();
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();
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
+ }
189
199
  // Connect to seeds. Satellites auto-source shared config from any hub/relay
190
200
  // they connect to (handled inside federation.connect()).
191
201
  for (const url of seeds) {
@@ -63,19 +63,28 @@ 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
- /** Publish this node's shared config to the fleet (backs POST /fed/apply). */
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). */
67
70
  onApply?: () => Promise<unknown> | unknown;
68
- /** Gate remote slash-commands. Returns {ok:false,reason} to reject — used to
69
- * keep an unauthenticated, network-exposed node read-only. */
71
+ /** Gate remote/dashboard slash-commands. Returns {ok:false,reason} to reject —
72
+ * used to keep an unauthenticated, network-exposed node read-only. */
70
73
  commandGate?: (input: string) => {
71
74
  ok: boolean;
72
75
  reason?: string;
73
76
  };
74
- /** TLS material — when set, the mesh is served over HTTPS. */
77
+ /** TLS material — when set, the node + dashboard are served over HTTPS. */
75
78
  tls?: {
76
79
  cert: Buffer;
77
80
  key: Buffer;
78
81
  };
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;
79
88
  }
80
89
  /** Consolidated fleet view served at GET /fed/status. */
81
90
  export interface FleetStatus {
@@ -89,9 +98,11 @@ export interface FleetStatus {
89
98
  peerId: string;
90
99
  f: StreamFrame;
91
100
  }>;
92
- /** Console capabilities: whether control writes are allowed on this node. */
101
+ /** Console capabilities for the dashboard: whether control writes are allowed
102
+ * on this node and whether a RoboPark scheduler is linked. */
93
103
  control: {
94
104
  readonly: boolean;
105
+ schedulerLinked: boolean;
95
106
  };
96
107
  ts: number;
97
108
  }
@@ -1,4 +1,5 @@
1
1
  import { MeshServer, MeshClient } from './transport-http.js';
2
+ import { DASHBOARD_HTML } from './dashboard-html.js';
2
3
  import { PeerMesh } from './peer-mesh.js';
3
4
  import { EventBridge } from './event-bridge.js';
4
5
  import { ConfigSync } from './config-sync.js';
@@ -88,7 +89,11 @@ export class Federation {
88
89
  runCommand: (input) => this.deps.kernel.command(input),
89
90
  commandGate: options.commandGate,
90
91
  onApply: options.onApply,
92
+ dashboardHtml: options.dashboard ? DASHBOARD_HTML : undefined,
91
93
  tls: options.tls,
94
+ schedulerUrl: options.schedulerUrl,
95
+ schedulerToken: options.schedulerToken,
96
+ schedulerReadOnly: options.schedulerReadOnly,
92
97
  onRpc: (env, remote) => this.onRpc(env, remote),
93
98
  });
94
99
  await this.server.start();
@@ -689,7 +694,8 @@ export class Federation {
689
694
  agents: this.localAgents(),
690
695
  activity: this.recentFrames(60),
691
696
  control: {
692
- readonly: Boolean(this.deps.options.commandGate && !this.deps.options.commandGate('/status').ok),
697
+ readonly: Boolean(this.deps.options.schedulerReadOnly),
698
+ schedulerLinked: Boolean(this.deps.options.schedulerUrl),
693
699
  },
694
700
  ts: Date.now(),
695
701
  };
@@ -23,11 +23,23 @@ 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;
26
28
  /** Optional TLS — when set, the node serves HTTPS/WSS instead of plain HTTP. */
27
29
  tls?: {
28
30
  cert: Buffer;
29
31
  key: Buffer;
30
32
  };
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;
31
43
  onRpc: RpcHandler;
32
44
  logger: Logger;
33
45
  }
@@ -39,6 +51,8 @@ export declare class MeshServer {
39
51
  constructor(opts: MeshServerOptions);
40
52
  get streamCount(): number;
41
53
  start(): Promise<void>;
54
+ /** Proxy a dashboard request to the RoboPark scheduler (product data). */
55
+ private proxyToScheduler;
42
56
  /** Push a frame to every connected stream subscriber. */
43
57
  broadcast(frame: StreamFrame): void;
44
58
  /** Token presented on a request — Bearer header (peers/CLI) OR ?token= query
@@ -16,7 +16,28 @@
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';
19
21
  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
+ }
20
41
  /** Constant-time string compare — avoids leaking the token via response timing. */
21
42
  function safeEqual(a, b) {
22
43
  const ab = Buffer.from(a);
@@ -51,6 +72,32 @@ export class MeshServer {
51
72
  const scheme = this.opts.tls ? 'https' : 'http';
52
73
  this.opts.logger.info(`[federation] mesh listening on ${scheme}://${this.opts.host ?? '0.0.0.0'}:${this.opts.port}`);
53
74
  }
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
+ }
54
101
  /** Push a frame to every connected stream subscriber. */
55
102
  broadcast(frame) {
56
103
  if (this.sse.size === 0)
@@ -94,10 +141,29 @@ export class MeshServer {
94
141
  return;
95
142
  }
96
143
  const remote = req.socket.remoteAddress ?? 'unknown';
97
- // Mesh root: redirect to /fed/status so browsers and probes get useful data.
144
+ // Control dashboard (single self-contained page). Same-origin fetches carry
145
+ // the ?token= through, so no CORS is opened.
98
146
  if (req.method === 'GET' && (path === '/' || path === '/ui' || path === '/dashboard')) {
99
- res.writeHead(302, { location: '/fed/status' });
100
- res.end();
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);
101
167
  return;
102
168
  }
103
169
  if (req.method === 'GET' && path === '/fed/status') {
@@ -105,6 +171,25 @@ export class MeshServer {
105
171
  res.end(JSON.stringify(this.opts.getStatus?.() ?? {}));
106
172
  return;
107
173
  }
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
+ }
108
193
  if (req.method === 'POST' && path.startsWith('/fed/apply')) {
109
194
  if (!this.opts.onApply) {
110
195
  res.writeHead(404).end('no apply handler');
@@ -88,12 +88,15 @@ async function setupHub(config, opts, internal) {
88
88
  lan: true,
89
89
  };
90
90
  config.set('federation', fed);
91
+ const schedulerPort = opts.schedulerPort ? parseInt(opts.schedulerPort, 10) : DEFAULT_SCHEDULER_PORT;
91
92
  const infinicodeArgs = [
92
93
  'serve', '--hub',
93
94
  '--name', internal.name,
94
95
  '--port', String(internal.port),
95
96
  '--token', internal.token,
96
97
  '--lan',
98
+ '--dashboard',
99
+ `--scheduler-url`, `http://127.0.0.1:${schedulerPort}`,
97
100
  '--auto-update',
98
101
  '--supervised',
99
102
  ];
@@ -101,7 +104,6 @@ async function setupHub(config, opts, internal) {
101
104
  infinicodeArgs.push('--gateway', internal.gatewayHost);
102
105
  if (internal.gatewayToken)
103
106
  infinicodeArgs.push('--gateway-token', internal.gatewayToken);
104
- const schedulerPort = opts.schedulerPort ? parseInt(opts.schedulerPort, 10) : DEFAULT_SCHEDULER_PORT;
105
107
  console.log(chalk.dim(' hub commands:'));
106
108
  console.log(' ' + chalk.cyan(`infinicode ${infinicodeArgs.join(' ')}`));
107
109
  console.log(' ' + chalk.cyan(`robopark serve --port ${schedulerPort}`));
@@ -17,7 +17,7 @@ const config = new Conf({
17
17
  });
18
18
  const program = new Command('robopark')
19
19
  .description('RoboPark fleet control CLI — set up, watch, and drive talking robots')
20
- .version('2.8.4');
20
+ .version('2.8.6');
21
21
  program
22
22
  .command('scan')
23
23
  .description('Auto-discover the fleet and print ready-to-run setup commands')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.4",
3
+ "version": "2.8.6",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",
@@ -276,19 +276,9 @@ async def init_db():
276
276
  pass # column already present
277
277
  await db.commit()
278
278
 
279
- # Insert default robots if not exist
280
- default_robots = [
281
- ("robopanda", "RoboPanda", "panda-character"),
282
- ("robobear", "RoboBear", "bear-character"),
283
- ("robocar", "RoboCar", "car-character"),
284
- ("robodragon", "RoboDragon", "dragon-character"),
285
- ("robofrog", "RoboFrog", "frog-character"),
286
- ]
287
- for robot_id, name, char_id in default_robots:
288
- await db.execute(
289
- "INSERT OR IGNORE INTO robots (id, name, character_id) VALUES (?, ?, ?)",
290
- (robot_id, name, char_id)
291
- )
279
+ # No demo robots are seeded. Actual robots enroll via the device
280
+ # enrollment API or are added through the scheduler UI. This keeps the
281
+ # fleet view honest — only real connected hardware appears.
292
282
  await db.commit()
293
283
 
294
284
  # Seed default enrollment token + production_mode flag (only on first run)