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.
Files changed (157) hide show
  1. package/.opencode/plugins/home-logo-animation.tsx +120 -0
  2. package/.opencode/plugins/routing-mode-display.tsx +135 -0
  3. package/.opencode/themes/infinibot-gold.json +222 -0
  4. package/.opencode/tui.json +8 -0
  5. package/README.md +527 -75
  6. package/dist/ascii-video-animation.d.ts +2 -0
  7. package/dist/ascii-video-animation.js +243 -0
  8. package/dist/cli.js +195 -50
  9. package/dist/commands/console.d.ts +6 -0
  10. package/dist/commands/console.js +111 -0
  11. package/dist/commands/kernel-setup.d.ts +3 -0
  12. package/dist/commands/kernel-setup.js +303 -0
  13. package/dist/commands/mcp.d.ts +12 -0
  14. package/dist/commands/mcp.js +96 -0
  15. package/dist/commands/mission.d.ts +16 -0
  16. package/dist/commands/mission.js +301 -0
  17. package/dist/commands/models.d.ts +3 -1
  18. package/dist/commands/models.js +111 -55
  19. package/dist/commands/providers.d.ts +6 -0
  20. package/dist/commands/providers.js +95 -0
  21. package/dist/commands/run.d.ts +2 -1
  22. package/dist/commands/run.js +349 -59
  23. package/dist/commands/serve.d.ts +18 -0
  24. package/dist/commands/serve.js +127 -0
  25. package/dist/commands/setup.d.ts +1 -1
  26. package/dist/commands/setup.js +77 -44
  27. package/dist/commands/status.d.ts +2 -1
  28. package/dist/commands/status.js +46 -30
  29. package/dist/commands/workers.d.ts +5 -0
  30. package/dist/commands/workers.js +103 -0
  31. package/dist/kernel/autonomy/goal-loop.d.ts +49 -0
  32. package/dist/kernel/autonomy/goal-loop.js +113 -0
  33. package/dist/kernel/autonomy/index.d.ts +8 -0
  34. package/dist/kernel/autonomy/index.js +7 -0
  35. package/dist/kernel/browser/browser-controller.d.ts +57 -0
  36. package/dist/kernel/browser/browser-controller.js +175 -0
  37. package/dist/kernel/checkpoint-engine.d.ts +17 -0
  38. package/dist/kernel/checkpoint-engine.js +128 -0
  39. package/dist/kernel/command-system.d.ts +37 -0
  40. package/dist/kernel/command-system.js +239 -0
  41. package/dist/kernel/config-schema.d.ts +53 -0
  42. package/dist/kernel/config-schema.js +36 -0
  43. package/dist/kernel/event-bus.d.ts +19 -0
  44. package/dist/kernel/event-bus.js +65 -0
  45. package/dist/kernel/federation/auto-update.d.ts +36 -0
  46. package/dist/kernel/federation/auto-update.js +57 -0
  47. package/dist/kernel/federation/compute-router.d.ts +27 -0
  48. package/dist/kernel/federation/compute-router.js +44 -0
  49. package/dist/kernel/federation/config-sync.d.ts +33 -0
  50. package/dist/kernel/federation/config-sync.js +37 -0
  51. package/dist/kernel/federation/discovery.d.ts +11 -0
  52. package/dist/kernel/federation/discovery.js +44 -0
  53. package/dist/kernel/federation/event-bridge.d.ts +30 -0
  54. package/dist/kernel/federation/event-bridge.js +61 -0
  55. package/dist/kernel/federation/federation-plugin.d.ts +24 -0
  56. package/dist/kernel/federation/federation-plugin.js +35 -0
  57. package/dist/kernel/federation/federation.d.ts +126 -0
  58. package/dist/kernel/federation/federation.js +335 -0
  59. package/dist/kernel/federation/index.d.ts +31 -0
  60. package/dist/kernel/federation/index.js +23 -0
  61. package/dist/kernel/federation/moltfed-adapter.d.ts +30 -0
  62. package/dist/kernel/federation/moltfed-adapter.js +52 -0
  63. package/dist/kernel/federation/moltfed-connector.d.ts +98 -0
  64. package/dist/kernel/federation/moltfed-connector.js +193 -0
  65. package/dist/kernel/federation/node-identity.d.ts +19 -0
  66. package/dist/kernel/federation/node-identity.js +86 -0
  67. package/dist/kernel/federation/peer-mesh.d.ts +46 -0
  68. package/dist/kernel/federation/peer-mesh.js +117 -0
  69. package/dist/kernel/federation/protocol.d.ts +20 -0
  70. package/dist/kernel/federation/protocol.js +61 -0
  71. package/dist/kernel/federation/role-context.d.ts +5 -0
  72. package/dist/kernel/federation/role-context.js +45 -0
  73. package/dist/kernel/federation/telemetry.d.ts +21 -0
  74. package/dist/kernel/federation/telemetry.js +138 -0
  75. package/dist/kernel/federation/transport-http.d.ts +44 -0
  76. package/dist/kernel/federation/transport-http.js +207 -0
  77. package/dist/kernel/federation/types.d.ts +212 -0
  78. package/dist/kernel/federation/types.js +3 -0
  79. package/dist/kernel/free-providers.d.ts +28 -0
  80. package/dist/kernel/free-providers.js +162 -0
  81. package/dist/kernel/frontend-scoring.d.ts +21 -0
  82. package/dist/kernel/frontend-scoring.js +125 -0
  83. package/dist/kernel/index.d.ts +63 -0
  84. package/dist/kernel/index.js +45 -0
  85. package/dist/kernel/kernel.d.ts +75 -0
  86. package/dist/kernel/kernel.js +223 -0
  87. package/dist/kernel/logger.d.ts +18 -0
  88. package/dist/kernel/logger.js +26 -0
  89. package/dist/kernel/mcp/index.d.ts +9 -0
  90. package/dist/kernel/mcp/index.js +8 -0
  91. package/dist/kernel/mcp/mcp-server.d.ts +27 -0
  92. package/dist/kernel/mcp/mcp-server.js +178 -0
  93. package/dist/kernel/mission-engine.d.ts +42 -0
  94. package/dist/kernel/mission-engine.js +160 -0
  95. package/dist/kernel/orchestrator.d.ts +51 -0
  96. package/dist/kernel/orchestrator.js +226 -0
  97. package/dist/kernel/plugin-manager.d.ts +28 -0
  98. package/dist/kernel/plugin-manager.js +76 -0
  99. package/dist/kernel/plugins/browser-plugin.d.ts +22 -0
  100. package/dist/kernel/plugins/browser-plugin.js +34 -0
  101. package/dist/kernel/plugins/dashboard-plugin.d.ts +17 -0
  102. package/dist/kernel/plugins/dashboard-plugin.js +72 -0
  103. package/dist/kernel/plugins/discord-plugin.d.ts +11 -0
  104. package/dist/kernel/plugins/discord-plugin.js +35 -0
  105. package/dist/kernel/plugins/metrics-plugin.d.ts +33 -0
  106. package/dist/kernel/plugins/metrics-plugin.js +86 -0
  107. package/dist/kernel/plugins/search-plugin.d.ts +17 -0
  108. package/dist/kernel/plugins/search-plugin.js +20 -0
  109. package/dist/kernel/plugins/slack-plugin.d.ts +11 -0
  110. package/dist/kernel/plugins/slack-plugin.js +35 -0
  111. package/dist/kernel/plugins/telegram-plugin.d.ts +20 -0
  112. package/dist/kernel/plugins/telegram-plugin.js +122 -0
  113. package/dist/kernel/policies.d.ts +33 -0
  114. package/dist/kernel/policies.js +105 -0
  115. package/dist/kernel/provider-discovery.d.ts +41 -0
  116. package/dist/kernel/provider-discovery.js +179 -0
  117. package/dist/kernel/provider-manager.d.ts +38 -0
  118. package/dist/kernel/provider-manager.js +206 -0
  119. package/dist/kernel/provider-url.d.ts +18 -0
  120. package/dist/kernel/provider-url.js +45 -0
  121. package/dist/kernel/providers/gemini-provider.d.ts +43 -0
  122. package/dist/kernel/providers/gemini-provider.js +203 -0
  123. package/dist/kernel/providers/ollama-provider.d.ts +44 -0
  124. package/dist/kernel/providers/ollama-provider.js +241 -0
  125. package/dist/kernel/providers/openai-compatible-provider.d.ts +43 -0
  126. package/dist/kernel/providers/openai-compatible-provider.js +233 -0
  127. package/dist/kernel/recovery-manager.d.ts +38 -0
  128. package/dist/kernel/recovery-manager.js +156 -0
  129. package/dist/kernel/router.d.ts +44 -0
  130. package/dist/kernel/router.js +222 -0
  131. package/dist/kernel/sample-missions.d.ts +11 -0
  132. package/dist/kernel/sample-missions.js +132 -0
  133. package/dist/kernel/scheduler.d.ts +30 -0
  134. package/dist/kernel/scheduler.js +147 -0
  135. package/dist/kernel/sdk/harness-sdk.d.ts +37 -0
  136. package/dist/kernel/sdk/harness-sdk.js +48 -0
  137. package/dist/kernel/sdk/plugin-sdk.d.ts +27 -0
  138. package/dist/kernel/sdk/plugin-sdk.js +40 -0
  139. package/dist/kernel/search/search-controller.d.ts +32 -0
  140. package/dist/kernel/search/search-controller.js +141 -0
  141. package/dist/kernel/setup.d.ts +13 -0
  142. package/dist/kernel/setup.js +59 -0
  143. package/dist/kernel/types.d.ts +479 -0
  144. package/dist/kernel/types.js +7 -0
  145. package/dist/kernel/verification-engine.d.ts +33 -0
  146. package/dist/kernel/verification-engine.js +287 -0
  147. package/dist/kernel/worker-runtime.d.ts +66 -0
  148. package/dist/kernel/worker-runtime.js +261 -0
  149. package/dist/kernel/workers/browser-worker.d.ts +6 -0
  150. package/dist/kernel/workers/browser-worker.js +92 -0
  151. package/dist/kernel/workers/builtin-workers.d.ts +20 -0
  152. package/dist/kernel/workers/builtin-workers.js +120 -0
  153. package/dist/kernel/workers/research-worker.d.ts +5 -0
  154. package/dist/kernel/workers/research-worker.js +90 -0
  155. package/dist/prompt-ascii-video-animation.d.ts +2 -0
  156. package/dist/prompt-ascii-video-animation.js +243 -0
  157. package/package.json +43 -9
@@ -0,0 +1,19 @@
1
+ /**
2
+ * OpenKernel — Event Bus
3
+ *
4
+ * Everything emits events. Plugins subscribe.
5
+ */
6
+ import type { EventHandler, KernelEvent, KernelEventType } from './types.js';
7
+ export declare class EventBus {
8
+ private handlers;
9
+ private wildcardHandlers;
10
+ private history;
11
+ private maxHistory;
12
+ on(types: KernelEventType[], handler: EventHandler): () => void;
13
+ onAll(handler: EventHandler): () => void;
14
+ emit(event: KernelEvent): void;
15
+ private invoke;
16
+ private emitSafe;
17
+ getHistory(): KernelEvent[];
18
+ clearHistory(): void;
19
+ }
@@ -0,0 +1,65 @@
1
+ export class EventBus {
2
+ handlers = new Map();
3
+ wildcardHandlers = new Set();
4
+ history = [];
5
+ maxHistory = 1000;
6
+ on(types, handler) {
7
+ for (const type of types) {
8
+ let set = this.handlers.get(type);
9
+ if (!set) {
10
+ set = new Set();
11
+ this.handlers.set(type, set);
12
+ }
13
+ set.add(handler);
14
+ }
15
+ return () => {
16
+ for (const type of types) {
17
+ this.handlers.get(type)?.delete(handler);
18
+ }
19
+ };
20
+ }
21
+ onAll(handler) {
22
+ this.wildcardHandlers.add(handler);
23
+ return () => this.wildcardHandlers.delete(handler);
24
+ }
25
+ emit(event) {
26
+ this.history.push(event);
27
+ if (this.history.length > this.maxHistory) {
28
+ this.history.shift();
29
+ }
30
+ const typeHandlers = this.handlers.get(event.type);
31
+ if (typeHandlers) {
32
+ for (const handler of typeHandlers) {
33
+ this.invoke(handler, event);
34
+ }
35
+ }
36
+ for (const handler of this.wildcardHandlers) {
37
+ this.invoke(handler, event);
38
+ }
39
+ }
40
+ invoke(handler, event) {
41
+ try {
42
+ const result = handler(event);
43
+ if (result instanceof Promise) {
44
+ result.catch(err => this.emitSafe('LOG', { message: `Event handler error: ${err}` }));
45
+ }
46
+ }
47
+ catch (err) {
48
+ this.emitSafe('LOG', { message: `Event handler error: ${err}` });
49
+ }
50
+ }
51
+ emitSafe(type, data) {
52
+ try {
53
+ this.emit({ type, timestamp: Date.now(), data });
54
+ }
55
+ catch {
56
+ // swallow
57
+ }
58
+ }
59
+ getHistory() {
60
+ return [...this.history];
61
+ }
62
+ clearHistory() {
63
+ this.history = [];
64
+ }
65
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * OpenKernel — Auto Mesh Update
3
+ *
4
+ * Version gossip across the mesh. Each node announces its version on connect and
5
+ * heartbeat; when a node hears of a newer version from a trusted peer it can
6
+ * (optionally) self-update via the configured channel. Off by default — updating
7
+ * is opt-in per node, and only accepted from a hub/relay, never from a satellite.
8
+ */
9
+ import type { Logger } from '../types.js';
10
+ import type { NodeRole, UpdateAnnounce } from './types.js';
11
+ /** Compare two dotted versions. Returns >0 if a>b, <0 if a<b, 0 if equal. */
12
+ export declare function compareVersions(a: string, b: string): number;
13
+ export interface AutoUpdateOptions {
14
+ version: string;
15
+ channel?: UpdateAnnounce['channel'];
16
+ ref?: string;
17
+ logger: Logger;
18
+ /** When true, act on a newer announce; otherwise just log availability. */
19
+ enabled?: boolean;
20
+ /** Perform the actual update (npm i -g, git pull, download). Caller-supplied
21
+ * so the kernel never shells out unexpectedly. */
22
+ applyUpdate?: (announce: UpdateAnnounce) => Promise<void>;
23
+ }
24
+ export declare class AutoUpdate {
25
+ private opts;
26
+ private latestSeen;
27
+ constructor(opts: AutoUpdateOptions);
28
+ /** This node's announcement. */
29
+ announce(): UpdateAnnounce;
30
+ /**
31
+ * Process a peer's announce. Returns true if an update was (or would be)
32
+ * triggered. Only honours announcements from hub/relay roles.
33
+ */
34
+ onAnnounce(announce: UpdateAnnounce, fromRole: NodeRole): Promise<boolean>;
35
+ currentVersion(): string;
36
+ }
@@ -0,0 +1,57 @@
1
+ /** Compare two dotted versions. Returns >0 if a>b, <0 if a<b, 0 if equal. */
2
+ export function compareVersions(a, b) {
3
+ const pa = a.split('.').map(n => parseInt(n, 10) || 0);
4
+ const pb = b.split('.').map(n => parseInt(n, 10) || 0);
5
+ const len = Math.max(pa.length, pb.length);
6
+ for (let i = 0; i < len; i++) {
7
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
8
+ if (d !== 0)
9
+ return d;
10
+ }
11
+ return 0;
12
+ }
13
+ export class AutoUpdate {
14
+ opts;
15
+ latestSeen;
16
+ constructor(opts) {
17
+ this.opts = opts;
18
+ this.latestSeen = opts.version;
19
+ }
20
+ /** This node's announcement. */
21
+ announce() {
22
+ return {
23
+ version: this.opts.version,
24
+ channel: this.opts.channel ?? 'npm',
25
+ ref: this.opts.ref,
26
+ };
27
+ }
28
+ /**
29
+ * Process a peer's announce. Returns true if an update was (or would be)
30
+ * triggered. Only honours announcements from hub/relay roles.
31
+ */
32
+ async onAnnounce(announce, fromRole) {
33
+ if (compareVersions(announce.version, this.latestSeen) <= 0)
34
+ return false;
35
+ this.latestSeen = announce.version;
36
+ if (fromRole !== 'hub' && fromRole !== 'relay') {
37
+ this.opts.logger.info(`[federation] newer version ${announce.version} seen from ${fromRole} (ignored — not a hub/relay)`);
38
+ return false;
39
+ }
40
+ if (!this.opts.enabled) {
41
+ this.opts.logger.info(`[federation] update available: ${announce.version} (auto-update disabled — run manually)`);
42
+ return false;
43
+ }
44
+ this.opts.logger.info(`[federation] applying update → ${announce.version} via ${announce.channel}`);
45
+ try {
46
+ await this.opts.applyUpdate?.(announce);
47
+ return true;
48
+ }
49
+ catch (err) {
50
+ this.opts.logger.warn(`[federation] update failed: ${err instanceof Error ? err.message : String(err)}`);
51
+ return false;
52
+ }
53
+ }
54
+ currentVersion() {
55
+ return this.opts.version;
56
+ }
57
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * OpenKernel — Compute Router (cross-device)
3
+ *
4
+ * Given a task's capability needs, rank connected peers by how well they fit:
5
+ * capability coverage, live load (from telemetry), and latency. This is the
6
+ * east-west analog of the in-process CapabilityRouter — it decides WHICH DEVICE
7
+ * runs a task, not which model.
8
+ */
9
+ import type { Capability } from '../types.js';
10
+ import type { PeerInfo } from './types.js';
11
+ export interface ComputeCandidate {
12
+ peer: PeerInfo;
13
+ score: number;
14
+ reason: string;
15
+ }
16
+ export interface ComputeNeed {
17
+ capabilities: Capability[];
18
+ /** Prefer a specific device if named (e.g. "robopanda"). */
19
+ preferNodeId?: string;
20
+ preferName?: string;
21
+ }
22
+ export declare class ComputeRouter {
23
+ /** Rank connected peers for a task. Best first. */
24
+ rank(peers: PeerInfo[], need: ComputeNeed): ComputeCandidate[];
25
+ best(peers: PeerInfo[], need: ComputeNeed): PeerInfo | null;
26
+ private coverage;
27
+ }
@@ -0,0 +1,44 @@
1
+ import { loadFromSnapshot } from './telemetry.js';
2
+ export class ComputeRouter {
3
+ /** Rank connected peers for a task. Best first. */
4
+ rank(peers, need) {
5
+ const candidates = [];
6
+ for (const peer of peers) {
7
+ if (!peer.connected)
8
+ continue;
9
+ const caps = peer.manifest?.capabilities ?? [];
10
+ const coverage = this.coverage(caps, need.capabilities);
11
+ if (coverage === 0 && need.capabilities.length > 0)
12
+ continue;
13
+ const load = loadFromSnapshot(peer.hardware); // 0–100
14
+ const latency = peer.latencyMs ?? 500;
15
+ let score = coverage * 5 + // capability fit dominates
16
+ (1 - load / 100) * 3 + // prefer idle devices
17
+ (1 - Math.min(1, latency / 2000)); // prefer nearby devices
18
+ // Explicit device preference wins big.
19
+ if (need.preferNodeId && peer.nodeId === need.preferNodeId)
20
+ score += 100;
21
+ if (need.preferName && peer.displayName.toLowerCase() === need.preferName.toLowerCase())
22
+ score += 100;
23
+ candidates.push({
24
+ peer,
25
+ score,
26
+ reason: `cap=${coverage.toFixed(2)} load=${load} lat=${latency}ms`,
27
+ });
28
+ }
29
+ return candidates.sort((a, b) => b.score - a.score);
30
+ }
31
+ best(peers, need) {
32
+ return this.rank(peers, need)[0]?.peer ?? null;
33
+ }
34
+ coverage(have, want) {
35
+ if (want.length === 0)
36
+ return 0.5;
37
+ const set = new Set(have);
38
+ let hit = 0;
39
+ for (const c of want)
40
+ if (set.has(c))
41
+ hit++;
42
+ return hit / want.length;
43
+ }
44
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * OpenKernel — Config Sync (API / provider sharing)
3
+ *
4
+ * A hub holds a SharedConfig (cloud providers + API keys, policy, default model)
5
+ * with a monotonic revision. Satellites request it on connect and re-apply only
6
+ * when the revision changes — so you set API keys once on the hub and the whole
7
+ * fleet inherits them, no per-device key management.
8
+ *
9
+ * Sensitive: the shared config carries API keys. It only travels over the
10
+ * admin-opened, optionally-tokened transport, and satellites persist it locally
11
+ * via the apply callback (which the federation manager wires to provider
12
+ * registration + the local config file).
13
+ */
14
+ import type { Logger } from '../types.js';
15
+ import type { SharedConfig } from './types.js';
16
+ export interface ConfigSyncOptions {
17
+ logger: Logger;
18
+ /** Apply an incoming shared config on a satellite (register providers, etc.). */
19
+ apply?: (cfg: SharedConfig) => void | Promise<void>;
20
+ }
21
+ export declare class ConfigSync {
22
+ private opts;
23
+ private shared;
24
+ private appliedRevision;
25
+ constructor(opts: ConfigSyncOptions);
26
+ /** Hub: publish a new shared config (bumps revision). */
27
+ publish(cfg: Omit<SharedConfig, 'revision' | 'updatedAt'>): SharedConfig;
28
+ /** Hub: current offer to satisfy a config.request. */
29
+ offer(): SharedConfig;
30
+ /** Satellite: receive a hub's offer; apply only if newer than what we have. */
31
+ receive(cfg: SharedConfig): Promise<boolean>;
32
+ currentRevision(): number;
33
+ }
@@ -0,0 +1,37 @@
1
+ export class ConfigSync {
2
+ opts;
3
+ shared = { revision: 0, updatedAt: Date.now() };
4
+ appliedRevision = -1;
5
+ constructor(opts) {
6
+ this.opts = opts;
7
+ }
8
+ /** Hub: publish a new shared config (bumps revision). */
9
+ publish(cfg) {
10
+ this.shared = { ...cfg, revision: this.shared.revision + 1, updatedAt: Date.now() };
11
+ this.opts.logger.info(`[federation] shared config published (rev ${this.shared.revision})`);
12
+ return this.shared;
13
+ }
14
+ /** Hub: current offer to satisfy a config.request. */
15
+ offer() {
16
+ return this.shared;
17
+ }
18
+ /** Satellite: receive a hub's offer; apply only if newer than what we have. */
19
+ async receive(cfg) {
20
+ if (cfg.revision <= this.appliedRevision)
21
+ return false;
22
+ this.shared = cfg;
23
+ this.appliedRevision = cfg.revision;
24
+ try {
25
+ await this.opts.apply?.(cfg);
26
+ this.opts.logger.info(`[federation] applied shared config (rev ${cfg.revision})`);
27
+ return true;
28
+ }
29
+ catch (err) {
30
+ this.opts.logger.warn(`[federation] apply config failed: ${err instanceof Error ? err.message : String(err)}`);
31
+ return false;
32
+ }
33
+ }
34
+ currentRevision() {
35
+ return this.shared.revision;
36
+ }
37
+ }
@@ -0,0 +1,11 @@
1
+ import type { Logger } from '../types.js';
2
+ export interface DiscoverOptions {
3
+ port: number;
4
+ /** Only include peers carrying this Tailscale ACL tag (e.g. "tag:robopark"). */
5
+ tag?: string;
6
+ logger?: Logger;
7
+ }
8
+ /** Return mesh base URLs (http://ip:port) for online Tailscale peers. */
9
+ export declare function discoverTailscalePeers(opts: DiscoverOptions): Promise<string[]>;
10
+ /** True if the Tailscale CLI is available on this host. */
11
+ export declare function tailscaleAvailable(): Promise<boolean>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * OpenKernel — Peer Discovery
3
+ *
4
+ * Tailscale-based auto-discovery so `serve`/`mcp` find peers without --seed.
5
+ * Runs `tailscale status --json` and returns candidate mesh base URLs for
6
+ * online peers. Best-effort: if the Tailscale CLI is absent, returns []. LAN
7
+ * mDNS can be added later; Tailscale covers the LAN+remote mix directly.
8
+ */
9
+ import { execa } from 'execa';
10
+ /** Return mesh base URLs (http://ip:port) for online Tailscale peers. */
11
+ export async function discoverTailscalePeers(opts) {
12
+ let status;
13
+ try {
14
+ const { stdout } = await execa('tailscale', ['status', '--json'], { timeout: 5000 });
15
+ status = JSON.parse(stdout);
16
+ }
17
+ catch (err) {
18
+ opts.logger?.debug?.(`[federation] tailscale discovery unavailable: ${err instanceof Error ? err.message : String(err)}`);
19
+ return [];
20
+ }
21
+ const urls = [];
22
+ for (const peer of Object.values(status.Peer ?? {})) {
23
+ if (!peer.Online)
24
+ continue;
25
+ if (opts.tag && !(peer.Tags ?? []).includes(opts.tag))
26
+ continue;
27
+ const ip = (peer.TailscaleIPs ?? []).find(a => a.includes('.')); // prefer IPv4
28
+ if (!ip)
29
+ continue;
30
+ urls.push(`http://${ip}:${opts.port}`);
31
+ }
32
+ opts.logger?.info?.(`[federation] tailscale discovery found ${urls.length} peer(s)`);
33
+ return urls;
34
+ }
35
+ /** True if the Tailscale CLI is available on this host. */
36
+ export async function tailscaleAvailable() {
37
+ try {
38
+ await execa('tailscale', ['version'], { timeout: 3000 });
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * OpenKernel — Event Bridge
3
+ *
4
+ * Taps the kernel EventBus (via subscribeAll) and turns local events into
5
+ * compact StreamFrames the mesh broadcasts to connected harnesses. Tuned for
6
+ * animation: lifecycle events flow immediately (so the map reacts instantly),
7
+ * while noisy LOG/PROVIDER_HEALTH chatter is coalesced on a short flush timer to
8
+ * keep the byte-rate low.
9
+ */
10
+ import type { KernelEvent } from '../types.js';
11
+ import type { StreamFrame } from './types.js';
12
+ export interface EventBridgeOptions {
13
+ nodeId: string;
14
+ /** Push a frame onto the mesh (MeshServer.broadcast). */
15
+ emit: (f: StreamFrame) => void;
16
+ flushMs?: number;
17
+ }
18
+ export declare class EventBridge {
19
+ private opts;
20
+ private buffer;
21
+ private timer?;
22
+ constructor(opts: EventBridgeOptions);
23
+ /** Compact an event to a tiny payload for the wire. */
24
+ private compact;
25
+ /** Handle a kernel event (wire this to kernel.subscribeAll). */
26
+ handle(e: KernelEvent): void;
27
+ private scheduleFlush;
28
+ private flush;
29
+ stop(): void;
30
+ }
@@ -0,0 +1,61 @@
1
+ import { frame } from './protocol.js';
2
+ /** Events that should render instantly (map animation triggers). */
3
+ const IMMEDIATE = new Set([
4
+ 'MISSION_STARTED', 'MISSION_COMPLETED', 'MISSION_FAILED',
5
+ 'TASK_STARTED', 'TASK_COMPLETED', 'TASK_FAILED',
6
+ 'WORKER_SPAWNED', 'WORKER_EXITED', 'ROUTING_DECISION', 'RECOVERY', 'NOTIFICATION',
7
+ ]);
8
+ /** High-frequency, low-value chatter to coalesce. */
9
+ const COALESCE = new Set(['LOG', 'PROVIDER_HEALTH']);
10
+ export class EventBridge {
11
+ opts;
12
+ buffer = [];
13
+ timer;
14
+ constructor(opts) {
15
+ this.opts = opts;
16
+ }
17
+ /** Compact an event to a tiny payload for the wire. */
18
+ compact(e) {
19
+ return {
20
+ k: e.type,
21
+ m: e.missionId,
22
+ task: e.taskId,
23
+ w: e.workerId,
24
+ d: e.data,
25
+ };
26
+ }
27
+ /** Handle a kernel event (wire this to kernel.subscribeAll). */
28
+ handle(e) {
29
+ if (COALESCE.has(e.type)) {
30
+ this.buffer.push(e);
31
+ this.scheduleFlush();
32
+ return;
33
+ }
34
+ if (IMMEDIATE.has(e.type)) {
35
+ const t = e.type === 'ROUTING_DECISION' ? 'rt' : 'ev';
36
+ this.opts.emit(frame(t, this.opts.nodeId, this.compact(e)));
37
+ }
38
+ // other events are dropped from the stream (still available via history)
39
+ }
40
+ scheduleFlush() {
41
+ if (this.timer)
42
+ return;
43
+ this.timer = setTimeout(() => this.flush(), this.opts.flushMs ?? 250);
44
+ }
45
+ flush() {
46
+ this.timer = undefined;
47
+ if (this.buffer.length === 0)
48
+ return;
49
+ const batch = this.buffer.splice(0, this.buffer.length);
50
+ // One coalesced 'log' frame carrying a count + the last few lines.
51
+ this.opts.emit(frame('log', this.opts.nodeId, {
52
+ count: batch.length,
53
+ tail: batch.slice(-5).map(e => ({ k: e.type, d: e.data })),
54
+ }));
55
+ }
56
+ stop() {
57
+ if (this.timer)
58
+ clearTimeout(this.timer);
59
+ this.flush();
60
+ }
61
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * OpenKernel — Federation Plugin
3
+ *
4
+ * Thin wrapper so a harness can enable the mesh via kernel.registerPlugin(...).
5
+ * The Federation manager already subscribes to events through the kernel it's
6
+ * given; this plugin just owns its lifecycle (start on register, stop on
7
+ * destroy) and exposes the running manager for callers that want it.
8
+ */
9
+ import type { Logger, Plugin } from '../types.js';
10
+ import { Federation, type FederationOptions } from './federation.js';
11
+ import { type ManifestInputs } from './node-identity.js';
12
+ import type { NodeRole } from './types.js';
13
+ export interface FederationPluginOptions extends FederationOptions {
14
+ role?: NodeRole;
15
+ displayName?: string;
16
+ storageDir?: string;
17
+ logger?: Logger;
18
+ /** Supplies the manifest inputs (caps/workers/commands/providers). */
19
+ describe: () => ManifestInputs;
20
+ }
21
+ export interface FederationPlugin extends Plugin {
22
+ readonly federation: Federation;
23
+ }
24
+ export declare function createFederationPlugin(opts: FederationPluginOptions): FederationPlugin;
@@ -0,0 +1,35 @@
1
+ import { Federation } from './federation.js';
2
+ import { loadOrCreateIdentity } from './node-identity.js';
3
+ export function createFederationPlugin(opts) {
4
+ const identity = loadOrCreateIdentity({
5
+ storageDir: opts.storageDir,
6
+ displayName: opts.displayName,
7
+ role: opts.role,
8
+ });
9
+ let federation;
10
+ return {
11
+ get federation() {
12
+ if (!federation)
13
+ throw new Error('federation not started');
14
+ return federation;
15
+ },
16
+ manifest: {
17
+ name: 'federation',
18
+ version: '0.1.0',
19
+ description: 'Peer-to-peer device mesh (LAN + Tailscale) — spawn, telemetry, config sync',
20
+ },
21
+ async onRegister(kernel) {
22
+ const logger = opts.logger ?? {
23
+ debug: () => { },
24
+ info: (m, ...a) => console.error('[federation]', m, ...a),
25
+ warn: (m, ...a) => console.error('[federation]', m, ...a),
26
+ error: (m, ...a) => console.error('[federation]', m, ...a),
27
+ };
28
+ federation = new Federation({ kernel, identity, describe: opts.describe, logger, options: opts });
29
+ await federation.start();
30
+ },
31
+ async onDestroy() {
32
+ await federation?.stop();
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * OpenKernel — Federation Manager
3
+ *
4
+ * The one object that wires the mesh together:
5
+ * transport (HTTP+SSE) · peer-mesh · telemetry · event-bridge · config-sync ·
6
+ * auto-update · compute-router · MOLTFED emit.
7
+ *
8
+ * Lifecycle: build with a KernelLike + a describe() for the manifest, then
9
+ * start(). It stands up a mesh server, streams local activity/telemetry to
10
+ * connected harnesses, dispatches tasks to peers, and answers inbound RPC
11
+ * (heartbeat / task.dispatch / command / config / update).
12
+ */
13
+ import type { Capability, KernelLike, Logger } from '../types.js';
14
+ import type { HardwareSnapshot, NodeIdentity, NodeStatus, SharedConfig, StreamFrame } from './types.js';
15
+ import { ConfigSync } from './config-sync.js';
16
+ import { AutoUpdate } from './auto-update.js';
17
+ import { ComputeRouter, type ComputeNeed } from './compute-router.js';
18
+ import { type ManifestInputs } from './node-identity.js';
19
+ import { type RoleContext, type RunRecord } from './types.js';
20
+ export interface DispatchTask {
21
+ description: string;
22
+ capabilities: Capability[];
23
+ prompt: string;
24
+ role?: string;
25
+ context?: Record<string, unknown>;
26
+ }
27
+ export interface FederationOptions {
28
+ version: string;
29
+ port?: number;
30
+ host?: string;
31
+ token?: string;
32
+ heartbeatMs?: number;
33
+ telemetryMs?: number;
34
+ /** Base URLs of peers to connect to on start (static seeds). */
35
+ seeds?: string[];
36
+ autoUpdate?: boolean;
37
+ /** Satellite: how to apply an incoming shared config (register providers etc.). */
38
+ applyConfig?: (cfg: SharedConfig) => void | Promise<void>;
39
+ }
40
+ export interface FederationDeps {
41
+ kernel: KernelLike;
42
+ identity: NodeIdentity;
43
+ describe: () => ManifestInputs;
44
+ logger: Logger;
45
+ options: FederationOptions;
46
+ }
47
+ export declare class Federation {
48
+ private deps;
49
+ private server?;
50
+ private client;
51
+ private mesh?;
52
+ private bridge?;
53
+ private telemetry;
54
+ private telemetryTimer?;
55
+ private lastHw?;
56
+ private unsub?;
57
+ readonly configSync: ConfigSync;
58
+ readonly autoUpdate: AutoUpdate;
59
+ readonly compute: ComputeRouter;
60
+ /** Local subscribers to the merged mesh frame stream (for MCP stream_activity). */
61
+ private frameSubs;
62
+ /** Recent frames for catch-up. */
63
+ private frameHistory;
64
+ /** Runs received + executing on THIS node (async handoff). */
65
+ private runs;
66
+ /** Runs WE dispatched → which peer holds them (for follow_task). */
67
+ private dispatchedRuns;
68
+ constructor(deps: FederationDeps);
69
+ get identity(): NodeIdentity;
70
+ start(): Promise<void>;
71
+ private onRpc;
72
+ getRole(): RoleContext | null;
73
+ setRole(role: Omit<RoleContext, 'updatedAt'>): RoleContext;
74
+ getRoleOf(nodeId: string): Promise<RoleContext | null>;
75
+ setRoleOf(nodeId: string, role: Omit<RoleContext, 'updatedAt'>): Promise<RoleContext>;
76
+ /** Execute a dispatched task on THIS node via the local kernel. */
77
+ private runTask;
78
+ /** Run a dispatched task in the background, updating its RunRecord + streaming. */
79
+ private runTaskAsync;
80
+ /**
81
+ * Hand a task to the best-fit connected peer (compute-routed) or a named one.
82
+ * Non-blocking by default — returns a runId to follow. Pass wait to block
83
+ * until the run finishes (short tasks).
84
+ */
85
+ dispatch(task: DispatchTask, need?: Partial<ComputeNeed>, opts?: {
86
+ wait?: boolean;
87
+ timeoutMs?: number;
88
+ }): Promise<{
89
+ peerId: string;
90
+ runId: string;
91
+ record: RunRecord;
92
+ } | null>;
93
+ /** Query a dispatched run's current record (from whichever peer holds it). */
94
+ followRun(runId: string): Promise<{
95
+ record: RunRecord;
96
+ frames: Array<{
97
+ peerId: string;
98
+ f: StreamFrame;
99
+ }>;
100
+ } | null>;
101
+ /** Poll a run until it finishes or the timeout elapses. */
102
+ awaitRun(runId: string, timeoutMs?: number): Promise<RunRecord>;
103
+ /** Run a slash-command on a specific peer. */
104
+ runCommandOn(nodeId: string, input: string): Promise<unknown>;
105
+ /** Hub: publish shared config (providers/API keys/policy) to the fleet. */
106
+ publishConfig(cfg: Omit<SharedConfig, 'revision' | 'updatedAt'>): SharedConfig;
107
+ /** Satellite: pull shared config from a peer (usually the hub). */
108
+ pullConfig(nodeId: string): Promise<boolean>;
109
+ connect(url: string): Promise<import("./types.js").PeerInfo | null>;
110
+ /** Auto-discover peers over Tailscale and connect to each (skips self/known). */
111
+ discoverAndConnect(tag?: string): Promise<number>;
112
+ /** Neutral node-status list (self + peers) — feeds the mesh map. */
113
+ nodeStatuses(): NodeStatus[];
114
+ peers(): import("./types.js").PeerInfo[];
115
+ /** Latest local hardware snapshot (for the MOLTFED connector / panels). */
116
+ latestHardware(): HardwareSnapshot | undefined;
117
+ /** Subscribe to the merged mesh frame stream (local + peer frames). */
118
+ onFrames(sub: (peerId: string, f: StreamFrame) => void): () => void;
119
+ recentFrames(n?: number): Array<{
120
+ peerId: string;
121
+ f: StreamFrame;
122
+ }>;
123
+ private ingestFrame;
124
+ private emitLocal;
125
+ stop(): Promise<void>;
126
+ }