infinicode 2.8.37 → 2.8.39

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.
@@ -53,6 +53,8 @@ export declare class MeshServer {
53
53
  start(): Promise<void>;
54
54
  /** Proxy a dashboard request to the RoboPark scheduler (product data). */
55
55
  private proxyToScheduler;
56
+ /** Proxy the scheduler's full Control Center and its same-origin assets. */
57
+ private proxySchedulerPath;
56
58
  /** Protected local runtime controls for the RoboPark operator dashboard. */
57
59
  private runtimeDocker;
58
60
  /** Push a frame to every connected stream subscriber. */
@@ -123,6 +123,39 @@ export class MeshServer {
123
123
  res.end(JSON.stringify({ ok: false, error: `scheduler unreachable: ${err instanceof Error ? err.message : String(err)}` }));
124
124
  });
125
125
  }
126
+ /** Proxy the scheduler's full Control Center and its same-origin assets. */
127
+ proxySchedulerPath(req, res, path) {
128
+ const base = this.opts.schedulerUrl.replace(/\/+$/, '');
129
+ const target = `${base}${path}${req.url?.includes('?') ? req.url.slice(req.url.indexOf('?')) : ''}`;
130
+ this.readBody(req)
131
+ .then(async (body) => {
132
+ const headers = {};
133
+ if (req.headers['content-type'])
134
+ headers['content-type'] = req.headers['content-type'];
135
+ if (this.opts.schedulerToken)
136
+ headers.authorization = `Bearer ${this.opts.schedulerToken}`;
137
+ const upstream = await fetch(target, {
138
+ method: req.method,
139
+ headers,
140
+ body: req.method === 'GET' || req.method === 'HEAD' ? undefined : body,
141
+ signal: AbortSignal.timeout(15_000),
142
+ });
143
+ const responseHeaders = {
144
+ 'content-type': upstream.headers.get('content-type') ?? 'application/octet-stream',
145
+ 'cache-control': upstream.headers.get('cache-control') ?? 'no-store',
146
+ };
147
+ const presented = this.presentedToken(req);
148
+ if (this.opts.token && presented) {
149
+ responseHeaders['set-cookie'] = `robopark_auth=${encodeURIComponent(presented)}; Path=/; SameSite=Lax`;
150
+ }
151
+ res.writeHead(upstream.status, responseHeaders);
152
+ res.end(await upstream.arrayBuffer());
153
+ })
154
+ .catch(err => {
155
+ res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
156
+ res.end(`scheduler unreachable: ${err instanceof Error ? err.message : String(err)}`);
157
+ });
158
+ }
126
159
  /** Protected local runtime controls for the RoboPark operator dashboard. */
127
160
  async runtimeDocker(req, res, path) {
128
161
  const send = (status, body) => {
@@ -195,6 +228,10 @@ export class MeshServer {
195
228
  if (t)
196
229
  return t;
197
230
  }
231
+ const cookie = req.headers.cookie ?? '';
232
+ const match = cookie.match(/(?:^|;\s*)robopark_auth=([^;]+)/);
233
+ if (match)
234
+ return decodeURIComponent(match[1]);
198
235
  return undefined;
199
236
  }
200
237
  authOk(req) {
@@ -215,17 +252,41 @@ export class MeshServer {
215
252
  return;
216
253
  }
217
254
  const remote = req.socket.remoteAddress ?? 'unknown';
218
- // Control dashboard (single self-contained page). Same-origin fetches carry
219
- // the ?token= through, so no CORS is opened.
255
+ // The scheduler owns the complete RoboPark Control Center. The mesh page is
256
+ // retained at /mesh as a diagnostics view, not as the primary operator UI.
220
257
  if (req.method === 'GET' && (path === '/' || path === '/ui' || path === '/dashboard')) {
258
+ if (this.opts.schedulerUrl) {
259
+ this.proxySchedulerPath(req, res, '/');
260
+ return;
261
+ }
221
262
  if (!this.opts.dashboardHtml) {
222
263
  res.writeHead(404).end('dashboard not enabled (start with --dashboard)');
223
264
  return;
224
265
  }
266
+ res.writeHead(200, {
267
+ 'content-type': 'text/html; charset=utf-8',
268
+ 'cache-control': 'no-store',
269
+ ...(this.opts.token && this.presentedToken(req)
270
+ ? { 'set-cookie': `robopark_auth=${encodeURIComponent(this.presentedToken(req))}; Path=/; SameSite=Lax` }
271
+ : {}),
272
+ });
273
+ res.end(this.opts.dashboardHtml);
274
+ return;
275
+ }
276
+ if (req.method === 'GET' && path === '/mesh') {
277
+ if (!this.opts.dashboardHtml) {
278
+ res.writeHead(404).end('mesh dashboard not enabled (start with --dashboard)');
279
+ return;
280
+ }
225
281
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
226
282
  res.end(this.opts.dashboardHtml);
227
283
  return;
228
284
  }
285
+ // Full scheduler dashboard requests are same-origin after the root proxy.
286
+ if (this.opts.schedulerUrl && (path.startsWith('/api/') || path.startsWith('/static/') || path.startsWith('/vendor/'))) {
287
+ this.proxySchedulerPath(req, res, path);
288
+ return;
289
+ }
229
290
  // Static assets bundled with the dashboard (robot concept avatars, etc.).
230
291
  if (req.method === 'GET' && path.startsWith('/static/')) {
231
292
  const asset = loadStaticAsset(path.slice(8));
@@ -7,5 +7,7 @@ export interface AddRobotOptions extends ExplicitContextOpts {
7
7
  /** Force a specific park-map pad/character preset id instead of the
8
8
  * fuzzy name-based match below — e.g. `--character jaguar`. */
9
9
  character?: string;
10
+ lan?: boolean;
11
+ tailscale?: boolean;
10
12
  }
11
13
  export declare function roboparkAddRobot(config: Conf<InfinicodeConfig>, opts: AddRobotOptions): Promise<void>;
@@ -73,6 +73,11 @@ export async function roboparkAddRobot(config, opts) {
73
73
  console.log(chalk.red(' ✗ --name is required, e.g. --name robobmw'));
74
74
  process.exit(1);
75
75
  }
76
+ if (opts.lan && opts.tailscale) {
77
+ console.log(chalk.red(' ✗ choose exactly one network mode: --lan or --tailscale'));
78
+ process.exit(1);
79
+ }
80
+ const network = opts.tailscale ? 'tailscale' : 'lan';
76
81
  const ctx = await resolveContext(opts);
77
82
  if (!ctx.schedulerUrl) {
78
83
  console.log(chalk.red(' ✗ no scheduler URL resolved. Pass --scheduler-url/--hub-url, run `robopark hub-use` first, or run this on/near the hub.'));
@@ -80,6 +85,7 @@ export async function roboparkAddRobot(config, opts) {
80
85
  }
81
86
  console.log(` scheduler: ${chalk.cyan(ctx.schedulerUrl)}`);
82
87
  console.log(` name: ${chalk.cyan(opts.name)}${ctx.site ? chalk.dim(' @ ' + ctx.site) : ''}`);
88
+ console.log(` network: ${chalk.cyan(network)}`);
83
89
  console.log();
84
90
  const characterId = opts.character?.trim() || await pickCharacterPreset(ctx.schedulerUrl, opts.name);
85
91
  if (characterId) {
@@ -114,6 +120,8 @@ export async function roboparkAddRobot(config, opts) {
114
120
  schedulerPort: ctx.schedulerUrl ? String(new URL(ctx.schedulerUrl).port || '8080') : undefined,
115
121
  enrollmentToken: minted.enrollment_token,
116
122
  start: true,
123
+ lan: opts.lan,
124
+ tailscale: opts.tailscale,
117
125
  });
118
126
  return;
119
127
  }
@@ -131,6 +139,7 @@ export async function roboparkAddRobot(config, opts) {
131
139
  const schedulerPort = new URL(ctx.schedulerUrl).port || '8080';
132
140
  argv.push('--scheduler-port', schedulerPort);
133
141
  }
142
+ argv.push(network === 'tailscale' ? '--tailscale' : '--lan');
134
143
  argv.push('--enrollment-token', minted.enrollment_token, '--start', '--auto-start');
135
144
  console.log(chalk.dim(` run this ON THE ROBOT${characterId ? ` (binds to pad: ${characterId})` : ''}:`));
136
145
  console.log(' ' + chalk.cyan(commandString(argv)));
@@ -0,0 +1,8 @@
1
+ type Options = {
2
+ schedulerUrl: string;
3
+ workerEnv: string;
4
+ rotate?: boolean;
5
+ };
6
+ /** Provision the scheduler's shared worker secret into a ROBOVOICE .env file. */
7
+ export declare function roboparkSecrets(opts: Options): Promise<void>;
8
+ export {};
@@ -0,0 +1,41 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import chalk from 'chalk';
4
+ function updateEnv(contents, token) {
5
+ const line = `ROBOPARK_AGENT_TOKEN=${token}`;
6
+ const re = /^ROBOPARK_AGENT_TOKEN=.*$/m;
7
+ return re.test(contents)
8
+ ? contents.replace(re, line)
9
+ : `${contents.trimEnd()}\n${line}\n`;
10
+ }
11
+ /** Provision the scheduler's shared worker secret into a ROBOVOICE .env file. */
12
+ export async function roboparkSecrets(opts) {
13
+ const base = opts.schedulerUrl.replace(/\/$/, '');
14
+ const response = await fetch(`${base}/api/settings/agent-token/provision`, {
15
+ method: 'POST',
16
+ headers: { 'content-type': 'application/json' },
17
+ body: JSON.stringify({ rotate: Boolean(opts.rotate) }),
18
+ });
19
+ if (!response.ok) {
20
+ throw new Error(`scheduler rejected secret provisioning (${response.status})`);
21
+ }
22
+ const body = await response.json();
23
+ if (!body.agent_token)
24
+ throw new Error('scheduler returned no agent token');
25
+ let current = '';
26
+ try {
27
+ current = await readFile(opts.workerEnv, 'utf8');
28
+ }
29
+ catch (err) {
30
+ if (err?.code !== 'ENOENT')
31
+ throw err;
32
+ }
33
+ const next = updateEnv(current, body.agent_token);
34
+ await mkdir(dirname(opts.workerEnv), { recursive: true });
35
+ const temp = `${opts.workerEnv}.tmp-${process.pid}`;
36
+ await writeFile(temp, next, { encoding: 'utf8', mode: 0o600 });
37
+ await rename(temp, opts.workerEnv);
38
+ console.log(chalk.green(` ✓ ROBOVOICE secret synchronized to ${opts.workerEnv}`));
39
+ if (opts.rotate)
40
+ console.log(chalk.yellow(' Restart the ROBOVOICE worker to use the rotated secret.'));
41
+ }
@@ -5,6 +5,7 @@ export interface ServeOptions {
5
5
  gateway?: string;
6
6
  gatewayToken?: string;
7
7
  gatewayPassword?: string;
8
+ /** Open the Control Center after the scheduler is reachable. */
8
9
  open?: boolean;
9
10
  foreground?: boolean;
10
11
  }
@@ -181,14 +181,19 @@ export async function roboparkServe(opts) {
181
181
  catch { /* ignore */ }
182
182
  });
183
183
  proc.on('exit', () => process.exit(0));
184
+ if (opts.open !== false) {
185
+ await openDashboard(host, port);
186
+ }
184
187
  }
185
188
  else {
186
189
  // Detached mode: let the user walk away. The process keeps running.
187
190
  proc.unref();
191
+ if (opts.open !== false) {
192
+ await openDashboard(host, port);
193
+ }
188
194
  printUrl(port);
189
195
  console.log(chalk.dim(' running in background. Use Task Manager / systemctl / launchctl to stop.'));
190
- // give the scheduler a moment to bind before printing URL
191
- setTimeout(() => process.exit(0), 1200);
196
+ setTimeout(() => process.exit(0), 100);
192
197
  }
193
198
  }
194
199
  function printUrl(port) {
@@ -205,3 +210,43 @@ export async function schedulerHealthy(url, timeoutMs = 3000) {
205
210
  return false;
206
211
  }
207
212
  }
213
+ function dashboardUrl(host, port) {
214
+ // 0.0.0.0/:: are bind addresses, not useful browser destinations.
215
+ const browserHost = host === '0.0.0.0' || host === '::' ? 'localhost' : host;
216
+ const formattedHost = browserHost.includes(':') && !browserHost.startsWith('[')
217
+ ? `[${browserHost}]`
218
+ : browserHost;
219
+ return `http://${formattedHost}:${port}/`;
220
+ }
221
+ function openBrowser(url) {
222
+ // Use detached platform-native launchers so robopark serve remains usable
223
+ // from terminals and does not inherit browser stdio or lifecycle.
224
+ const command = process.platform === 'win32'
225
+ ? 'cmd.exe'
226
+ : process.platform === 'darwin'
227
+ ? 'open'
228
+ : 'xdg-open';
229
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
230
+ try {
231
+ spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
232
+ }
233
+ catch (err) {
234
+ console.log(chalk.yellow(` ⚠ could not open browser: ${err instanceof Error ? err.message : String(err)}`));
235
+ }
236
+ }
237
+ async function openDashboard(host, port) {
238
+ const url = dashboardUrl(host, port);
239
+ const deadline = Date.now() + 10_000;
240
+ let healthy = false;
241
+ while (Date.now() < deadline) {
242
+ if (await schedulerHealthy(url, 500)) {
243
+ healthy = true;
244
+ break;
245
+ }
246
+ await new Promise((resolve) => setTimeout(resolve, 250));
247
+ }
248
+ if (!healthy) {
249
+ console.log(chalk.yellow(' ⚠ scheduler did not answer within 10s; opening the dashboard anyway.'));
250
+ }
251
+ openBrowser(url);
252
+ }
@@ -21,5 +21,8 @@ export interface SetupOptions {
21
21
  autoStart?: boolean;
22
22
  yes?: boolean;
23
23
  foreground?: boolean;
24
+ /** Robot transport. Defaults to LAN for backwards compatibility. */
25
+ lan?: boolean;
26
+ tailscale?: boolean;
24
27
  }
25
28
  export declare function roboparkSetup(config: Conf<InfinicodeConfig>, opts: SetupOptions): Promise<void>;
@@ -171,6 +171,12 @@ async function setupHub(config, opts, internal) {
171
171
  }
172
172
  }
173
173
  async function setupRobot(config, opts, ctx, internal) {
174
+ if (opts.lan && opts.tailscale) {
175
+ console.log(chalk.red(' ✗ choose exactly one robot network: --lan or --tailscale'));
176
+ return;
177
+ }
178
+ const network = opts.tailscale ? 'tailscale' : 'lan';
179
+ const networkFlag = network === 'tailscale' ? '--tailscale' : '--lan';
174
180
  const hubUrl = opts.hub ?? (ctx.hub ? `http://${ctx.hub.ip}:${internal.port}` : undefined);
175
181
  if (!hubUrl) {
176
182
  console.log(chalk.red(' ✗ no hub discovered. Pass --hub http://HUB_IP:47913'));
@@ -184,7 +190,7 @@ async function setupRobot(config, opts, ctx, internal) {
184
190
  displayName: internal.name,
185
191
  token: internal.token,
186
192
  seeds: [hubUrl],
187
- lan: true,
193
+ lan: network === 'lan',
188
194
  };
189
195
  config.set('federation', fed);
190
196
  const args = [
@@ -193,7 +199,7 @@ async function setupRobot(config, opts, ctx, internal) {
193
199
  '--port', String(internal.port),
194
200
  '--token', internal.token,
195
201
  '--seed', hubUrl,
196
- '--lan',
202
+ networkFlag,
197
203
  '--auto-update',
198
204
  '--supervised',
199
205
  ];
@@ -225,6 +231,7 @@ async function setupRobot(config, opts, ctx, internal) {
225
231
  console.log(' ' + chalk.cyan(`robopark ${previewAgentArgs.join(' ')}`));
226
232
  if (visionEnabled)
227
233
  console.log(' ' + chalk.cyan(`robopark ${visionAgentArgs.join(' ')}`));
234
+ console.log(chalk.dim(` network: ${network} (scheduler: ${schedulerUrl})`));
228
235
  console.log();
229
236
  if (opts.start) {
230
237
  const robotArgv = await infinicodeArgv(args);
@@ -20,6 +20,7 @@ import { roboparkDoctor } from './robopark/doctor.js';
20
20
  import { roboparkAddRobot } from './robopark/add-robot.js';
21
21
  import { saveHubProfile, clearHubProfile } from './robopark/profile.js';
22
22
  import { roboparkAgentUp, roboparkAgentDown, roboparkAgentLogs } from './robopark/agent-ctl.js';
23
+ import { roboparkSecrets } from './robopark/secrets.js';
23
24
  // Read the real version from package.json so `--version` never drifts from
24
25
  // what is actually installed (this was a hardcoded constant that went stale
25
26
  // across several releases — see the identical fix + comment in cli.ts).
@@ -93,10 +94,26 @@ program
93
94
  .option('--gateway <url>', 'InfiniBot gateway ws:// URL')
94
95
  .option('--gateway-token <token>', 'InfiniBot gateway token')
95
96
  .option('--gateway-password <pw>', 'InfiniBot gateway password')
97
+ .option('--no-open', 'do not open the RoboPark Control Center in a browser')
96
98
  .option('--foreground', 'stay in foreground; do not detach')
97
99
  .action(async (opts) => {
98
100
  await roboparkServe(opts);
99
101
  });
102
+ program
103
+ .command('secrets')
104
+ .description('Provision the scheduler-managed ROBOVOICE authentication secret')
105
+ .option('--scheduler-url <url>', 'scheduler base URL', 'http://localhost:8080')
106
+ .option('--worker-env <path>', 'ROBOVOICE worker .env file', '.env')
107
+ .option('--rotate', 'rotate the shared secret before synchronizing it')
108
+ .action(async (opts) => {
109
+ try {
110
+ await roboparkSecrets(opts);
111
+ }
112
+ catch (err) {
113
+ console.error(chalk.red(` ✗ secret provisioning failed: ${err?.message ?? err}`));
114
+ process.exitCode = 1;
115
+ }
116
+ });
100
117
  program
101
118
  .command('enroll')
102
119
  .description('Enroll this robot/satellite with the RoboPark scheduler')
@@ -227,6 +244,8 @@ program
227
244
  .option('--token <token>', 'shared mesh token (defaults to saved hub profile / auto-discovery)')
228
245
  .option('--scheduler-url <url>', 'scheduler base URL (defaults to saved hub profile / auto-discovery)')
229
246
  .option('--character <id>', 'force a specific park-map pad/character preset id (default: fuzzy-matched from the robot name)')
247
+ .option('--lan', 'generate a robot command for the same local network (default)')
248
+ .option('--tailscale', 'generate a robot command for a Tailscale-reachable hub')
230
249
  .option('--start', 'start the robot node on this machine now, instead of printing the one-liner')
231
250
  .action(async (name, opts) => {
232
251
  await roboparkAddRobot(config, { name, ...opts });
@@ -272,6 +291,8 @@ program
272
291
  .option('--tailscale-auth <token>', 'Tailscale auth key for the hub')
273
292
  .option('--tag <tag>', 'Tailscale tag filter')
274
293
  .option('--enrollment-token <token>', 'one-time scheduler enrollment token for robots')
294
+ .option('--lan', 'use LAN discovery and LAN hub/scheduler addresses (default for robots)')
295
+ .option('--tailscale', 'use Tailscale discovery and Tailscale hub/scheduler addresses')
275
296
  .option('--video-device <dev>', 'default camera for robot preview agent', 'auto')
276
297
  .option('--audio-device <dev>', 'default microphone for robot preview agent', 'default')
277
298
  .option('--no-vision', 'do not start the vision (camera/motion trigger) agent for a robot — armed by default')
@@ -303,6 +324,8 @@ program
303
324
  start: opts.start,
304
325
  autoStart: opts.autoStart,
305
326
  yes: opts.yes,
327
+ lan: opts.lan,
328
+ tailscale: opts.tailscale,
306
329
  });
307
330
  });
308
331
  // Keep legacy fleet/run/apply/dashboard commands available while migrating.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.37",
3
+ "version": "2.8.39",
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",