infinicode 2.3.6 → 2.4.0
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/bin/robopark.js +2 -0
- package/dist/cli.js +22 -1
- package/dist/commands/maintain.d.ts +29 -0
- package/dist/commands/maintain.js +186 -0
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/robopark.d.ts +6 -0
- package/dist/commands/robopark.js +450 -0
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +152 -11
- package/dist/kernel/agents/backends/cli-backend.d.ts +4 -0
- package/dist/kernel/agents/backends/cli-backend.js +13 -5
- package/dist/kernel/agents/backends/detect.d.ts +12 -0
- package/dist/kernel/agents/backends/detect.js +102 -2
- package/dist/kernel/agents/backends/profiles.d.ts +67 -0
- package/dist/kernel/agents/backends/profiles.js +48 -0
- package/dist/kernel/agents/backends/registry.d.ts +2 -1
- package/dist/kernel/agents/backends/registry.js +39 -5
- package/dist/kernel/agents/backends/rotation.d.ts +51 -0
- package/dist/kernel/agents/backends/rotation.js +107 -0
- package/dist/kernel/agents/backends/types.d.ts +3 -1
- package/dist/kernel/federation/dashboard-html.d.ts +13 -0
- package/dist/kernel/federation/dashboard-html.js +371 -0
- package/dist/kernel/federation/federation.d.ts +58 -1
- package/dist/kernel/federation/federation.js +111 -23
- package/dist/kernel/federation/moltfed-adapter.js +1 -0
- package/dist/kernel/federation/telemetry.d.ts +1 -1
- package/dist/kernel/federation/telemetry.js +2 -1
- package/dist/kernel/federation/transport-http.d.ts +17 -1
- package/dist/kernel/federation/transport-http.js +65 -2
- package/dist/kernel/federation/types.d.ts +20 -0
- package/dist/kernel/mcp/mcp-server.js +23 -7
- package/dist/robopark-cli.d.ts +2 -0
- package/dist/robopark-cli.js +23 -0
- package/package.json +6 -3
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
* (heartbeat / task.dispatch / command / config / update).
|
|
12
12
|
*/
|
|
13
13
|
import type { Capability, KernelLike, Logger } from '../types.js';
|
|
14
|
-
import type { HardwareSnapshot, NodeIdentity, NodeStatus, PeerInfo, SharedConfig, StreamFrame } from './types.js';
|
|
14
|
+
import type { HardwareSnapshot, NodeIdentity, NodeManifest, NodeStatus, PeerInfo, SharedConfig, StreamFrame } from './types.js';
|
|
15
15
|
import { ConfigSync } from './config-sync.js';
|
|
16
16
|
import { AutoUpdate } from './auto-update.js';
|
|
17
17
|
import { ComputeRouter, type ComputeNeed } from './compute-router.js';
|
|
18
18
|
import { type ManifestInputs } from './node-identity.js';
|
|
19
19
|
import { type AgentHandle, type RoleContext, type RunRecord } from './types.js';
|
|
20
20
|
import { AgentBackendRegistry, type BackendId } from '../agents/index.js';
|
|
21
|
+
import { loadCoderConfig, type CoderConfig } from '../agents/backends/profiles.js';
|
|
21
22
|
export interface DispatchTask {
|
|
22
23
|
description: string;
|
|
23
24
|
capabilities: Capability[];
|
|
@@ -34,6 +35,14 @@ export interface DispatchTask {
|
|
|
34
35
|
sessionId?: string;
|
|
35
36
|
/** Model hint passed to a CLI backend (provider/model or a CLI-native id). */
|
|
36
37
|
model?: string;
|
|
38
|
+
/** Named coder profile to run (e.g. "deep", "cheap"). */
|
|
39
|
+
profile?: string;
|
|
40
|
+
/** Rotation strategy: 'auto' | 'tasktype' | 'policy' | 'roundrobin' | 'fallback'. */
|
|
41
|
+
strategy?: string;
|
|
42
|
+
/** Policy tier: 'free-first' | 'cheapest' | 'fastest' | 'highest-quality' | 'balanced'. */
|
|
43
|
+
policy?: string;
|
|
44
|
+
/** Task-type key for routing (e.g. 'refactor', 'docs', 'bulk'). */
|
|
45
|
+
taskType?: string;
|
|
37
46
|
}
|
|
38
47
|
export interface FederationOptions {
|
|
39
48
|
version: string;
|
|
@@ -51,6 +60,34 @@ export interface FederationOptions {
|
|
|
51
60
|
autoPullConfig?: boolean;
|
|
52
61
|
/** Satellite: how often to re-pull shared config (ms, default 30000; 0 disables). */
|
|
53
62
|
configPullMs?: number;
|
|
63
|
+
/** Coder profiles + rotation config (from node config `coder`). Merged over
|
|
64
|
+
* built-in defaults, so the mesh rotates across backends out of the box. */
|
|
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). */
|
|
70
|
+
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. */
|
|
73
|
+
commandGate?: (input: string) => {
|
|
74
|
+
ok: boolean;
|
|
75
|
+
reason?: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Consolidated fleet view served at GET /fed/status. */
|
|
79
|
+
export interface FleetStatus {
|
|
80
|
+
self: NodeManifest;
|
|
81
|
+
role: RoleContext | null;
|
|
82
|
+
nodes: NodeStatus[];
|
|
83
|
+
hardware?: HardwareSnapshot;
|
|
84
|
+
runs: RunRecord[];
|
|
85
|
+
agents: AgentHandle[];
|
|
86
|
+
activity: Array<{
|
|
87
|
+
peerId: string;
|
|
88
|
+
f: StreamFrame;
|
|
89
|
+
}>;
|
|
90
|
+
ts: number;
|
|
54
91
|
}
|
|
55
92
|
export interface FederationDeps {
|
|
56
93
|
kernel: KernelLike;
|
|
@@ -83,6 +120,10 @@ export declare class Federation {
|
|
|
83
120
|
readonly compute: ComputeRouter;
|
|
84
121
|
/** Executors available on this node (native kernel + installed CLIs). */
|
|
85
122
|
readonly backends: AgentBackendRegistry;
|
|
123
|
+
/** Resolved coder profiles + rotation policy for this node. */
|
|
124
|
+
readonly coderConfig: CoderConfig;
|
|
125
|
+
/** Per-node round-robin cursor for backend rotation. */
|
|
126
|
+
private readonly rotationState;
|
|
86
127
|
/** Local subscribers to the merged mesh frame stream (for MCP stream_activity). */
|
|
87
128
|
private frameSubs;
|
|
88
129
|
/** Recent frames for catch-up. */
|
|
@@ -110,6 +151,20 @@ export declare class Federation {
|
|
|
110
151
|
* is captured on the RunRecord so the run can be followed and later messaged.
|
|
111
152
|
*/
|
|
112
153
|
private runViaBackend;
|
|
154
|
+
/** Stream one backend's frames up the mesh, tagged with the backend id. */
|
|
155
|
+
private backendFrameSink;
|
|
156
|
+
/** Run a single backend attempt. Never throws — a missing/unavailable backend
|
|
157
|
+
* returns a failed result so a rotation chain can fall through to the next. */
|
|
158
|
+
private runBackendOnce;
|
|
159
|
+
/** Write a backend result onto the run record. */
|
|
160
|
+
private writeResult;
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a rotation chain for the task and run it with fallback: try each
|
|
163
|
+
* profile's backend in order, streaming its activity; the first `completed`
|
|
164
|
+
* wins, otherwise fall through to the next. Composes task-type routing, policy
|
|
165
|
+
* tiers, round-robin and fallback (see agents/backends/rotation.ts).
|
|
166
|
+
*/
|
|
167
|
+
private runViaBackendChain;
|
|
113
168
|
/**
|
|
114
169
|
* Hand a task to the best-fit connected peer (compute-routed) or a named one.
|
|
115
170
|
* Non-blocking by default — returns a runId to follow. Pass wait to block
|
|
@@ -203,6 +258,8 @@ export declare class Federation {
|
|
|
203
258
|
/** Neutral node-status list (self + peers) — feeds the mesh map. */
|
|
204
259
|
nodeStatuses(): NodeStatus[];
|
|
205
260
|
peers(): PeerInfo[];
|
|
261
|
+
/** Consolidated fleet view for the dashboard + `robopark fleet` (GET /fed/status). */
|
|
262
|
+
statusSnapshot(): FleetStatus;
|
|
206
263
|
/** Latest local hardware snapshot (for the MOLTFED connector / panels). */
|
|
207
264
|
latestHardware(): HardwareSnapshot | undefined;
|
|
208
265
|
/** Subscribe to the merged mesh frame stream (local + peer frames). */
|
|
@@ -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';
|
|
@@ -12,6 +13,8 @@ import { discoverTailscalePeers } from './discovery.js';
|
|
|
12
13
|
import { LanBeacon } from './lan-discovery.js';
|
|
13
14
|
import { DEFAULT_MESH_PORT } from './types.js';
|
|
14
15
|
import { createDefaultBackendRegistry } from '../agents/index.js';
|
|
16
|
+
import { RotationState, selectChain } from '../agents/backends/rotation.js';
|
|
17
|
+
import { loadCoderConfig } from '../agents/backends/profiles.js';
|
|
15
18
|
export class Federation {
|
|
16
19
|
deps;
|
|
17
20
|
server;
|
|
@@ -33,6 +36,10 @@ export class Federation {
|
|
|
33
36
|
compute = new ComputeRouter();
|
|
34
37
|
/** Executors available on this node (native kernel + installed CLIs). */
|
|
35
38
|
backends;
|
|
39
|
+
/** Resolved coder profiles + rotation policy for this node. */
|
|
40
|
+
coderConfig;
|
|
41
|
+
/** Per-node round-robin cursor for backend rotation. */
|
|
42
|
+
rotationState = new RotationState();
|
|
36
43
|
/** Local subscribers to the merged mesh frame stream (for MCP stream_activity). */
|
|
37
44
|
frameSubs = new Set();
|
|
38
45
|
/** Recent frames for catch-up. */
|
|
@@ -45,6 +52,7 @@ export class Federation {
|
|
|
45
52
|
this.deps = deps;
|
|
46
53
|
this.client = new MeshClient({ token: deps.options.token });
|
|
47
54
|
this.backends = deps.backends ?? createDefaultBackendRegistry();
|
|
55
|
+
this.coderConfig = loadCoderConfig(deps.options.coder);
|
|
48
56
|
this.configSync = new ConfigSync({ logger: deps.logger, apply: deps.options.applyConfig });
|
|
49
57
|
this.autoUpdate = new AutoUpdate({
|
|
50
58
|
version: deps.options.version,
|
|
@@ -65,7 +73,11 @@ export class Federation {
|
|
|
65
73
|
logger,
|
|
66
74
|
getManifest: () => buildManifest(identity, this.describeWithBackends()),
|
|
67
75
|
getNodes: () => this.nodeStatuses(),
|
|
76
|
+
getStatus: () => this.statusSnapshot(),
|
|
68
77
|
runCommand: (input) => this.deps.kernel.command(input),
|
|
78
|
+
commandGate: options.commandGate,
|
|
79
|
+
onApply: options.onApply,
|
|
80
|
+
dashboardHtml: options.dashboard ? DASHBOARD_HTML : undefined,
|
|
69
81
|
onRpc: (env, remote) => this.onRpc(env, remote),
|
|
70
82
|
});
|
|
71
83
|
await this.server.start();
|
|
@@ -81,9 +93,15 @@ export class Federation {
|
|
|
81
93
|
});
|
|
82
94
|
this.mesh.start();
|
|
83
95
|
// Bridge local kernel events → mesh stream (coalesced, animation-friendly).
|
|
96
|
+
// Also record into frameHistory so this node's own activity shows up in the
|
|
97
|
+
// /fed/status snapshot the dashboard + `robopark fleet` poll — not just in
|
|
98
|
+
// the live SSE push to connected harnesses.
|
|
84
99
|
this.bridge = new EventBridge({
|
|
85
100
|
nodeId: identity.nodeId,
|
|
86
|
-
emit: f =>
|
|
101
|
+
emit: f => {
|
|
102
|
+
this.server?.broadcast(f);
|
|
103
|
+
this.ingestFrame(identity.nodeId, f);
|
|
104
|
+
},
|
|
87
105
|
});
|
|
88
106
|
this.unsub = this.deps.kernel.subscribeAll(e => this.bridge?.handle(e));
|
|
89
107
|
// Telemetry heartbeat → stream a HardwareSnapshot on a fixed cadence.
|
|
@@ -234,11 +252,17 @@ export class Federation {
|
|
|
234
252
|
if (!rec)
|
|
235
253
|
return;
|
|
236
254
|
const nodeId = this.deps.identity.nodeId;
|
|
237
|
-
|
|
238
|
-
|
|
255
|
+
// Rotation is requested when a profile/strategy/route/policy is given, or the
|
|
256
|
+
// caller asks for 'auto'. Otherwise a concrete agent is a single fixed backend.
|
|
257
|
+
const wantsRotation = !!(task.profile || task.strategy || task.taskType || task.policy || task.agent === 'auto');
|
|
258
|
+
const backendId = task.agent && task.agent !== 'kernel' && task.agent !== 'auto' ? task.agent : null;
|
|
259
|
+
rec.agent = wantsRotation ? 'rotate' : (backendId ?? 'kernel');
|
|
239
260
|
this.server?.broadcast(frame('ev', nodeId, { k: 'RUN_STARTED', run: runId, desc: task.description, agent: rec.agent }));
|
|
240
261
|
try {
|
|
241
|
-
if (
|
|
262
|
+
if (wantsRotation) {
|
|
263
|
+
await this.runViaBackendChain(runId, task, rec);
|
|
264
|
+
}
|
|
265
|
+
else if (backendId) {
|
|
242
266
|
await this.runViaBackend(runId, backendId, task, rec);
|
|
243
267
|
}
|
|
244
268
|
else {
|
|
@@ -263,31 +287,82 @@ export class Federation {
|
|
|
263
287
|
* is captured on the RunRecord so the run can be followed and later messaged.
|
|
264
288
|
*/
|
|
265
289
|
async runViaBackend(runId, backendId, task, rec) {
|
|
290
|
+
const res = await this.runBackendOnce(runId, backendId, task, task.model);
|
|
291
|
+
this.writeResult(rec, runId, backendId, res);
|
|
292
|
+
}
|
|
293
|
+
/** Stream one backend's frames up the mesh, tagged with the backend id. */
|
|
294
|
+
backendFrameSink(runId, backendId) {
|
|
295
|
+
const nodeId = this.deps.identity.nodeId;
|
|
296
|
+
return (f) => {
|
|
297
|
+
if (f.kind === 'log' && f.text) {
|
|
298
|
+
this.server?.broadcast(frame('log', nodeId, { run: runId, agent: backendId, line: f.text }));
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this.server?.broadcast(frame('ev', nodeId, { k: 'AGENT_OUTPUT', run: runId, agent: backendId, kind: f.kind, text: f.text }));
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** Run a single backend attempt. Never throws — a missing/unavailable backend
|
|
306
|
+
* returns a failed result so a rotation chain can fall through to the next. */
|
|
307
|
+
async runBackendOnce(runId, backendId, task, model, args) {
|
|
266
308
|
const backend = this.backends.get(backendId);
|
|
267
309
|
if (!backend || !backend.available()) {
|
|
268
|
-
|
|
310
|
+
return { status: 'failed', error: `backend not available on this node: ${backendId}` };
|
|
269
311
|
}
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this.server?.broadcast(frame('ev', nodeId, { k: 'AGENT_OUTPUT', run: runId, agent: backendId, kind: f.kind, text: f.text }));
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
|
-
});
|
|
312
|
+
const preamble = rolePreamble(loadRole());
|
|
313
|
+
try {
|
|
314
|
+
return await backend.run({ prompt: task.prompt, capabilities: task.capabilities, model: model ?? task.model, args, sessionId: task.sessionId }, { runId, rolePreamble: preamble, onFrame: this.backendFrameSink(runId, backendId) });
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
return { status: 'failed', error: err instanceof Error ? err.message : String(err) };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** Write a backend result onto the run record. */
|
|
321
|
+
writeResult(rec, runId, backendId, res) {
|
|
322
|
+
rec.agent = backendId;
|
|
285
323
|
rec.status = res.status;
|
|
286
324
|
rec.sessionId = res.sessionId;
|
|
287
325
|
rec.missionId = res.sessionId ?? runId;
|
|
288
326
|
rec.outputs = [{ status: res.status, content: res.content, error: res.error }];
|
|
289
|
-
|
|
290
|
-
|
|
327
|
+
rec.error = res.status === 'failed' ? res.error : undefined;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Resolve a rotation chain for the task and run it with fallback: try each
|
|
331
|
+
* profile's backend in order, streaming its activity; the first `completed`
|
|
332
|
+
* wins, otherwise fall through to the next. Composes task-type routing, policy
|
|
333
|
+
* tiers, round-robin and fallback (see agents/backends/rotation.ts).
|
|
334
|
+
*/
|
|
335
|
+
async runViaBackendChain(runId, task, rec) {
|
|
336
|
+
const nodeId = this.deps.identity.nodeId;
|
|
337
|
+
const req = {
|
|
338
|
+
strategy: task.strategy,
|
|
339
|
+
profile: task.profile,
|
|
340
|
+
model: task.model,
|
|
341
|
+
taskType: task.taskType,
|
|
342
|
+
policy: task.policy,
|
|
343
|
+
backend: task.agent && task.agent !== 'auto' && task.agent !== 'kernel' ? task.agent : undefined,
|
|
344
|
+
};
|
|
345
|
+
const chain = selectChain(this.coderConfig, req, this.rotationState);
|
|
346
|
+
let last = { status: 'failed', error: 'no backend available in rotation chain' };
|
|
347
|
+
for (let i = 0; i < chain.length; i++) {
|
|
348
|
+
const step = chain[i];
|
|
349
|
+
this.server?.broadcast(frame('ev', nodeId, {
|
|
350
|
+
k: 'ROTATION_ATTEMPT', run: runId, agent: step.backend, model: step.model,
|
|
351
|
+
profile: step.profileName, attempt: i + 1, of: chain.length,
|
|
352
|
+
}));
|
|
353
|
+
last = await this.runBackendOnce(runId, step.backend, task, step.model, step.args);
|
|
354
|
+
if (last.status === 'completed') {
|
|
355
|
+
this.writeResult(rec, runId, step.backend, last);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (i < chain.length - 1) {
|
|
359
|
+
this.server?.broadcast(frame('ev', nodeId, {
|
|
360
|
+
k: 'ROTATION_FALLBACK', run: runId, from: step.backend,
|
|
361
|
+
next: chain[i + 1].backend, reason: (last.error ?? '').slice(0, 200),
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
this.writeResult(rec, runId, chain[chain.length - 1]?.backend ?? 'kernel', last);
|
|
291
366
|
}
|
|
292
367
|
// ── Outbound operations ──────────────────────────────────────────────────
|
|
293
368
|
/**
|
|
@@ -585,13 +660,26 @@ export class Federation {
|
|
|
585
660
|
version: d.version,
|
|
586
661
|
capabilities: d.capabilities,
|
|
587
662
|
commands: d.commands,
|
|
588
|
-
});
|
|
663
|
+
}, loadRole()?.site);
|
|
589
664
|
const peers = (this.mesh?.list() ?? []).map(nodeStatusFromPeer);
|
|
590
665
|
return [self, ...peers];
|
|
591
666
|
}
|
|
592
667
|
peers() {
|
|
593
668
|
return this.mesh?.list() ?? [];
|
|
594
669
|
}
|
|
670
|
+
/** Consolidated fleet view for the dashboard + `robopark fleet` (GET /fed/status). */
|
|
671
|
+
statusSnapshot() {
|
|
672
|
+
return {
|
|
673
|
+
self: buildManifest(this.deps.identity, this.describeWithBackends()),
|
|
674
|
+
role: this.getRole(),
|
|
675
|
+
nodes: this.nodeStatuses(),
|
|
676
|
+
hardware: this.lastHw,
|
|
677
|
+
runs: this.localRuns(),
|
|
678
|
+
agents: this.localAgents(),
|
|
679
|
+
activity: this.recentFrames(60),
|
|
680
|
+
ts: Date.now(),
|
|
681
|
+
};
|
|
682
|
+
}
|
|
595
683
|
/** Latest local hardware snapshot (for the MOLTFED connector / panels). */
|
|
596
684
|
latestHardware() {
|
|
597
685
|
return this.lastHw;
|
|
@@ -120,7 +120,7 @@ export function nodeStatusFromPeer(peer) {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
/** Local node's own status entry. */
|
|
123
|
-
export function selfNodeStatus(identity, hw, manifest) {
|
|
123
|
+
export function selfNodeStatus(identity, hw, manifest, site) {
|
|
124
124
|
return {
|
|
125
125
|
nodeId: identity.nodeId,
|
|
126
126
|
displayName: identity.displayName,
|
|
@@ -134,5 +134,6 @@ export function selfNodeStatus(identity, hw, manifest) {
|
|
|
134
134
|
connectedAtMs: Date.now(),
|
|
135
135
|
load: loadFromSnapshot(hw),
|
|
136
136
|
lastSeenAt: Date.now(),
|
|
137
|
+
site,
|
|
137
138
|
};
|
|
138
139
|
}
|
|
@@ -9,9 +9,22 @@ export interface MeshServerOptions {
|
|
|
9
9
|
getManifest: () => NodeManifest;
|
|
10
10
|
/** Optional: full mesh view (self + peers) for the GET /fed/nodes probe. */
|
|
11
11
|
getNodes?: () => unknown;
|
|
12
|
+
/** Optional: consolidated fleet snapshot (self + nodes + hardware + roles +
|
|
13
|
+
* runs + recent activity) for the dashboard and `robopark fleet` (GET /fed/status). */
|
|
14
|
+
getStatus?: () => unknown;
|
|
12
15
|
/** Optional: run a kernel slash-command on this node (POST /fed/command).
|
|
13
|
-
* Powers the in-TUI mesh command palette. */
|
|
16
|
+
* Powers the in-TUI mesh command palette + the dashboard's action buttons. */
|
|
14
17
|
runCommand?: (input: string) => Promise<unknown>;
|
|
18
|
+
/** Optional gate on POST /fed/command inputs. Returns {ok:false,reason} to
|
|
19
|
+
* reject — used to keep an unauthenticated, network-exposed node read-only. */
|
|
20
|
+
commandGate?: (input: string) => {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
reason?: string;
|
|
23
|
+
};
|
|
24
|
+
/** Optional: publish this node's shared config to the fleet (POST /fed/apply). */
|
|
25
|
+
onApply?: () => Promise<unknown> | unknown;
|
|
26
|
+
/** Optional: static HTML for the control dashboard, served at GET / and /ui. */
|
|
27
|
+
dashboardHtml?: string;
|
|
15
28
|
onRpc: RpcHandler;
|
|
16
29
|
logger: Logger;
|
|
17
30
|
}
|
|
@@ -25,6 +38,9 @@ export declare class MeshServer {
|
|
|
25
38
|
start(): Promise<void>;
|
|
26
39
|
/** Push a frame to every connected stream subscriber. */
|
|
27
40
|
broadcast(frame: StreamFrame): void;
|
|
41
|
+
/** Token presented on a request — Bearer header (peers/CLI) OR ?token= query
|
|
42
|
+
* (browsers, which can't set headers on navigation or EventSource). */
|
|
43
|
+
private presentedToken;
|
|
28
44
|
private authOk;
|
|
29
45
|
private handle;
|
|
30
46
|
private openStream;
|
|
@@ -14,7 +14,16 @@
|
|
|
14
14
|
* MeshClient/MeshServer shape later for deep InfiniBot-fabric interop.
|
|
15
15
|
*/
|
|
16
16
|
import { createServer } from 'node:http';
|
|
17
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
17
18
|
import { frameToSSE, parseSSE } from './protocol.js';
|
|
19
|
+
/** Constant-time string compare — avoids leaking the token via response timing. */
|
|
20
|
+
function safeEqual(a, b) {
|
|
21
|
+
const ab = Buffer.from(a);
|
|
22
|
+
const bb = Buffer.from(b);
|
|
23
|
+
if (ab.length !== bb.length)
|
|
24
|
+
return false;
|
|
25
|
+
return timingSafeEqual(ab, bb);
|
|
26
|
+
}
|
|
18
27
|
export class MeshServer {
|
|
19
28
|
opts;
|
|
20
29
|
server;
|
|
@@ -51,19 +60,67 @@ export class MeshServer {
|
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
}
|
|
63
|
+
/** Token presented on a request — Bearer header (peers/CLI) OR ?token= query
|
|
64
|
+
* (browsers, which can't set headers on navigation or EventSource). */
|
|
65
|
+
presentedToken(req) {
|
|
66
|
+
const h = req.headers['authorization'];
|
|
67
|
+
if (typeof h === 'string' && h.startsWith('Bearer '))
|
|
68
|
+
return h.slice(7);
|
|
69
|
+
const url = req.url ?? '';
|
|
70
|
+
const q = url.indexOf('?');
|
|
71
|
+
if (q >= 0) {
|
|
72
|
+
const t = new URLSearchParams(url.slice(q + 1)).get('token');
|
|
73
|
+
if (t)
|
|
74
|
+
return t;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
54
78
|
authOk(req) {
|
|
55
79
|
if (!this.opts.token)
|
|
56
80
|
return true;
|
|
57
|
-
const
|
|
58
|
-
return
|
|
81
|
+
const presented = this.presentedToken(req);
|
|
82
|
+
return presented !== undefined && safeEqual(presented, this.opts.token);
|
|
59
83
|
}
|
|
60
84
|
handle(req, res) {
|
|
61
85
|
const url = req.url ?? '/';
|
|
86
|
+
const path = url.split('?', 1)[0];
|
|
62
87
|
if (!this.authOk(req)) {
|
|
63
88
|
res.writeHead(401).end('unauthorized');
|
|
64
89
|
return;
|
|
65
90
|
}
|
|
66
91
|
const remote = req.socket.remoteAddress ?? 'unknown';
|
|
92
|
+
// Control dashboard (single self-contained page). Same-origin fetches carry
|
|
93
|
+
// the ?token= through, so no CORS is opened.
|
|
94
|
+
if (req.method === 'GET' && (path === '/' || path === '/ui' || path === '/dashboard')) {
|
|
95
|
+
if (!this.opts.dashboardHtml) {
|
|
96
|
+
res.writeHead(404).end('dashboard not enabled (start with --dashboard)');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
100
|
+
res.end(this.opts.dashboardHtml);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (req.method === 'GET' && path === '/fed/status') {
|
|
104
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
105
|
+
res.end(JSON.stringify(this.opts.getStatus?.() ?? {}));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (req.method === 'POST' && path.startsWith('/fed/apply')) {
|
|
109
|
+
if (!this.opts.onApply) {
|
|
110
|
+
res.writeHead(404).end('no apply handler');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
Promise.resolve(this.opts.onApply())
|
|
114
|
+
.then(result => {
|
|
115
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
116
|
+
res.end(JSON.stringify(result ?? { ok: true }));
|
|
117
|
+
})
|
|
118
|
+
.catch(err => {
|
|
119
|
+
this.opts.logger.warn(`[federation] apply error: ${err instanceof Error ? err.message : String(err)}`);
|
|
120
|
+
res.writeHead(500).end('apply failed');
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
67
124
|
if (req.method === 'GET' && url.startsWith('/fed/manifest')) {
|
|
68
125
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
69
126
|
res.end(JSON.stringify(this.opts.getManifest()));
|
|
@@ -93,6 +150,12 @@ export class MeshServer {
|
|
|
93
150
|
res.writeHead(400).end('bad json');
|
|
94
151
|
return;
|
|
95
152
|
}
|
|
153
|
+
const gate = this.opts.commandGate?.(input);
|
|
154
|
+
if (gate && !gate.ok) {
|
|
155
|
+
res.writeHead(403, { 'content-type': 'application/json' });
|
|
156
|
+
res.end(JSON.stringify({ ok: false, text: gate.reason ?? 'command not permitted on this node' }));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
96
159
|
const result = await this.opts.runCommand(input);
|
|
97
160
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
98
161
|
res.end(JSON.stringify(result ?? { ok: false, text: 'no result' }));
|
|
@@ -125,6 +125,10 @@ export interface NodeStatus {
|
|
|
125
125
|
/** 0–100 rough load, from the latest hardware snapshot. */
|
|
126
126
|
load?: number;
|
|
127
127
|
lastSeenAt: number;
|
|
128
|
+
/** Physical deployment this node belongs to (e.g. "Tel Aviv"). Sourced from
|
|
129
|
+
* this node's own RoleContext.site — lets a fleet UI group by location as
|
|
130
|
+
* a customer scales from one site to many, without per-site bespoke code. */
|
|
131
|
+
site?: string;
|
|
128
132
|
}
|
|
129
133
|
export type MessageKind = 'hello' | 'welcome' | 'heartbeat' | 'manifest' | 'task.dispatch' | 'task.accepted' | 'task.status' | 'task.result' | 'task.cancel' | 'command' | 'command.result' | 'config.request' | 'config.offer' | 'update.announce' | 'update.request' | 'role.get' | 'role.set' | 'agents.list' | 'agents.offer' | 'message.send' | 'message.accepted' | 'event' | 'telemetry' | 'ack' | 'error';
|
|
130
134
|
/** A dispatched/received task run — tracked for non-blocking handoff + monitoring. */
|
|
@@ -167,10 +171,26 @@ export interface AgentHandle {
|
|
|
167
171
|
description: string;
|
|
168
172
|
lastActiveAt: number;
|
|
169
173
|
}
|
|
174
|
+
/** Result of a one-shot `infinicode maintain` run, pushed to an InfiniBot gateway. */
|
|
175
|
+
export interface MaintenanceReport {
|
|
176
|
+
nodeId: string;
|
|
177
|
+
role: string;
|
|
178
|
+
site?: string;
|
|
179
|
+
taskName: string;
|
|
180
|
+
startedAt: number;
|
|
181
|
+
finishedAt: number;
|
|
182
|
+
durationMs: number;
|
|
183
|
+
ok: boolean;
|
|
184
|
+
summary: string;
|
|
185
|
+
errors?: string[];
|
|
186
|
+
}
|
|
170
187
|
/** Persistent per-device role + server-architecture context. */
|
|
171
188
|
export interface RoleContext {
|
|
172
189
|
/** e.g. "RoboPanda" — the identity a spawned agent adopts. */
|
|
173
190
|
role: string;
|
|
191
|
+
/** Physical deployment/location this device belongs to (e.g. "Tel Aviv").
|
|
192
|
+
* Optional — a single-site deployment can leave it unset. */
|
|
193
|
+
site?: string;
|
|
174
194
|
/** Free-text description of this device's job + the wider architecture. */
|
|
175
195
|
architecture?: string;
|
|
176
196
|
/** Extra structured context injected into every task run on this node. */
|
|
@@ -37,28 +37,43 @@ export function createMcpServer(deps) {
|
|
|
37
37
|
// ── Spawn + dispatch (parallel agents across devices, no SSH) ───────────────
|
|
38
38
|
server.registerTool('spawn_agent', {
|
|
39
39
|
title: 'Spawn agent on a device',
|
|
40
|
-
description: 'Hand a task to a device\'s local agent (by name or id). Non-blocking: returns a runId immediately — use follow_task to monitor. Pass wait:true to block until done (short tasks). Runs on the target with its persistent role
|
|
40
|
+
description: 'Hand a task to a device\'s local agent (by name or id). Non-blocking: returns a runId immediately — use follow_task to monitor. Pass wait:true to block until done (short tasks). Runs on the target with its persistent role.\n\nExecutor: set `agent` to a fixed CLI ("claude" | "codex" | "opencode" | "gemini") or "kernel" (native). For automatic backend rotation instead of a fixed one, set `agent:"auto"` (or any of profile/strategy/policy/taskType) — the target then resolves a coder-profile *chain* and runs it with fallback (first backend that succeeds wins). Use `profile` to pin a named profile, `taskType` to route by kind (e.g. "refactor","docs","bulk"), `policy` to order by tier ("free-first"|"cheapest"|"fastest"|"highest-quality"|"balanced"), `strategy` to force one mode, and `model` to override the head model.',
|
|
41
41
|
inputSchema: {
|
|
42
42
|
task: z.string().describe('what the agent should do'),
|
|
43
43
|
device: z.string().optional().describe('target device name or node id'),
|
|
44
44
|
capabilities: z.array(z.string()).optional().describe('required capabilities, e.g. ["coding"]'),
|
|
45
|
-
agent: z.string().optional().describe('executor
|
|
45
|
+
agent: z.string().optional().describe('executor: "kernel" (default), "claude" | "codex" | "opencode" | "gemini", or "auto" to rotate across coder profiles'),
|
|
46
|
+
profile: z.string().optional().describe('pin a named coder profile (e.g. "deep","fast","precise","cheap","oss")'),
|
|
47
|
+
strategy: z.enum(['auto', 'tasktype', 'policy', 'roundrobin', 'fallback']).optional().describe('rotation strategy (default from config: auto)'),
|
|
48
|
+
policy: z.enum(['free-first', 'cheapest', 'fastest', 'highest-quality', 'balanced']).optional().describe('policy tier ordering when rotating'),
|
|
49
|
+
taskType: z.string().optional().describe('route by task kind: refactor|architecture|debug|feature|edit|test|docs|bulk|boilerplate|fix'),
|
|
50
|
+
model: z.string().optional().describe('model override applied to the chosen backend'),
|
|
46
51
|
role: z.string().optional().describe('override the role context for this task'),
|
|
47
52
|
wait: z.boolean().optional().describe('block until the run finishes (default false)'),
|
|
48
53
|
},
|
|
49
|
-
}, async ({ task, device, capabilities, agent, role, wait }) => {
|
|
50
|
-
const
|
|
54
|
+
}, async ({ task, device, capabilities, agent, profile, strategy, policy, taskType, model, role, wait }) => {
|
|
55
|
+
const rotate = !!(profile || strategy || policy || taskType || agent === 'auto');
|
|
56
|
+
const caps = [
|
|
57
|
+
...(capabilities ?? []),
|
|
58
|
+
...(agent && agent !== 'kernel' && agent !== 'auto' ? [`backend:${agent}`] : []),
|
|
59
|
+
...(rotate ? ['coding'] : []),
|
|
60
|
+
];
|
|
51
61
|
const t = {
|
|
52
62
|
description: task.slice(0, 80),
|
|
53
63
|
prompt: task,
|
|
54
64
|
capabilities: caps,
|
|
55
65
|
agent,
|
|
56
66
|
role,
|
|
67
|
+
model,
|
|
68
|
+
profile,
|
|
69
|
+
strategy,
|
|
70
|
+
policy,
|
|
71
|
+
taskType,
|
|
57
72
|
};
|
|
58
73
|
const res = await federation.dispatch(t, { preferName: device, preferNodeId: device }, { wait });
|
|
59
74
|
if (!res)
|
|
60
75
|
return text({ ok: false, error: 'no capable device online for this task' });
|
|
61
|
-
return text({ ok: true, peerId: res.peerId, runId: res.runId, agent: agent ?? 'kernel', status: res.record.status, record: res.record });
|
|
76
|
+
return text({ ok: true, peerId: res.peerId, runId: res.runId, agent: rotate ? 'rotate' : (agent ?? 'kernel'), status: res.record.status, record: res.record });
|
|
62
77
|
});
|
|
63
78
|
server.registerTool('dispatch', {
|
|
64
79
|
title: 'Dispatch to best device',
|
|
@@ -142,11 +157,12 @@ export function createMcpServer(deps) {
|
|
|
142
157
|
description: 'Persist a device\'s role + architecture so spawned agents are proactive and context-aware.',
|
|
143
158
|
inputSchema: {
|
|
144
159
|
role: z.string().describe('e.g. "RoboPanda"'),
|
|
160
|
+
site: z.string().optional().describe('physical deployment/location, e.g. "Tel Aviv" — lets a fleet UI group by site as you add more locations'),
|
|
145
161
|
architecture: z.string().optional().describe('this device\'s job + the wider system'),
|
|
146
162
|
nodeId: z.string().optional().describe('target node; omit for local'),
|
|
147
163
|
},
|
|
148
|
-
}, async ({ role, architecture, nodeId }) => {
|
|
149
|
-
const payload = { role, architecture };
|
|
164
|
+
}, async ({ role, site, architecture, nodeId }) => {
|
|
165
|
+
const payload = { role, site, architecture };
|
|
150
166
|
return text(nodeId ? await federation.setRoleOf(nodeId, payload) : federation.setRole(payload));
|
|
151
167
|
});
|
|
152
168
|
// ── Voice (LiveKit intent — media plane handles TTS) ────────────────────────
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `robopark` — standalone entry for the RoboPark fleet control CLI.
|
|
4
|
+
*
|
|
5
|
+
* Same verbs as `infinicode robopark …`, exposed as a top-level `robopark`
|
|
6
|
+
* command so operators type `robopark fleet` / `robopark setup` directly.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import Conf from 'conf';
|
|
10
|
+
import { attachRoboparkSubcommands } from './commands/robopark.js';
|
|
11
|
+
import { DEFAULT_CONFIG } from './kernel/config-schema.js';
|
|
12
|
+
const VERSION = '2.4.0';
|
|
13
|
+
const config = new Conf({
|
|
14
|
+
projectName: 'infinicode', // share infinicode's config store (providers, federation)
|
|
15
|
+
defaults: DEFAULT_CONFIG,
|
|
16
|
+
});
|
|
17
|
+
const program = new Command();
|
|
18
|
+
program
|
|
19
|
+
.name('robopark')
|
|
20
|
+
.description('RoboPark fleet control — setup, fleet status, run, apply, dashboard (rides the infinicode mesh)')
|
|
21
|
+
.version(VERSION);
|
|
22
|
+
attachRoboparkSubcommands(program, config);
|
|
23
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinicode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
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",
|
|
7
7
|
"types": "./dist/kernel/index.d.ts",
|
|
8
8
|
"bin": {
|
|
9
9
|
"infinicode": "./bin/infinicode.js",
|
|
10
|
-
"ic": "./bin/infinicode.js"
|
|
10
|
+
"ic": "./bin/infinicode.js",
|
|
11
|
+
"robopark": "./bin/robopark.js"
|
|
11
12
|
},
|
|
12
13
|
"exports": {
|
|
13
14
|
".": {
|
|
@@ -17,10 +18,12 @@
|
|
|
17
18
|
"./kernel": {
|
|
18
19
|
"types": "./dist/kernel/index.d.ts",
|
|
19
20
|
"import": "./dist/kernel/index.js"
|
|
20
|
-
}
|
|
21
|
+
},
|
|
22
|
+
"./robopark": "./dist/robopark-cli.js"
|
|
21
23
|
},
|
|
22
24
|
"files": [
|
|
23
25
|
"bin/infinicode.js",
|
|
26
|
+
"bin/robopark.js",
|
|
24
27
|
"dist",
|
|
25
28
|
".opencode/plugins",
|
|
26
29
|
".opencode/themes",
|