infinicode 1.0.0 → 2.1.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/.opencode/plugins/home-logo-animation.tsx +120 -0
- package/.opencode/plugins/routing-mode-display.tsx +135 -0
- package/.opencode/themes/infinibot-gold.json +222 -0
- package/.opencode/tui.json +8 -0
- package/README.md +564 -72
- package/dist/ascii-video-animation.d.ts +2 -0
- package/dist/ascii-video-animation.js +243 -0
- package/dist/cli.js +199 -50
- package/dist/commands/console.d.ts +6 -0
- package/dist/commands/console.js +111 -0
- package/dist/commands/kernel-setup.d.ts +3 -0
- package/dist/commands/kernel-setup.js +303 -0
- package/dist/commands/mcp.d.ts +14 -0
- package/dist/commands/mcp.js +100 -0
- package/dist/commands/mission.d.ts +16 -0
- package/dist/commands/mission.js +301 -0
- package/dist/commands/models.d.ts +3 -1
- package/dist/commands/models.js +111 -55
- package/dist/commands/providers.d.ts +6 -0
- package/dist/commands/providers.js +95 -0
- package/dist/commands/run.d.ts +2 -1
- package/dist/commands/run.js +349 -59
- package/dist/commands/serve.d.ts +20 -0
- package/dist/commands/serve.js +132 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +77 -44
- package/dist/commands/status.d.ts +2 -1
- package/dist/commands/status.js +46 -30
- package/dist/commands/workers.d.ts +5 -0
- package/dist/commands/workers.js +103 -0
- package/dist/kernel/autonomy/goal-loop.d.ts +49 -0
- package/dist/kernel/autonomy/goal-loop.js +113 -0
- package/dist/kernel/autonomy/index.d.ts +8 -0
- package/dist/kernel/autonomy/index.js +7 -0
- package/dist/kernel/browser/browser-controller.d.ts +57 -0
- package/dist/kernel/browser/browser-controller.js +175 -0
- package/dist/kernel/checkpoint-engine.d.ts +17 -0
- package/dist/kernel/checkpoint-engine.js +128 -0
- package/dist/kernel/command-system.d.ts +37 -0
- package/dist/kernel/command-system.js +239 -0
- package/dist/kernel/config-schema.d.ts +57 -0
- package/dist/kernel/config-schema.js +36 -0
- package/dist/kernel/event-bus.d.ts +19 -0
- package/dist/kernel/event-bus.js +65 -0
- package/dist/kernel/federation/auto-update.d.ts +36 -0
- package/dist/kernel/federation/auto-update.js +57 -0
- package/dist/kernel/federation/compute-router.d.ts +27 -0
- package/dist/kernel/federation/compute-router.js +44 -0
- package/dist/kernel/federation/config-sync.d.ts +33 -0
- package/dist/kernel/federation/config-sync.js +37 -0
- package/dist/kernel/federation/discovery.d.ts +11 -0
- package/dist/kernel/federation/discovery.js +44 -0
- package/dist/kernel/federation/event-bridge.d.ts +30 -0
- package/dist/kernel/federation/event-bridge.js +61 -0
- package/dist/kernel/federation/federation-plugin.d.ts +24 -0
- package/dist/kernel/federation/federation-plugin.js +35 -0
- package/dist/kernel/federation/federation.d.ts +135 -0
- package/dist/kernel/federation/federation.js +378 -0
- package/dist/kernel/federation/index.d.ts +33 -0
- package/dist/kernel/federation/index.js +24 -0
- package/dist/kernel/federation/lan-discovery.d.ts +43 -0
- package/dist/kernel/federation/lan-discovery.js +135 -0
- package/dist/kernel/federation/moltfed-adapter.d.ts +30 -0
- package/dist/kernel/federation/moltfed-adapter.js +52 -0
- package/dist/kernel/federation/moltfed-connector.d.ts +98 -0
- package/dist/kernel/federation/moltfed-connector.js +193 -0
- package/dist/kernel/federation/node-identity.d.ts +19 -0
- package/dist/kernel/federation/node-identity.js +86 -0
- package/dist/kernel/federation/peer-mesh.d.ts +46 -0
- package/dist/kernel/federation/peer-mesh.js +117 -0
- package/dist/kernel/federation/protocol.d.ts +20 -0
- package/dist/kernel/federation/protocol.js +61 -0
- package/dist/kernel/federation/role-context.d.ts +5 -0
- package/dist/kernel/federation/role-context.js +45 -0
- package/dist/kernel/federation/telemetry.d.ts +21 -0
- package/dist/kernel/federation/telemetry.js +138 -0
- package/dist/kernel/federation/transport-http.d.ts +44 -0
- package/dist/kernel/federation/transport-http.js +207 -0
- package/dist/kernel/federation/types.d.ts +212 -0
- package/dist/kernel/federation/types.js +3 -0
- package/dist/kernel/free-providers.d.ts +28 -0
- package/dist/kernel/free-providers.js +162 -0
- package/dist/kernel/frontend-scoring.d.ts +21 -0
- package/dist/kernel/frontend-scoring.js +125 -0
- package/dist/kernel/index.d.ts +63 -0
- package/dist/kernel/index.js +45 -0
- package/dist/kernel/kernel.d.ts +75 -0
- package/dist/kernel/kernel.js +223 -0
- package/dist/kernel/logger.d.ts +18 -0
- package/dist/kernel/logger.js +26 -0
- package/dist/kernel/mcp/index.d.ts +9 -0
- package/dist/kernel/mcp/index.js +8 -0
- package/dist/kernel/mcp/mcp-server.d.ts +27 -0
- package/dist/kernel/mcp/mcp-server.js +178 -0
- package/dist/kernel/mission-engine.d.ts +42 -0
- package/dist/kernel/mission-engine.js +160 -0
- package/dist/kernel/orchestrator.d.ts +51 -0
- package/dist/kernel/orchestrator.js +226 -0
- package/dist/kernel/plugin-manager.d.ts +28 -0
- package/dist/kernel/plugin-manager.js +76 -0
- package/dist/kernel/plugins/browser-plugin.d.ts +22 -0
- package/dist/kernel/plugins/browser-plugin.js +34 -0
- package/dist/kernel/plugins/dashboard-plugin.d.ts +17 -0
- package/dist/kernel/plugins/dashboard-plugin.js +72 -0
- package/dist/kernel/plugins/discord-plugin.d.ts +11 -0
- package/dist/kernel/plugins/discord-plugin.js +35 -0
- package/dist/kernel/plugins/metrics-plugin.d.ts +33 -0
- package/dist/kernel/plugins/metrics-plugin.js +86 -0
- package/dist/kernel/plugins/search-plugin.d.ts +17 -0
- package/dist/kernel/plugins/search-plugin.js +20 -0
- package/dist/kernel/plugins/slack-plugin.d.ts +11 -0
- package/dist/kernel/plugins/slack-plugin.js +35 -0
- package/dist/kernel/plugins/telegram-plugin.d.ts +20 -0
- package/dist/kernel/plugins/telegram-plugin.js +122 -0
- package/dist/kernel/policies.d.ts +33 -0
- package/dist/kernel/policies.js +105 -0
- package/dist/kernel/provider-discovery.d.ts +41 -0
- package/dist/kernel/provider-discovery.js +179 -0
- package/dist/kernel/provider-manager.d.ts +38 -0
- package/dist/kernel/provider-manager.js +206 -0
- package/dist/kernel/provider-url.d.ts +18 -0
- package/dist/kernel/provider-url.js +45 -0
- package/dist/kernel/providers/gemini-provider.d.ts +43 -0
- package/dist/kernel/providers/gemini-provider.js +203 -0
- package/dist/kernel/providers/ollama-provider.d.ts +44 -0
- package/dist/kernel/providers/ollama-provider.js +241 -0
- package/dist/kernel/providers/openai-compatible-provider.d.ts +43 -0
- package/dist/kernel/providers/openai-compatible-provider.js +233 -0
- package/dist/kernel/recovery-manager.d.ts +38 -0
- package/dist/kernel/recovery-manager.js +156 -0
- package/dist/kernel/router.d.ts +44 -0
- package/dist/kernel/router.js +222 -0
- package/dist/kernel/sample-missions.d.ts +11 -0
- package/dist/kernel/sample-missions.js +132 -0
- package/dist/kernel/scheduler.d.ts +30 -0
- package/dist/kernel/scheduler.js +147 -0
- package/dist/kernel/sdk/harness-sdk.d.ts +37 -0
- package/dist/kernel/sdk/harness-sdk.js +48 -0
- package/dist/kernel/sdk/plugin-sdk.d.ts +27 -0
- package/dist/kernel/sdk/plugin-sdk.js +40 -0
- package/dist/kernel/search/search-controller.d.ts +32 -0
- package/dist/kernel/search/search-controller.js +141 -0
- package/dist/kernel/setup.d.ts +13 -0
- package/dist/kernel/setup.js +59 -0
- package/dist/kernel/types.d.ts +479 -0
- package/dist/kernel/types.js +7 -0
- package/dist/kernel/verification-engine.d.ts +33 -0
- package/dist/kernel/verification-engine.js +287 -0
- package/dist/kernel/worker-runtime.d.ts +66 -0
- package/dist/kernel/worker-runtime.js +261 -0
- package/dist/kernel/workers/browser-worker.d.ts +6 -0
- package/dist/kernel/workers/browser-worker.js +92 -0
- package/dist/kernel/workers/builtin-workers.d.ts +20 -0
- package/dist/kernel/workers/builtin-workers.js +120 -0
- package/dist/kernel/workers/research-worker.d.ts +5 -0
- package/dist/kernel/workers/research-worker.js +90 -0
- package/dist/prompt-ascii-video-animation.d.ts +2 -0
- package/dist/prompt-ascii-video-animation.js +243 -0
- package/package.json +43 -9
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** InfiniBot `nodes.status` / `node.list` response shape. */
|
|
2
|
+
export function toNodesStatus(statuses) {
|
|
3
|
+
return {
|
|
4
|
+
ts: Date.now(),
|
|
5
|
+
nodes: statuses.map(s => ({
|
|
6
|
+
nodeId: s.nodeId,
|
|
7
|
+
displayName: s.displayName,
|
|
8
|
+
platform: s.platform,
|
|
9
|
+
deviceFamily: s.deviceFamily,
|
|
10
|
+
modelIdentifier: s.modelIdentifier,
|
|
11
|
+
version: s.version,
|
|
12
|
+
caps: s.capabilities,
|
|
13
|
+
commands: s.commands,
|
|
14
|
+
connected: s.connected,
|
|
15
|
+
connectedAtMs: s.connectedAtMs,
|
|
16
|
+
paired: true,
|
|
17
|
+
// extra infinicode fields are ignored by InfiniBot but useful to richer UIs
|
|
18
|
+
role: s.role,
|
|
19
|
+
load: s.load,
|
|
20
|
+
})),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** InfiniBot `hardware.snapshot` payload (shape already matches). */
|
|
24
|
+
export function toHardwareSnapshot(nodeId, hw) {
|
|
25
|
+
return { nodeId, snapshot: hw };
|
|
26
|
+
}
|
|
27
|
+
/** Map a compact activity/routing frame to an InfiniBot-style `agent` event. */
|
|
28
|
+
export function toAgentEvent(f) {
|
|
29
|
+
if (f.t !== 'ev' && f.t !== 'rt')
|
|
30
|
+
return null;
|
|
31
|
+
const d = (f.d ?? {});
|
|
32
|
+
return {
|
|
33
|
+
source: 'infinicode',
|
|
34
|
+
nodeId: f.n,
|
|
35
|
+
seq: f.s,
|
|
36
|
+
ts: f.ts,
|
|
37
|
+
kind: d.k,
|
|
38
|
+
missionId: d.m,
|
|
39
|
+
taskId: d.task,
|
|
40
|
+
workerId: d.w,
|
|
41
|
+
data: d.d,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Build the full set of InfiniBot-bound messages for a periodic push. */
|
|
45
|
+
export function buildMoltfedBatch(statuses, selfId, hw) {
|
|
46
|
+
const out = [
|
|
47
|
+
{ method: 'nodes.status', params: toNodesStatus(statuses) },
|
|
48
|
+
];
|
|
49
|
+
if (hw)
|
|
50
|
+
out.push({ method: 'hardware.snapshot', params: toHardwareSnapshot(selfId, hw) });
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — MOLTFED / InfiniBot Gateway Connector
|
|
3
|
+
*
|
|
4
|
+
* Logs an infinicode node into an InfiniBot gateway over WebSocket and streams
|
|
5
|
+
* it onto InfiniBot's neural-mesh map + hardware panels — WITHOUT editing
|
|
6
|
+
* InfiniBot. It speaks InfiniBot's gateway protocol (observed in InfiniBot
|
|
7
|
+
* src/mcp/server.ts):
|
|
8
|
+
*
|
|
9
|
+
* 1. open ws://<host>:18789
|
|
10
|
+
* 2. optionally receive event `connect.challenge` → nonce
|
|
11
|
+
* 3. send { type:'req', id, method:'connect', params:{ minProtocol, client,
|
|
12
|
+
* auth, role, scopes, device } }
|
|
13
|
+
* 4. receive `hello-ok` → connected
|
|
14
|
+
* 5. push request frames: { type:'req', id, method, params }
|
|
15
|
+
*
|
|
16
|
+
* Auth is PLUGGABLE (token/password by default; a device-signer can be supplied
|
|
17
|
+
* for gateways that require signed device identity). Push method names are
|
|
18
|
+
* CONFIGURABLE so this adapts to a gateway's exact method registry without code
|
|
19
|
+
* changes. The WebSocket impl is injectable for testing.
|
|
20
|
+
*
|
|
21
|
+
* NOTE: verify end-to-end against a live InfiniBot gateway; the frame shapes
|
|
22
|
+
* here mirror the observed protocol but a given gateway version may differ.
|
|
23
|
+
*/
|
|
24
|
+
import type { Logger } from '../types.js';
|
|
25
|
+
import type { NodeStatus, HardwareSnapshot, StreamFrame } from './types.js';
|
|
26
|
+
/** Minimal browser-style WebSocket surface (Node global WebSocket / ws both fit). */
|
|
27
|
+
export interface WsLike {
|
|
28
|
+
send(data: string): void;
|
|
29
|
+
close(): void;
|
|
30
|
+
onopen: ((ev?: unknown) => void) | null;
|
|
31
|
+
onmessage: ((ev: {
|
|
32
|
+
data: unknown;
|
|
33
|
+
}) => void) | null;
|
|
34
|
+
onclose: ((ev?: unknown) => void) | null;
|
|
35
|
+
onerror: ((ev?: unknown) => void) | null;
|
|
36
|
+
}
|
|
37
|
+
export type WsFactory = (url: string) => WsLike;
|
|
38
|
+
export interface DeviceAuth {
|
|
39
|
+
id: string;
|
|
40
|
+
publicKey: string;
|
|
41
|
+
/** Sign the connect payload (nonce-bound). Return a signature string. */
|
|
42
|
+
sign: (payload: unknown, nonce: string | null, signedAt: number) => string;
|
|
43
|
+
}
|
|
44
|
+
export interface MoltfedConnectorOptions {
|
|
45
|
+
gatewayUrl: string;
|
|
46
|
+
token?: string;
|
|
47
|
+
password?: string;
|
|
48
|
+
client?: {
|
|
49
|
+
id: string;
|
|
50
|
+
version: string;
|
|
51
|
+
platform: string;
|
|
52
|
+
mode: string;
|
|
53
|
+
};
|
|
54
|
+
role?: string;
|
|
55
|
+
scopes?: string[];
|
|
56
|
+
device?: DeviceAuth;
|
|
57
|
+
minProtocol?: number;
|
|
58
|
+
maxProtocol?: number;
|
|
59
|
+
pushIntervalMs?: number;
|
|
60
|
+
logger: Logger;
|
|
61
|
+
/** Data sources — usually bound to a Federation instance. */
|
|
62
|
+
getNodeStatuses: () => NodeStatus[];
|
|
63
|
+
getHardware: () => HardwareSnapshot | undefined;
|
|
64
|
+
subscribeFrames: (cb: (peerId: string, f: StreamFrame) => void) => () => void;
|
|
65
|
+
/** Gateway method names (defaults chosen to match observed InfiniBot usage). */
|
|
66
|
+
methodNodes?: string;
|
|
67
|
+
methodHardware?: string;
|
|
68
|
+
methodAgent?: string;
|
|
69
|
+
/** Injectable WS constructor for tests; defaults to global WebSocket. */
|
|
70
|
+
wsFactory?: WsFactory;
|
|
71
|
+
}
|
|
72
|
+
export declare class MoltfedConnector {
|
|
73
|
+
private opts;
|
|
74
|
+
private ws;
|
|
75
|
+
private connected;
|
|
76
|
+
private nonce;
|
|
77
|
+
private connectId;
|
|
78
|
+
private connectSent;
|
|
79
|
+
private pushTimer?;
|
|
80
|
+
private connectTimer?;
|
|
81
|
+
private unsubFrames?;
|
|
82
|
+
private stopped;
|
|
83
|
+
private reconnectAttempts;
|
|
84
|
+
constructor(opts: MoltfedConnectorOptions);
|
|
85
|
+
get isConnected(): boolean;
|
|
86
|
+
start(): void;
|
|
87
|
+
private open;
|
|
88
|
+
private onMessage;
|
|
89
|
+
private buildConnectFrame;
|
|
90
|
+
private sendConnect;
|
|
91
|
+
private onConnected;
|
|
92
|
+
private pushSnapshot;
|
|
93
|
+
/** Send a request frame (fire-and-forget; gateway acks via res). */
|
|
94
|
+
request(method: string, params: unknown): void;
|
|
95
|
+
private scheduleReconnect;
|
|
96
|
+
private stopPush;
|
|
97
|
+
stop(): void;
|
|
98
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { toNodesStatus, toHardwareSnapshot, toAgentEvent } from './moltfed-adapter.js';
|
|
2
|
+
let reqSeq = 0;
|
|
3
|
+
function reqId() {
|
|
4
|
+
reqSeq = (reqSeq + 1) % Number.MAX_SAFE_INTEGER;
|
|
5
|
+
return `oc_${Date.now().toString(36)}_${reqSeq}`;
|
|
6
|
+
}
|
|
7
|
+
export class MoltfedConnector {
|
|
8
|
+
opts;
|
|
9
|
+
ws = null;
|
|
10
|
+
connected = false;
|
|
11
|
+
nonce = null;
|
|
12
|
+
connectId = null;
|
|
13
|
+
connectSent = false;
|
|
14
|
+
pushTimer;
|
|
15
|
+
connectTimer;
|
|
16
|
+
unsubFrames;
|
|
17
|
+
stopped = false;
|
|
18
|
+
reconnectAttempts = 0;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
this.opts = opts;
|
|
21
|
+
}
|
|
22
|
+
get isConnected() {
|
|
23
|
+
return this.connected;
|
|
24
|
+
}
|
|
25
|
+
start() {
|
|
26
|
+
this.stopped = false;
|
|
27
|
+
this.open();
|
|
28
|
+
}
|
|
29
|
+
open() {
|
|
30
|
+
if (this.stopped)
|
|
31
|
+
return;
|
|
32
|
+
const factory = this.opts.wsFactory
|
|
33
|
+
?? ((url) => new globalThis.WebSocket(url));
|
|
34
|
+
let socket;
|
|
35
|
+
try {
|
|
36
|
+
socket = factory(this.opts.gatewayUrl);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
this.opts.logger.warn(`[moltfed] cannot open ${this.opts.gatewayUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
40
|
+
this.scheduleReconnect();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.ws = socket;
|
|
44
|
+
this.connected = false;
|
|
45
|
+
this.connectSent = false;
|
|
46
|
+
this.connectId = null;
|
|
47
|
+
socket.onopen = () => {
|
|
48
|
+
this.opts.logger.info(`[moltfed] socket open → ${this.opts.gatewayUrl}`);
|
|
49
|
+
// If no challenge arrives, send connect after a short delay (mirrors InfiniBot).
|
|
50
|
+
this.connectTimer = setTimeout(() => this.sendConnect(), 750);
|
|
51
|
+
};
|
|
52
|
+
socket.onmessage = (ev) => this.onMessage(String(ev.data));
|
|
53
|
+
socket.onerror = () => { };
|
|
54
|
+
socket.onclose = () => {
|
|
55
|
+
if (this.ws !== socket)
|
|
56
|
+
return;
|
|
57
|
+
this.connected = false;
|
|
58
|
+
this.ws = null;
|
|
59
|
+
this.opts.logger.warn('[moltfed] gateway connection closed');
|
|
60
|
+
this.stopPush();
|
|
61
|
+
this.scheduleReconnect();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
onMessage(text) {
|
|
65
|
+
let msg;
|
|
66
|
+
try {
|
|
67
|
+
msg = JSON.parse(text);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (msg.type === 'event' && msg.event === 'connect.challenge') {
|
|
73
|
+
const payload = msg.payload;
|
|
74
|
+
if (payload?.nonce) {
|
|
75
|
+
this.nonce = payload.nonce;
|
|
76
|
+
this.sendConnect();
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const helloOk = msg.type === 'hello-ok'
|
|
81
|
+
|| (msg.type === 'res' && msg.payload?.type === 'hello-ok')
|
|
82
|
+
|| (msg.type === 'res' && msg.id === this.connectId && msg.ok === true);
|
|
83
|
+
if (helloOk) {
|
|
84
|
+
this.onConnected();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
buildConnectFrame(id) {
|
|
88
|
+
const o = this.opts;
|
|
89
|
+
const signedAt = Date.now();
|
|
90
|
+
const client = o.client ?? { id: 'infinicode', version: '2.0.0', platform: 'node', mode: 'backend' };
|
|
91
|
+
const params = {
|
|
92
|
+
minProtocol: o.minProtocol ?? 3,
|
|
93
|
+
maxProtocol: o.maxProtocol ?? 3,
|
|
94
|
+
client,
|
|
95
|
+
caps: [],
|
|
96
|
+
role: o.role ?? 'operator',
|
|
97
|
+
scopes: o.scopes ?? ['operator.admin'],
|
|
98
|
+
};
|
|
99
|
+
if (o.token || o.password) {
|
|
100
|
+
params.auth = { token: o.token, password: o.password || undefined };
|
|
101
|
+
}
|
|
102
|
+
if (o.device) {
|
|
103
|
+
params.device = {
|
|
104
|
+
id: o.device.id,
|
|
105
|
+
publicKey: o.device.publicKey,
|
|
106
|
+
signature: o.device.sign(params, this.nonce, signedAt),
|
|
107
|
+
signedAt,
|
|
108
|
+
nonce: this.nonce ?? undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return { type: 'req', id, method: 'connect', params };
|
|
112
|
+
}
|
|
113
|
+
sendConnect() {
|
|
114
|
+
if (this.connectSent || !this.ws)
|
|
115
|
+
return;
|
|
116
|
+
this.connectSent = true;
|
|
117
|
+
if (this.connectTimer) {
|
|
118
|
+
clearTimeout(this.connectTimer);
|
|
119
|
+
this.connectTimer = undefined;
|
|
120
|
+
}
|
|
121
|
+
this.connectId = reqId();
|
|
122
|
+
const frame = this.buildConnectFrame(this.connectId);
|
|
123
|
+
try {
|
|
124
|
+
this.ws.send(JSON.stringify(frame));
|
|
125
|
+
this.opts.logger.info('[moltfed] connect frame sent');
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
this.opts.logger.warn(`[moltfed] connect send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
onConnected() {
|
|
132
|
+
if (this.connected)
|
|
133
|
+
return;
|
|
134
|
+
this.connected = true;
|
|
135
|
+
this.reconnectAttempts = 0;
|
|
136
|
+
this.opts.logger.info('[moltfed] handshake ok — node is live on the gateway');
|
|
137
|
+
this.pushSnapshot(); // immediate first paint
|
|
138
|
+
this.pushTimer = setInterval(() => this.pushSnapshot(), this.opts.pushIntervalMs ?? 2000);
|
|
139
|
+
// Stream activity/routing frames as agent events for map animation.
|
|
140
|
+
this.unsubFrames = this.opts.subscribeFrames((_peer, f) => {
|
|
141
|
+
const evt = toAgentEvent(f);
|
|
142
|
+
if (evt)
|
|
143
|
+
this.request(this.opts.methodAgent ?? 'agent.event', evt);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
pushSnapshot() {
|
|
147
|
+
if (!this.connected)
|
|
148
|
+
return;
|
|
149
|
+
this.request(this.opts.methodNodes ?? 'nodes.status', toNodesStatus(this.opts.getNodeStatuses()));
|
|
150
|
+
const hw = this.opts.getHardware();
|
|
151
|
+
if (hw) {
|
|
152
|
+
const selfId = this.opts.getNodeStatuses().find(s => s.connected)?.nodeId ?? 'infinicode';
|
|
153
|
+
this.request(this.opts.methodHardware ?? 'hardware.snapshot', toHardwareSnapshot(selfId, hw));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Send a request frame (fire-and-forget; gateway acks via res). */
|
|
157
|
+
request(method, params) {
|
|
158
|
+
if (!this.ws)
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
this.ws.send(JSON.stringify({ type: 'req', id: reqId(), method, params }));
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* dropped — next push will retry state */
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
scheduleReconnect() {
|
|
168
|
+
if (this.stopped)
|
|
169
|
+
return;
|
|
170
|
+
this.reconnectAttempts++;
|
|
171
|
+
const delay = Math.min(30_000, 1000 * 2 ** Math.min(this.reconnectAttempts, 5));
|
|
172
|
+
setTimeout(() => this.open(), delay);
|
|
173
|
+
}
|
|
174
|
+
stopPush() {
|
|
175
|
+
if (this.pushTimer)
|
|
176
|
+
clearInterval(this.pushTimer);
|
|
177
|
+
this.pushTimer = undefined;
|
|
178
|
+
this.unsubFrames?.();
|
|
179
|
+
this.unsubFrames = undefined;
|
|
180
|
+
}
|
|
181
|
+
stop() {
|
|
182
|
+
this.stopped = true;
|
|
183
|
+
this.stopPush();
|
|
184
|
+
if (this.connectTimer)
|
|
185
|
+
clearTimeout(this.connectTimer);
|
|
186
|
+
try {
|
|
187
|
+
this.ws?.close();
|
|
188
|
+
}
|
|
189
|
+
catch { /* ignore */ }
|
|
190
|
+
this.ws = null;
|
|
191
|
+
this.connected = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { NodeIdentity, NodeManifest, NodeRole } from './types.js';
|
|
2
|
+
export interface IdentityOptions {
|
|
3
|
+
storageDir?: string;
|
|
4
|
+
displayName?: string;
|
|
5
|
+
role?: NodeRole;
|
|
6
|
+
}
|
|
7
|
+
/** Load the persisted identity or create + persist a fresh one. */
|
|
8
|
+
export declare function loadOrCreateIdentity(opts?: IdentityOptions): NodeIdentity;
|
|
9
|
+
export interface ManifestInputs {
|
|
10
|
+
version: string;
|
|
11
|
+
capabilities: NodeManifest['capabilities'];
|
|
12
|
+
workers: string[];
|
|
13
|
+
commands: string[];
|
|
14
|
+
providers: string[];
|
|
15
|
+
}
|
|
16
|
+
/** Build the manifest advertised to peers + the mesh map. */
|
|
17
|
+
export declare function buildManifest(identity: NodeIdentity, inputs: ManifestInputs): NodeManifest;
|
|
18
|
+
/** Total memory in bytes — used by telemetry + routing. */
|
|
19
|
+
export declare function totalMemoryBytes(): number;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Node Identity
|
|
3
|
+
*
|
|
4
|
+
* A stable, persisted identity for this device so it keeps the same nodeId
|
|
5
|
+
* across restarts (the mesh map + peers track it by id). Stored as JSON under
|
|
6
|
+
* the kernel storage dir. No crypto dependency — the optional keypair is a
|
|
7
|
+
* lightweight placeholder that app-layer auth can fill in later.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { hostname, platform, arch, cpus, totalmem } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { nanoid } from 'nanoid';
|
|
13
|
+
function defaultStorageDir() {
|
|
14
|
+
return join(process.cwd(), '.openkernel');
|
|
15
|
+
}
|
|
16
|
+
/** Best-effort device family label from platform + memory + CPU. */
|
|
17
|
+
function detectDeviceFamily() {
|
|
18
|
+
const p = platform();
|
|
19
|
+
const model = cpus()[0]?.model ?? '';
|
|
20
|
+
if (/raspberry|bcm2|cortex-a/i.test(model) || (arch().startsWith('arm') && p === 'linux')) {
|
|
21
|
+
return 'Raspberry Pi / ARM';
|
|
22
|
+
}
|
|
23
|
+
if (p === 'win32')
|
|
24
|
+
return 'Windows Workstation';
|
|
25
|
+
if (p === 'darwin')
|
|
26
|
+
return 'Mac';
|
|
27
|
+
if (p === 'linux')
|
|
28
|
+
return 'Linux';
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
/** Load the persisted identity or create + persist a fresh one. */
|
|
32
|
+
export function loadOrCreateIdentity(opts = {}) {
|
|
33
|
+
const dir = opts.storageDir ?? defaultStorageDir();
|
|
34
|
+
const file = join(dir, 'node-identity.json');
|
|
35
|
+
if (existsSync(file)) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
38
|
+
// Allow role/displayName overrides at launch without losing the id.
|
|
39
|
+
if (opts.role)
|
|
40
|
+
parsed.role = opts.role;
|
|
41
|
+
if (opts.displayName)
|
|
42
|
+
parsed.displayName = opts.displayName;
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// fall through and recreate
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const identity = {
|
|
50
|
+
nodeId: `node_${nanoid(10)}`,
|
|
51
|
+
displayName: opts.displayName ?? hostname(),
|
|
52
|
+
role: opts.role ?? 'peer',
|
|
53
|
+
createdAt: Date.now(),
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
if (!existsSync(dir))
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
writeFileSync(file, JSON.stringify(identity, null, 2));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// non-fatal: run with an ephemeral identity if disk is unavailable
|
|
62
|
+
}
|
|
63
|
+
return identity;
|
|
64
|
+
}
|
|
65
|
+
/** Build the manifest advertised to peers + the mesh map. */
|
|
66
|
+
export function buildManifest(identity, inputs) {
|
|
67
|
+
return {
|
|
68
|
+
nodeId: identity.nodeId,
|
|
69
|
+
displayName: identity.displayName,
|
|
70
|
+
role: identity.role,
|
|
71
|
+
version: inputs.version,
|
|
72
|
+
platform: platform(),
|
|
73
|
+
arch: arch(),
|
|
74
|
+
deviceFamily: detectDeviceFamily(),
|
|
75
|
+
modelIdentifier: cpus()[0]?.model,
|
|
76
|
+
capabilities: inputs.capabilities,
|
|
77
|
+
workers: inputs.workers,
|
|
78
|
+
commands: inputs.commands,
|
|
79
|
+
providers: inputs.providers,
|
|
80
|
+
startedAt: Date.now(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Total memory in bytes — used by telemetry + routing. */
|
|
84
|
+
export function totalMemoryBytes() {
|
|
85
|
+
return totalmem();
|
|
86
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Peer Mesh
|
|
3
|
+
*
|
|
4
|
+
* Tracks peers, keeps them alive with heartbeats, and (for peers we manage)
|
|
5
|
+
* holds an open SSE stream to receive their activity + telemetry. On heartbeat
|
|
6
|
+
* failure a peer is marked down and its stream reconnected — the self-healing
|
|
7
|
+
* that replaces a hand-rolled session scheduler.
|
|
8
|
+
*
|
|
9
|
+
* Connection direction honours the security model: WE open streams to peers we
|
|
10
|
+
* manage (satellites are accept-only). A satellite never dials us.
|
|
11
|
+
*/
|
|
12
|
+
import type { Logger } from '../types.js';
|
|
13
|
+
import type { FederationEnvelope, NodeIdentity, PeerInfo, StreamFrame } from './types.js';
|
|
14
|
+
import { MeshClient } from './transport-http.js';
|
|
15
|
+
export interface PeerMeshOptions {
|
|
16
|
+
self: NodeIdentity;
|
|
17
|
+
client: MeshClient;
|
|
18
|
+
logger: Logger;
|
|
19
|
+
heartbeatMs?: number;
|
|
20
|
+
/** Frames streamed from a managed peer (activity, telemetry, heartbeat). */
|
|
21
|
+
onFrame: (peerId: string, frame: StreamFrame) => void;
|
|
22
|
+
/** Called whenever the peer set or a peer's connected state changes. */
|
|
23
|
+
onPeerChange?: () => void;
|
|
24
|
+
/** Current local load 0–100, sent with heartbeats. */
|
|
25
|
+
getLoad: () => number;
|
|
26
|
+
}
|
|
27
|
+
export declare class PeerMesh {
|
|
28
|
+
private opts;
|
|
29
|
+
private peers;
|
|
30
|
+
private streams;
|
|
31
|
+
private timer?;
|
|
32
|
+
private readonly hbMs;
|
|
33
|
+
constructor(opts: PeerMeshOptions);
|
|
34
|
+
list(): PeerInfo[];
|
|
35
|
+
get(nodeId: string): PeerInfo | undefined;
|
|
36
|
+
connected(): PeerInfo[];
|
|
37
|
+
/** Discover + attach a peer at a base URL (manifest probe + open its stream). */
|
|
38
|
+
connect(url: string): Promise<PeerInfo | null>;
|
|
39
|
+
private openStream;
|
|
40
|
+
start(): void;
|
|
41
|
+
private tick;
|
|
42
|
+
/** Upsert a peer we learned about via handshake (e.g. inbound hello). */
|
|
43
|
+
note(env: FederationEnvelope, url?: string): void;
|
|
44
|
+
remove(nodeId: string): void;
|
|
45
|
+
stop(): void;
|
|
46
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { envelope } from './protocol.js';
|
|
2
|
+
export class PeerMesh {
|
|
3
|
+
opts;
|
|
4
|
+
peers = new Map();
|
|
5
|
+
streams = new Map();
|
|
6
|
+
timer;
|
|
7
|
+
hbMs;
|
|
8
|
+
constructor(opts) {
|
|
9
|
+
this.opts = opts;
|
|
10
|
+
this.hbMs = opts.heartbeatMs ?? 30_000;
|
|
11
|
+
}
|
|
12
|
+
list() {
|
|
13
|
+
return [...this.peers.values()];
|
|
14
|
+
}
|
|
15
|
+
get(nodeId) {
|
|
16
|
+
return this.peers.get(nodeId);
|
|
17
|
+
}
|
|
18
|
+
connected() {
|
|
19
|
+
return this.list().filter(p => p.connected);
|
|
20
|
+
}
|
|
21
|
+
/** Discover + attach a peer at a base URL (manifest probe + open its stream). */
|
|
22
|
+
async connect(url) {
|
|
23
|
+
try {
|
|
24
|
+
const manifest = await this.opts.client.fetchManifest(url);
|
|
25
|
+
const peer = {
|
|
26
|
+
nodeId: manifest.nodeId,
|
|
27
|
+
displayName: manifest.displayName,
|
|
28
|
+
role: manifest.role,
|
|
29
|
+
url,
|
|
30
|
+
connected: true,
|
|
31
|
+
lastSeenAt: Date.now(),
|
|
32
|
+
manifest,
|
|
33
|
+
version: manifest.version,
|
|
34
|
+
};
|
|
35
|
+
this.peers.set(peer.nodeId, peer);
|
|
36
|
+
this.openStream(peer);
|
|
37
|
+
this.opts.logger.info(`[federation] connected to ${peer.displayName} (${peer.nodeId}) @ ${url}`);
|
|
38
|
+
this.opts.onPeerChange?.();
|
|
39
|
+
return peer;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
this.opts.logger.warn(`[federation] connect failed ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
openStream(peer) {
|
|
47
|
+
this.streams.get(peer.nodeId)?.(); // close any prior
|
|
48
|
+
const stop = this.opts.client.openStream(peer.url, f => {
|
|
49
|
+
peer.lastSeenAt = Date.now();
|
|
50
|
+
// Absorb telemetry frames into the peer record for routing.
|
|
51
|
+
if (f.t === 'hw' && f.d)
|
|
52
|
+
peer.hardware = f.d;
|
|
53
|
+
this.opts.onFrame(peer.nodeId, f);
|
|
54
|
+
}, () => {
|
|
55
|
+
// stream closed — heartbeat loop will attempt to revive
|
|
56
|
+
peer.connected = false;
|
|
57
|
+
this.opts.onPeerChange?.();
|
|
58
|
+
});
|
|
59
|
+
this.streams.set(peer.nodeId, stop);
|
|
60
|
+
}
|
|
61
|
+
start() {
|
|
62
|
+
if (this.timer)
|
|
63
|
+
return;
|
|
64
|
+
this.timer = setInterval(() => void this.tick(), this.hbMs);
|
|
65
|
+
}
|
|
66
|
+
async tick() {
|
|
67
|
+
await Promise.all(this.list().map(async (peer) => {
|
|
68
|
+
const t0 = Date.now();
|
|
69
|
+
try {
|
|
70
|
+
await this.opts.client.rpc(peer.url, envelope('heartbeat', this.opts.self.nodeId, { load: this.opts.getLoad() }, { to: peer.nodeId }));
|
|
71
|
+
peer.latencyMs = Date.now() - t0;
|
|
72
|
+
peer.lastSeenAt = Date.now();
|
|
73
|
+
if (!peer.connected) {
|
|
74
|
+
peer.connected = true;
|
|
75
|
+
this.openStream(peer); // revive stream after an outage
|
|
76
|
+
this.opts.logger.info(`[federation] peer ${peer.displayName} back online`);
|
|
77
|
+
this.opts.onPeerChange?.();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
if (peer.connected) {
|
|
82
|
+
peer.connected = false;
|
|
83
|
+
this.streams.get(peer.nodeId)?.();
|
|
84
|
+
this.streams.delete(peer.nodeId);
|
|
85
|
+
this.opts.logger.warn(`[federation] peer ${peer.displayName} went offline`);
|
|
86
|
+
this.opts.onPeerChange?.();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
/** Upsert a peer we learned about via handshake (e.g. inbound hello). */
|
|
92
|
+
note(env, url) {
|
|
93
|
+
const from = env.from;
|
|
94
|
+
const existing = this.peers.get(from);
|
|
95
|
+
if (existing) {
|
|
96
|
+
existing.lastSeenAt = Date.now();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!url)
|
|
100
|
+
return;
|
|
101
|
+
void this.connect(url);
|
|
102
|
+
}
|
|
103
|
+
remove(nodeId) {
|
|
104
|
+
this.streams.get(nodeId)?.();
|
|
105
|
+
this.streams.delete(nodeId);
|
|
106
|
+
this.peers.delete(nodeId);
|
|
107
|
+
this.opts.onPeerChange?.();
|
|
108
|
+
}
|
|
109
|
+
stop() {
|
|
110
|
+
if (this.timer)
|
|
111
|
+
clearInterval(this.timer);
|
|
112
|
+
this.timer = undefined;
|
|
113
|
+
for (const stop of this.streams.values())
|
|
114
|
+
stop();
|
|
115
|
+
this.streams.clear();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type FederationEnvelope, type MessageKind, type StreamFrame } from './types.js';
|
|
2
|
+
export declare function newId(): string;
|
|
3
|
+
export declare function envelope<T>(kind: MessageKind, from: string, data?: T, opts?: {
|
|
4
|
+
to?: string;
|
|
5
|
+
re?: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
auth?: string;
|
|
8
|
+
}): FederationEnvelope<T>;
|
|
9
|
+
/** Build a reply envelope correlated to a request. */
|
|
10
|
+
export declare function reply<T>(to: FederationEnvelope, kind: MessageKind, from: string, data?: T): FederationEnvelope<T>;
|
|
11
|
+
export declare function nextSeq(): number;
|
|
12
|
+
export declare function frame(t: StreamFrame['t'], from: string, d?: unknown): StreamFrame;
|
|
13
|
+
/** Serialize a frame as an SSE `data:` block. Event name = frame type for
|
|
14
|
+
* per-type handling on the harness side. */
|
|
15
|
+
export declare function frameToSSE(f: StreamFrame): string;
|
|
16
|
+
/** Parse a raw SSE chunk buffer into complete frames. Returns leftover text. */
|
|
17
|
+
export declare function parseSSE(buffer: string): {
|
|
18
|
+
frames: StreamFrame[];
|
|
19
|
+
rest: string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Federation Protocol helpers
|
|
3
|
+
*
|
|
4
|
+
* Builders for the JSON envelope + the compact SSE stream frames. Keeping frame
|
|
5
|
+
* construction here means the transport, the event bridge, and the MOLTFED
|
|
6
|
+
* adapter all agree on one wire shape.
|
|
7
|
+
*/
|
|
8
|
+
import { nanoid } from 'nanoid';
|
|
9
|
+
import { FEDERATION_PROTOCOL_VERSION, } from './types.js';
|
|
10
|
+
export function newId() {
|
|
11
|
+
return nanoid(12);
|
|
12
|
+
}
|
|
13
|
+
export function envelope(kind, from, data, opts = {}) {
|
|
14
|
+
return {
|
|
15
|
+
v: FEDERATION_PROTOCOL_VERSION,
|
|
16
|
+
id: opts.id ?? newId(),
|
|
17
|
+
kind,
|
|
18
|
+
from,
|
|
19
|
+
to: opts.to,
|
|
20
|
+
re: opts.re,
|
|
21
|
+
ts: Date.now(),
|
|
22
|
+
auth: opts.auth,
|
|
23
|
+
data,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Build a reply envelope correlated to a request. */
|
|
27
|
+
export function reply(to, kind, from, data) {
|
|
28
|
+
return envelope(kind, from, data, { to: to.from, re: to.id });
|
|
29
|
+
}
|
|
30
|
+
// ── Stream frames ────────────────────────────────────────────────────────────
|
|
31
|
+
let seq = 0;
|
|
32
|
+
export function nextSeq() {
|
|
33
|
+
seq = (seq + 1) % Number.MAX_SAFE_INTEGER;
|
|
34
|
+
return seq;
|
|
35
|
+
}
|
|
36
|
+
export function frame(t, from, d) {
|
|
37
|
+
return { t, n: from, s: nextSeq(), ts: Date.now(), d };
|
|
38
|
+
}
|
|
39
|
+
/** Serialize a frame as an SSE `data:` block. Event name = frame type for
|
|
40
|
+
* per-type handling on the harness side. */
|
|
41
|
+
export function frameToSSE(f) {
|
|
42
|
+
return `event: ${f.t}\ndata: ${JSON.stringify(f)}\n\n`;
|
|
43
|
+
}
|
|
44
|
+
/** Parse a raw SSE chunk buffer into complete frames. Returns leftover text. */
|
|
45
|
+
export function parseSSE(buffer) {
|
|
46
|
+
const frames = [];
|
|
47
|
+
const parts = buffer.split('\n\n');
|
|
48
|
+
const rest = parts.pop() ?? '';
|
|
49
|
+
for (const block of parts) {
|
|
50
|
+
const dataLine = block.split('\n').find(l => l.startsWith('data:'));
|
|
51
|
+
if (!dataLine)
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
frames.push(JSON.parse(dataLine.slice(5).trim()));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// skip malformed frame
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { frames, rest };
|
|
61
|
+
}
|