/** * OpenKernel — Federation Manager * * The one object that wires the mesh together: * transport (HTTP+SSE) · peer-mesh · telemetry · event-bridge · config-sync · * auto-update · compute-router · MOLTFED emit. * * Lifecycle: build with a KernelLike + a describe() for the manifest, then * start(). It stands up a mesh server, streams local activity/telemetry to * connected harnesses, dispatches tasks to peers, and answers inbound RPC * (heartbeat / task.dispatch / command / config / update). */ import type { Capability, KernelLike, Logger } from '../types.js'; import type { HardwareSnapshot, NodeIdentity, NodeManifest, NodeStatus, PeerInfo, SharedConfig, StreamFrame } from './types.js'; import { ConfigSync } from './config-sync.js'; import { AutoUpdate } from './auto-update.js'; import { ComputeRouter, type ComputeNeed } from './compute-router.js'; import { type ManifestInputs } from './node-identity.js'; import { type AgentHandle, type RoleContext, type RunRecord } from './types.js'; import { AgentBackendRegistry, type BackendId } from '../agents/index.js'; import { loadCoderConfig, type CoderConfig } from '../agents/backends/profiles.js'; export interface DispatchTask { description: string; capabilities: Capability[]; prompt: string; role?: string; context?: Record; /** When true, the receiving node auto-plans the goal into phases (a phased * build) instead of running it as one atomic task. */ autoPlan?: boolean; /** Which executor should run this on the receiving node: 'kernel' (native, * default) or an installed CLI backend ('claude' | 'codex' | 'opencode'). */ agent?: BackendId; /** Resume a specific CLI session on the receiving node instead of a fresh run. */ sessionId?: string; /** Model hint passed to a CLI backend (provider/model or a CLI-native id). */ model?: string; /** Named coder profile to run (e.g. "deep", "cheap"). */ profile?: string; /** Rotation strategy: 'auto' | 'tasktype' | 'policy' | 'roundrobin' | 'fallback'. */ strategy?: string; /** Policy tier: 'free-first' | 'cheapest' | 'fastest' | 'highest-quality' | 'balanced'. */ policy?: string; /** Task-type key for routing (e.g. 'refactor', 'docs', 'bulk'). */ taskType?: string; } export interface FederationOptions { version: string; port?: number; host?: string; token?: string; heartbeatMs?: number; telemetryMs?: number; /** Base URLs of peers to connect to on start (static seeds). */ seeds?: string[]; autoUpdate?: boolean; /** Hub/relay npm-release polling interval. Defaults to one minute. */ updatePollMs?: number; /** Satellite: how to apply an incoming shared config (register providers etc.). */ applyConfig?: (cfg: SharedConfig) => void | Promise; /** Satellite: auto-pull shared config from any hub/relay we connect to. */ autoPullConfig?: boolean; /** Satellite: how often to re-pull shared config (ms, default 30000; 0 disables). */ configPullMs?: number; /** Coder profiles + rotation config (from node config `coder`). Merged over * built-in defaults, so the mesh rotates across backends out of the box. */ coder?: Parameters[0]; /** Serve the web control dashboard at GET / and /ui (off unless enabled). */ dashboard?: boolean; /** Publish this node's shared config to the fleet (backs POST /fed/apply + * the dashboard's "Apply config to fleet" button). */ onApply?: () => Promise | unknown; /** Gate remote/dashboard slash-commands. Returns {ok:false,reason} to reject — * used to keep an unauthenticated, network-exposed node read-only. */ commandGate?: (input: string) => { ok: boolean; reason?: string; }; /** TLS material — when set, the node + dashboard are served over HTTPS. */ tls?: { cert: Buffer; key: Buffer; }; /** RoboPark scheduler base URL — proxied under /robopark/* for the dashboard. */ schedulerUrl?: string; /** Bearer token presented to the scheduler on proxied requests. */ schedulerToken?: string; /** Refuse write actions through the scheduler proxy (read-only console). */ schedulerReadOnly?: boolean; } /** Consolidated fleet view served at GET /fed/status. */ export interface FleetStatus { self: NodeManifest; role: RoleContext | null; nodes: NodeStatus[]; hardware?: HardwareSnapshot; runs: RunRecord[]; agents: AgentHandle[]; activity: Array<{ peerId: string; f: StreamFrame; }>; /** Console capabilities for the dashboard: whether control writes are allowed * on this node and whether a RoboPark scheduler is linked. */ control: { readonly: boolean; schedulerLinked: boolean; schedulerUrl?: string; }; ts: number; } export interface FederationDeps { kernel: KernelLike; identity: NodeIdentity; describe: () => ManifestInputs; logger: Logger; options: FederationOptions; /** Executors this node can run dispatched tasks on. Defaults to the native * kernel plus any installed coding CLIs (Claude Code / Codex / OpenCode). */ backends?: AgentBackendRegistry; } export declare class Federation { private deps; private server?; private client; private mesh?; private bridge?; private telemetry; private telemetryTimer?; private lastHw?; private unsub?; private lan?; /** nodeIds we're currently dialing via LAN discovery (dedup guard). */ private lanConnecting; /** nodeIds we're currently dialing back after an inbound heartbeat/hello. */ private inboundConnecting; private configPullTimer?; private updatePollTimer?; readonly configSync: ConfigSync; readonly autoUpdate: AutoUpdate; readonly compute: ComputeRouter; /** Executors available on this node (native kernel + installed CLIs). */ readonly backends: AgentBackendRegistry; /** Resolved coder profiles + rotation policy for this node. */ readonly coderConfig: CoderConfig; /** Per-node round-robin cursor for backend rotation. */ private readonly rotationState; /** Local subscribers to the merged mesh frame stream (for MCP stream_activity). */ private frameSubs; /** Recent frames for catch-up. */ private frameHistory; /** Runs received + executing on THIS node (async handoff). */ private runs; /** Runs WE dispatched → which peer holds them (for follow_task). */ private dispatchedRuns; constructor(deps: FederationDeps); get identity(): NodeIdentity; start(): Promise; private onRpc; getRole(): RoleContext | null; setRole(role: Omit): RoleContext; getRoleOf(nodeId: string): Promise; setRoleOf(nodeId: string, role: Omit): Promise; /** Execute a dispatched task on THIS node via the local kernel. */ private runTask; /** Run a dispatched task in the background, updating its RunRecord + streaming. */ private runTaskAsync; /** * Execute a dispatched task with a third-party CLI backend (Claude Code / * Codex / OpenCode) instead of the native kernel. The backend's activity * streams up the same mesh SSE feed (log/ev frames), and its native session id * is captured on the RunRecord so the run can be followed and later messaged. */ private runViaBackend; /** Stream one backend's frames up the mesh, tagged with the backend id. */ private backendFrameSink; /** Run a single backend attempt. Never throws — a missing/unavailable backend * returns a failed result so a rotation chain can fall through to the next. */ private runBackendOnce; /** Write a backend result onto the run record. */ private writeResult; /** * Resolve a rotation chain for the task and run it with fallback: try each * profile's backend in order, streaming its activity; the first `completed` * wins, otherwise fall through to the next. Composes task-type routing, policy * tiers, round-robin and fallback (see agents/backends/rotation.ts). */ private runViaBackendChain; /** * Hand a task to the best-fit connected peer (compute-routed) or a named one. * Non-blocking by default — returns a runId to follow. Pass wait to block * until the run finishes (short tasks). */ dispatch(task: DispatchTask, need?: Partial, opts?: { wait?: boolean; timeoutMs?: number; }): Promise<{ peerId: string; runId: string; record: RunRecord; } | null>; /** Runs received + executing on THIS node (async handoff), newest last. */ localRuns(): RunRecord[]; /** Runs WE dispatched to peers → { runId, peerId } for monitoring. */ outboundRuns(): Array<{ runId: string; peerId: string; }>; /** Query a dispatched run's current record (from whichever peer holds it). */ followRun(runId: string): Promise<{ record: RunRecord; frames: Array<{ peerId: string; f: StreamFrame; }>; } | null>; /** Poll a run until it finishes or the timeout elapses. */ awaitRun(runId: string, timeoutMs?: number): Promise; /** Run a slash-command on a specific peer. */ runCommandOn(nodeId: string, input: string): Promise; /** * CLI agent sessions on THIS node — any run that used a CLI backend and * captured a session id. Live while running, dormant (still resumable) once * complete. Deduped by session, newest kept, so a session that's been messaged * several times shows once. */ localAgents(): AgentHandle[]; /** All messageable CLI agent sessions across the mesh (local + peers). */ agents(): Promise; /** Resolve an address (`nodeId/sessionId`, a sessionId, or a runId) to a handle. */ private resolveAgent; /** * Message a CLI agent session — wherever it lives on the mesh. Resumes that * exact session with the text (so the agent keeps its context) and returns a * runId to follow the reply. This is how one CLI agent talks to another. */ sendTo(address: string, text: string, opts?: { wait?: boolean; timeoutMs?: number; }): Promise<{ runId: string; nodeId: string; record?: RunRecord; } | null>; /** Hub: publish shared config (providers/API keys/policy) to the fleet. */ publishConfig(cfg: Omit): SharedConfig; /** Satellite: pull shared config from a peer (usually the hub). */ pullConfig(nodeId: string): Promise; connect(url: string): Promise; /** * Learn the rest of the mesh from a hub or relay we just joined. * * Connecting only ever produced a single peer, so a node that dialled the hub * knew the hub and nothing else: every other machine on the mesh was * invisible and undispatchable, even though the hub could see them all. The * hub already publishes its full view on GET /fed/nodes, so adopt it and dial * each member directly - that is what makes the mesh transitive instead of a * collection of two-node links. */ private adoptPeersOf; /** * Register an inbound peer that dialed US (learned from its heartbeat/hello), * so dispatch works regardless of who connected first. The hub dials the * sender back (→ full manifest + stream), making a satellite that only * `--seed`s the hub visible + dispatchable. Gated: a satellite never dials * back (accept-only, never dials home). */ private learnInboundPeer; /** Connect with a longer manifest fetch timeout. Inbound dial-backs often * cross Tailscale or Windows firewall, so the default 5s is too aggressive. */ private connectWithTimeout; /** Satellite: pull shared config from a freshly-connected hub/relay. */ private maybeAutoPull; /** Satellite: re-pull from the first connected hub/relay (periodic refresh). */ private autoPullFromHub; /** Fetch npm's latest tag, announce it to satellites, then update this hub. */ private pollNpmRelease; /** Auto-discover peers over Tailscale and connect to each (skips self/known). */ discoverAndConnect(tag?: string): Promise; /** * Start zero-config LAN discovery: broadcast a beacon on the local subnet and * auto-connect to any peer we hear (no --seed / Tailscale needed). Continuous — * new nodes that come online later are picked up too. Idempotent. */ startLanDiscovery(tag?: string, discoveryPort?: number): void; /** Backend ids this node can execute tasks with ('kernel' + installed CLIs). */ availableBackends(): BackendId[]; /** * The manifest inputs augmented with `backend:` capability tags for each * installed executor. This lets the compute-router match a task that asks for * a specific agent (e.g. capabilities:['backend:claude']) to a node that * actually has that CLI — without changing the caller's describe(). */ private describeWithBackends; /** Neutral node-status list (self + peers) — feeds the mesh map. */ nodeStatuses(): NodeStatus[]; peers(): PeerInfo[]; /** Consolidated fleet view for the dashboard + `robopark fleet` (GET /fed/status). */ statusSnapshot(): FleetStatus; /** Latest local hardware snapshot (for the MOLTFED connector / panels). */ latestHardware(): HardwareSnapshot | undefined; /** Subscribe to the merged mesh frame stream (local + peer frames). */ onFrames(sub: (peerId: string, f: StreamFrame) => void): () => void; recentFrames(n?: number): Array<{ peerId: string; f: StreamFrame; }>; private ingestFrame; private emitLocal; stop(): Promise; }