infinicode 2.8.3 → 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 +4 -1
- package/dist/commands/robopark.js +35 -19
- package/dist/commands/serve.d.ts +3 -0
- package/dist/commands/serve.js +23 -13
- package/dist/kernel/federation/federation.d.ts +16 -5
- package/dist/kernel/federation/federation.js +7 -1
- package/dist/kernel/federation/transport-http.d.ts +14 -0
- package/dist/kernel/federation/transport-http.js +88 -3
- package/dist/robopark/serve.d.ts +8 -1
- package/dist/robopark/serve.js +10 -2
- package/dist/robopark/setup.js +6 -4
- package/dist/robopark-cli.js +1 -1
- package/package.json +3 -2
- package/packages/robopark/scheduler/main.py +3 -13
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('--
|
|
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,
|
|
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
|
-
|
|
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
|
-
]
|
|
253
|
-
|
|
254
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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) => {
|
package/dist/commands/serve.d.ts
CHANGED
package/dist/commands/serve.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
|
-
import
|
|
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
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
/**
|
|
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 —
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
100
|
-
|
|
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');
|
package/dist/robopark/serve.d.ts
CHANGED
|
@@ -9,7 +9,14 @@ export interface ServeOptions {
|
|
|
9
9
|
foreground?: boolean;
|
|
10
10
|
}
|
|
11
11
|
/** Resolve the actual infinicode CLI entry point so `robopark setup --start`
|
|
12
|
-
* works even when global bins are not on PATH.
|
|
12
|
+
* works even when global bins are not on PATH.
|
|
13
|
+
*
|
|
14
|
+
* Returns the node executable path and the path to `bin/infinicode.js` inside
|
|
15
|
+
* the installed `infinicode` package. We avoid spawning the bare `infinicode`
|
|
16
|
+
* command because npm's `.cmd` shim is not reliably resolvable by Node's
|
|
17
|
+
* `spawn` without `shell: true` on Windows, and shell concatenation triggers
|
|
18
|
+
* a deprecation warning when args are passed.
|
|
19
|
+
*/
|
|
13
20
|
export declare function resolveInfinicodeBin(): Promise<{
|
|
14
21
|
node: string;
|
|
15
22
|
script: string;
|
package/dist/robopark/serve.js
CHANGED
|
@@ -51,7 +51,14 @@ function findPython() {
|
|
|
51
51
|
return 'python3';
|
|
52
52
|
}
|
|
53
53
|
/** Resolve the actual infinicode CLI entry point so `robopark setup --start`
|
|
54
|
-
* works even when global bins are not on PATH.
|
|
54
|
+
* works even when global bins are not on PATH.
|
|
55
|
+
*
|
|
56
|
+
* Returns the node executable path and the path to `bin/infinicode.js` inside
|
|
57
|
+
* the installed `infinicode` package. We avoid spawning the bare `infinicode`
|
|
58
|
+
* command because npm's `.cmd` shim is not reliably resolvable by Node's
|
|
59
|
+
* `spawn` without `shell: true` on Windows, and shell concatenation triggers
|
|
60
|
+
* a deprecation warning when args are passed.
|
|
61
|
+
*/
|
|
55
62
|
export async function resolveInfinicodeBin() {
|
|
56
63
|
try {
|
|
57
64
|
// When running from the infinicode package itself, the CLI entry is nearby.
|
|
@@ -63,7 +70,8 @@ export async function resolveInfinicodeBin() {
|
|
|
63
70
|
try {
|
|
64
71
|
// When running from the robopark wrapper, resolve infinicode from npm.
|
|
65
72
|
const { createRequire } = await import('node:module');
|
|
66
|
-
|
|
73
|
+
// createRequire expects a file path, not a file:// URL.
|
|
74
|
+
const require = createRequire(fileURLToPath(import.meta.url));
|
|
67
75
|
const pkgMain = require.resolve('infinicode/package.json');
|
|
68
76
|
const candidate = join(dirname(pkgMain), 'bin', 'infinicode.js');
|
|
69
77
|
if (existsSync(candidate))
|
package/dist/robopark/setup.js
CHANGED
|
@@ -29,7 +29,6 @@ function runDetached(cmd, args, env) {
|
|
|
29
29
|
stdio: 'ignore',
|
|
30
30
|
detached: true,
|
|
31
31
|
env: { ...process.env, ...env },
|
|
32
|
-
shell: process.platform === 'win32' && cmd === 'infinicode',
|
|
33
32
|
});
|
|
34
33
|
proc.on('error', (err) => {
|
|
35
34
|
console.log(chalk.red(` ✗ failed to start ${cmd}: ${err.message}`));
|
|
@@ -41,8 +40,9 @@ async function infinicodeArgv(args) {
|
|
|
41
40
|
const bin = await resolveInfinicodeBin();
|
|
42
41
|
if (bin)
|
|
43
42
|
return [bin.node, bin.script, ...args];
|
|
44
|
-
// Fallback: assume `infinicode` is on PATH.
|
|
45
|
-
//
|
|
43
|
+
// Fallback: assume `infinicode` is on PATH. This will fail on Windows if the
|
|
44
|
+
// .cmd shim is not resolvable, but the resolved-bin path above is the
|
|
45
|
+
// intended path for all npm installs.
|
|
46
46
|
return ['infinicode', ...args];
|
|
47
47
|
}
|
|
48
48
|
/** Build a Windows-task-friendly command string from an argv array. */
|
|
@@ -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}`));
|
package/dist/robopark-cli.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"types": "./dist/kernel/index.d.ts",
|
|
20
20
|
"import": "./dist/kernel/index.js"
|
|
21
21
|
},
|
|
22
|
-
"./robopark": "./dist/robopark-cli.js"
|
|
22
|
+
"./robopark": "./dist/robopark-cli.js",
|
|
23
|
+
"./package.json": "./package.json"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"bin/infinicode.js",
|
|
@@ -276,19 +276,9 @@ async def init_db():
|
|
|
276
276
|
pass # column already present
|
|
277
277
|
await db.commit()
|
|
278
278
|
|
|
279
|
-
#
|
|
280
|
-
|
|
281
|
-
|
|
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)
|