infinicode 1.0.0 → 2.0.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 +527 -75
- package/dist/ascii-video-animation.d.ts +2 -0
- package/dist/ascii-video-animation.js +243 -0
- package/dist/cli.js +195 -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 +12 -0
- package/dist/commands/mcp.js +96 -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 +18 -0
- package/dist/commands/serve.js +127 -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 +53 -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 +126 -0
- package/dist/kernel/federation/federation.js +335 -0
- package/dist/kernel/federation/index.d.ts +31 -0
- package/dist/kernel/federation/index.js +23 -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,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
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RoleContext } from './types.js';
|
|
2
|
+
export declare function loadRole(storageDir?: string): RoleContext | null;
|
|
3
|
+
export declare function saveRole(role: Omit<RoleContext, 'updatedAt'>, storageDir?: string): RoleContext;
|
|
4
|
+
/** Render a role context as a system-preamble line for task prompts. */
|
|
5
|
+
export declare function rolePreamble(ctx: RoleContext | null): string | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Role Context
|
|
3
|
+
*
|
|
4
|
+
* A device's persistent identity + server-architecture brief. Loaded on every
|
|
5
|
+
* task run so a spawned agent is proactive and context-aware without being
|
|
6
|
+
* re-briefed ("you are RoboPanda; camera+mic on GPIO; part of the RoboPark
|
|
7
|
+
* mesh; report scene events to the hub"). Stored as JSON under the kernel dir.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
function roleFile(storageDir) {
|
|
12
|
+
return join(storageDir, 'role.json');
|
|
13
|
+
}
|
|
14
|
+
export function loadRole(storageDir = join(process.cwd(), '.openkernel')) {
|
|
15
|
+
const file = roleFile(storageDir);
|
|
16
|
+
if (!existsSync(file))
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function saveRole(role, storageDir = join(process.cwd(), '.openkernel')) {
|
|
26
|
+
const ctx = { ...role, updatedAt: Date.now() };
|
|
27
|
+
try {
|
|
28
|
+
if (!existsSync(storageDir))
|
|
29
|
+
mkdirSync(storageDir, { recursive: true });
|
|
30
|
+
writeFileSync(roleFile(storageDir), JSON.stringify(ctx, null, 2));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// non-fatal
|
|
34
|
+
}
|
|
35
|
+
return ctx;
|
|
36
|
+
}
|
|
37
|
+
/** Render a role context as a system-preamble line for task prompts. */
|
|
38
|
+
export function rolePreamble(ctx) {
|
|
39
|
+
if (!ctx)
|
|
40
|
+
return undefined;
|
|
41
|
+
const parts = [`You are ${ctx.role}.`];
|
|
42
|
+
if (ctx.architecture)
|
|
43
|
+
parts.push(ctx.architecture);
|
|
44
|
+
return parts.join(' ');
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HardwareSnapshot, NodeIdentity, NodeStatus, PeerInfo } from './types.js';
|
|
2
|
+
export declare class TelemetryCollector {
|
|
3
|
+
private lastCpu;
|
|
4
|
+
private lastNet;
|
|
5
|
+
private cpuTimes;
|
|
6
|
+
private cpuPercent;
|
|
7
|
+
/** Per-core % from a single snapshot (approximate, no per-core delta kept). */
|
|
8
|
+
private perCore;
|
|
9
|
+
private network;
|
|
10
|
+
collect(): HardwareSnapshot;
|
|
11
|
+
}
|
|
12
|
+
/** A 0–100 load figure derived from a snapshot (used by the compute router). */
|
|
13
|
+
export declare function loadFromSnapshot(hw: HardwareSnapshot | undefined): number;
|
|
14
|
+
/** Build the neutral NodeStatus entry the mesh map renders. */
|
|
15
|
+
export declare function nodeStatusFromPeer(peer: PeerInfo): NodeStatus;
|
|
16
|
+
/** Local node's own status entry. */
|
|
17
|
+
export declare function selfNodeStatus(identity: NodeIdentity, hw: HardwareSnapshot, manifest: {
|
|
18
|
+
version: string;
|
|
19
|
+
capabilities: NodeStatus['capabilities'];
|
|
20
|
+
commands: string[];
|
|
21
|
+
}): NodeStatus;
|