@theaiinc/yggdrasil-ratatoskr 0.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/dist/src/index.d.ts +12 -0
  4. package/dist/src/index.d.ts.map +1 -0
  5. package/dist/src/index.js +11 -0
  6. package/dist/src/index.js.map +1 -0
  7. package/dist/src/ratatoskr.d.ts +71 -0
  8. package/dist/src/ratatoskr.d.ts.map +1 -0
  9. package/dist/src/ratatoskr.js +179 -0
  10. package/dist/src/ratatoskr.js.map +1 -0
  11. package/dist/src/runner.d.ts +3 -0
  12. package/dist/src/runner.d.ts.map +1 -0
  13. package/dist/src/runner.js +44 -0
  14. package/dist/src/runner.js.map +1 -0
  15. package/dist/src/services/endpoint-detector.d.ts +49 -0
  16. package/dist/src/services/endpoint-detector.d.ts.map +1 -0
  17. package/dist/src/services/endpoint-detector.js +105 -0
  18. package/dist/src/services/endpoint-detector.js.map +1 -0
  19. package/dist/src/services/health-monitor.d.ts +21 -0
  20. package/dist/src/services/health-monitor.d.ts.map +1 -0
  21. package/dist/src/services/health-monitor.js +40 -0
  22. package/dist/src/services/health-monitor.js.map +1 -0
  23. package/dist/src/services/heartbeat-sender.d.ts +29 -0
  24. package/dist/src/services/heartbeat-sender.d.ts.map +1 -0
  25. package/dist/src/services/heartbeat-sender.js +61 -0
  26. package/dist/src/services/heartbeat-sender.js.map +1 -0
  27. package/dist/src/services/lease-manager.d.ts +35 -0
  28. package/dist/src/services/lease-manager.d.ts.map +1 -0
  29. package/dist/src/services/lease-manager.js +48 -0
  30. package/dist/src/services/lease-manager.js.map +1 -0
  31. package/dist/src/services/registrar.d.ts +50 -0
  32. package/dist/src/services/registrar.d.ts.map +1 -0
  33. package/dist/src/services/registrar.js +102 -0
  34. package/dist/src/services/registrar.js.map +1 -0
  35. package/dist/src/services/retry-manager.d.ts +33 -0
  36. package/dist/src/services/retry-manager.d.ts.map +1 -0
  37. package/dist/src/services/retry-manager.js +58 -0
  38. package/dist/src/services/retry-manager.js.map +1 -0
  39. package/dist/src/transports/http-transport.d.ts +33 -0
  40. package/dist/src/transports/http-transport.d.ts.map +1 -0
  41. package/dist/src/transports/http-transport.js +53 -0
  42. package/dist/src/transports/http-transport.js.map +1 -0
  43. package/dist/src/transports/transport.d.ts +2 -0
  44. package/dist/src/transports/transport.d.ts.map +1 -0
  45. package/dist/src/transports/transport.js +2 -0
  46. package/dist/src/transports/transport.js.map +1 -0
  47. package/dist/src/types/index.d.ts +105 -0
  48. package/dist/src/types/index.d.ts.map +1 -0
  49. package/dist/src/types/index.js +10 -0
  50. package/dist/src/types/index.js.map +1 -0
  51. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 The AI Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @theaiinc/yggdrasil-ratatoskr
2
+
3
+ Lightweight discovery and heartbeat daemon for Yggdrasil runner registration.
4
+
5
+ Ratatoskr runs alongside an agent runner and continuously informs Yggdrasil about:
6
+
7
+ - Runner availability
8
+ - Network endpoints
9
+ - Capabilities
10
+ - Health status
11
+ - IP changes
12
+ - Shutdown events
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @theaiinc/yggdrasil-ratatoskr
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { Ratatoskr } from '@theaiinc/yggdrasil-ratatoskr';
24
+
25
+ const ratatoskr = new Ratatoskr({
26
+ yggdrasilUrl: 'http://localhost:4000',
27
+ });
28
+
29
+ await ratatoskr.start();
30
+ ```
31
+
32
+ Within seconds, Yggdrasil automatically knows that the runner exists, where it lives, what it can do, and whether it is healthy.
33
+
34
+ ## Advanced Usage
35
+
36
+ ```typescript
37
+ import { Ratatoskr } from '@theaiinc/yggdrasil-ratatoskr';
38
+
39
+ const ratatoskr = new Ratatoskr({
40
+ runnerId: 'runner-a',
41
+ name: 'MacBook Pro',
42
+ yggdrasilUrl: 'http://yggdrasil.prod:4000',
43
+ capabilities: ['browser', 'computer-use', 'llm'],
44
+ heartbeatInterval: 30,
45
+ leaseTtl: 60,
46
+ detectPublicIp: false,
47
+ endpointProvider: async () => {
48
+ return 'http://192.168.1.5:8080';
49
+ },
50
+ healthProvider: async () => {
51
+ return {
52
+ status: 'healthy',
53
+ };
54
+ },
55
+ });
56
+
57
+ await ratatoskr.start();
58
+ ```
59
+
60
+ ## Configuration
61
+
62
+ | Option | Type | Default | Description |
63
+ |--------|------|---------|-------------|
64
+ | `runnerId` | `string` | Auto-generated | Unique runner identifier |
65
+ | `name` | `string` | `'unknown'` | Human-readable runner name |
66
+ | `yggdrasilUrl` | `string` | (required) | Yggdrasil server URL |
67
+ | `capabilities` | `string[]` | `[]` | List of runner capabilities |
68
+ | `heartbeatInterval` | `number` | `30` | Heartbeat interval in seconds |
69
+ | `leaseTtl` | `number` | `60` | Lease TTL in seconds |
70
+ | `detectLocalIp` | `boolean` | `true` | Auto-detect local IP |
71
+ | `detectPublicIp` | `boolean` | `false` | Auto-detect public IP |
72
+ | `endpointProvider` | `() => Promise<string>` | `undefined` | Custom endpoint resolver |
73
+ | `healthProvider` | `() => Promise<HealthResult>` | `undefined` | Custom health check |
74
+ | `labels` | `Record<string, string>` | `{}` | Additional labels |
75
+ | `metadata` | `Record<string, unknown>` | `{}` | Additional metadata |
76
+
77
+ ## Architecture
78
+
79
+ ```
80
+ yggdrasil-ratatoskr
81
+
82
+ ├── ratatoskr.ts # Main entry point
83
+ ├── types/ # TypeScript interfaces and enums
84
+ ├── transports/ # Transport abstraction (HTTP, WebSocket, etc.)
85
+ │ └── http-transport.ts # HTTP transport implementation
86
+ └── services/
87
+ ├── registrar.ts # Runner registration lifecycle
88
+ ├── heartbeat-sender.ts # Periodic heartbeat sender
89
+ ├── endpoint-detector.ts # IP/hostname change detection
90
+ ├── health-monitor.ts # Health check orchestration
91
+ ├── lease-manager.ts # Lease expiry tracking
92
+ └── retry-manager.ts # Exponential backoff retry
93
+ ```
94
+
95
+ ## How It Works
96
+
97
+ 1. **`ratatoskr.start()`** — Registers the runner with Yggdrasil, begins heartbeats, starts monitoring IP and lease expiry, and registers shutdown handlers.
98
+ 2. **Heartbeats** — Sent every 30 seconds (configurable) to confirm the runner is alive.
99
+ 3. **Lease** — Each registration has a 60-second TTL. If Yggdrasil misses 2 heartbeats (~60s), the runner is marked `offline`.
100
+ 4. **IP Changes** — Detected every 10 seconds; if the local IP changes, Yggdrasil is notified via `POST /runners/update`.
101
+ 5. **Graceful Shutdown** — On SIGTERM/SIGINT, Ratatoskr deregisters the runner with `POST /runners/offline`.
102
+
103
+ ## Reliability
104
+
105
+ Ratatoskr is designed to survive:
106
+
107
+ - Temporary network outages
108
+ - Yggdrasil restarts
109
+ - IP changes
110
+ - Laptop sleep/wake cycles
111
+ - Docker container restarts
112
+
113
+ It uses exponential backoff for retries, persistent runner IDs, and automatic re-registration.
114
+
115
+ ## API Endpoints (Yggdrasil)
116
+
117
+ Ratatoskr expects these endpoints on the Yggdrasil server:
118
+
119
+ | Method | Path | Purpose |
120
+ |--------|------|---------|
121
+ | `POST` | `/runners/register` | Register a new runner |
122
+ | `POST` | `/runners/heartbeat` | Send a heartbeat |
123
+ | `POST` | `/runners/update` | Update runner endpoint |
124
+ | `POST` | `/runners/offline` | Deregister a runner |
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,12 @@
1
+ export { Ratatoskr } from './ratatoskr.js';
2
+ export { HttpTransport } from './transports/http-transport.js';
3
+ export { EndpointDetector } from './services/endpoint-detector.js';
4
+ export { HealthMonitor } from './services/health-monitor.js';
5
+ export { HeartbeatSender } from './services/heartbeat-sender.js';
6
+ export { LeaseManager } from './services/lease-manager.js';
7
+ export { Registrar } from './services/registrar.js';
8
+ export { RetryManager } from './services/retry-manager.js';
9
+ export type { Transport } from './types/index.js';
10
+ export type { RatatoskrConfig, RatatoskrState, RunnerRegistration, HeartbeatPayload, EndpointUpdatePayload, DeregisterPayload, HealthResult, } from './types/index.js';
11
+ export { RunnerHealth } from './types/index.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAG3D,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EACV,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,EACjB,YAAY,GACb,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,11 @@
1
+ export { Ratatoskr } from './ratatoskr.js';
2
+ export { HttpTransport } from './transports/http-transport.js';
3
+ export { EndpointDetector } from './services/endpoint-detector.js';
4
+ export { HealthMonitor } from './services/health-monitor.js';
5
+ export { HeartbeatSender } from './services/heartbeat-sender.js';
6
+ export { LeaseManager } from './services/lease-manager.js';
7
+ export { Registrar } from './services/registrar.js';
8
+ export { RetryManager } from './services/retry-manager.js';
9
+ // Export enum
10
+ export { RunnerHealth } from './types/index.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAc3D,cAAc;AACd,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,71 @@
1
+ import type { RatatoskrConfig, RatatoskrState, HealthResult } from './types/index.js';
2
+ /**
3
+ * Ratatoskr — Lightweight discovery and heartbeat daemon.
4
+ *
5
+ * Runs alongside an agent runner and continuously informs Yggdrasil about
6
+ * runner availability, network endpoints, capabilities, health status,
7
+ * IP changes, and shutdown events.
8
+ */
9
+ export declare class Ratatoskr {
10
+ private readonly config;
11
+ private readonly state;
12
+ private readonly transport;
13
+ private readonly endpointDetector;
14
+ private readonly healthMonitor;
15
+ private readonly leaseManager;
16
+ private readonly retryManager;
17
+ private readonly registrar;
18
+ private readonly heartbeatSender;
19
+ private endpointCheckTimer;
20
+ private leaseCheckTimer;
21
+ private shutdownHandlers;
22
+ private started;
23
+ constructor(config: RatatoskrConfig);
24
+ /**
25
+ * Start the Ratatoskr daemon.
26
+ *
27
+ * 1. Registers the runner with Yggdrasil
28
+ * 2. Begins sending heartbeats
29
+ * 3. Monitors for IP changes
30
+ * 4. Monitors lease expiry for re-registration
31
+ * 5. Registers shutdown handlers for graceful deregistration
32
+ */
33
+ start(): Promise<void>;
34
+ /**
35
+ * Stop the Ratatoskr daemon and deregister from Yggdrasil.
36
+ */
37
+ stop(): Promise<void>;
38
+ /**
39
+ * Set a custom health provider.
40
+ */
41
+ setHealthProvider(provider: () => Promise<HealthResult>): void;
42
+ /**
43
+ * Returns the current runner state.
44
+ */
45
+ getState(): Readonly<RatatoskrState>;
46
+ /**
47
+ * Returns whether the daemon is currently running.
48
+ */
49
+ isRunning(): boolean;
50
+ /**
51
+ * Returns whether the runner is registered with Yggdrasil.
52
+ */
53
+ isRegistered(): boolean;
54
+ /**
55
+ * Check for endpoint changes and notify Yggdrasil if detected.
56
+ */
57
+ private checkEndpoint;
58
+ /**
59
+ * Register handlers for SIGTERM and SIGINT.
60
+ */
61
+ private registerShutdownHandlers;
62
+ /**
63
+ * Resolve the provided config with defaults.
64
+ */
65
+ private resolveConfig;
66
+ /**
67
+ * Initialize the internal state.
68
+ */
69
+ private initializeState;
70
+ }
71
+ //# sourceMappingURL=ratatoskr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ratatoskr.d.ts","sourceRoot":"","sources":["../../src/ratatoskr.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,YAAY,EACb,MAAM,kBAAkB,CAAC;AAW1B;;;;;;GAMG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,kBAAkB,CAA6C;IACvE,OAAO,CAAC,eAAe,CAA6C;IACpE,OAAO,CAAC,gBAAgB,CAAsB;IAC9C,OAAO,CAAC,OAAO,CAAkB;gBAErB,MAAM,EAAE,eAAe;IA0CnC;;;;;;;;OAQG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB3B;;OAEG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,IAAI;IAI9D;;OAEG;IACH,QAAQ,IAAI,QAAQ,CAAC,cAAc,CAAC;IAIpC;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;OAEG;YACW,aAAa;IAO3B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAchC;;OAEG;IACH,OAAO,CAAC,aAAa;IAmBrB;;OAEG;IACH,OAAO,CAAC,eAAe;CAUxB"}
@@ -0,0 +1,179 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { RunnerHealth } from './types/index.js';
3
+ import { HttpTransport } from './transports/http-transport.js';
4
+ import { EndpointDetector } from './services/endpoint-detector.js';
5
+ import { HealthMonitor } from './services/health-monitor.js';
6
+ import { HeartbeatSender } from './services/heartbeat-sender.js';
7
+ import { LeaseManager } from './services/lease-manager.js';
8
+ import { Registrar } from './services/registrar.js';
9
+ import { RetryManager } from './services/retry-manager.js';
10
+ /**
11
+ * Ratatoskr — Lightweight discovery and heartbeat daemon.
12
+ *
13
+ * Runs alongside an agent runner and continuously informs Yggdrasil about
14
+ * runner availability, network endpoints, capabilities, health status,
15
+ * IP changes, and shutdown events.
16
+ */
17
+ export class Ratatoskr {
18
+ config;
19
+ state;
20
+ transport;
21
+ endpointDetector;
22
+ healthMonitor;
23
+ leaseManager;
24
+ retryManager;
25
+ registrar;
26
+ heartbeatSender;
27
+ endpointCheckTimer;
28
+ leaseCheckTimer;
29
+ shutdownHandlers = [];
30
+ started = false;
31
+ constructor(config) {
32
+ this.config = this.resolveConfig(config);
33
+ this.state = this.initializeState();
34
+ this.transport = new HttpTransport(this.config.yggdrasilUrl, this.config.apiKey);
35
+ this.endpointDetector = new EndpointDetector(8080, this.config.detectPublicIp);
36
+ this.healthMonitor = new HealthMonitor();
37
+ if (config.healthProvider) {
38
+ this.healthMonitor.setHealthProvider(config.healthProvider);
39
+ }
40
+ this.leaseManager = new LeaseManager(this.config.leaseTtl);
41
+ this.retryManager = new RetryManager();
42
+ this.registrar = new Registrar(this.transport, this.endpointDetector, this.retryManager, this.leaseManager, this.state.runnerId, this.state.runnerName, this.config.capabilities, this.config.labels, this.config.metadata);
43
+ this.heartbeatSender = new HeartbeatSender(this.transport, this.healthMonitor, this.retryManager, this.state.runnerId, this.config.heartbeatInterval);
44
+ }
45
+ /**
46
+ * Start the Ratatoskr daemon.
47
+ *
48
+ * 1. Registers the runner with Yggdrasil
49
+ * 2. Begins sending heartbeats
50
+ * 3. Monitors for IP changes
51
+ * 4. Monitors lease expiry for re-registration
52
+ * 5. Registers shutdown handlers for graceful deregistration
53
+ */
54
+ async start() {
55
+ if (this.started)
56
+ return;
57
+ this.started = true;
58
+ if (this.config.endpointProvider) {
59
+ const customEndpoint = await this.config.endpointProvider();
60
+ this.endpointDetector.setEndpoint(customEndpoint);
61
+ }
62
+ // 1. Register with Yggdrasil
63
+ await this.registrar.register();
64
+ // 2. Start heartbeats
65
+ this.heartbeatSender.start();
66
+ // 3. Monitor IP changes
67
+ this.endpointCheckTimer = setInterval(() => {
68
+ this.checkEndpoint();
69
+ }, 10_000);
70
+ // 4. Monitor lease expiry
71
+ this.leaseCheckTimer = setInterval(() => {
72
+ this.registrar.renewIfNeeded();
73
+ }, 5_000);
74
+ // 5. Register shutdown handlers
75
+ this.registerShutdownHandlers();
76
+ }
77
+ /**
78
+ * Stop the Ratatoskr daemon and deregister from Yggdrasil.
79
+ */
80
+ async stop() {
81
+ if (!this.started)
82
+ return;
83
+ this.started = false;
84
+ // Stop periodic checks
85
+ if (this.endpointCheckTimer !== undefined) {
86
+ clearInterval(this.endpointCheckTimer);
87
+ this.endpointCheckTimer = undefined;
88
+ }
89
+ if (this.leaseCheckTimer !== undefined) {
90
+ clearInterval(this.leaseCheckTimer);
91
+ this.leaseCheckTimer = undefined;
92
+ }
93
+ // Stop heartbeats
94
+ this.heartbeatSender.stop();
95
+ // Deregister from Yggdrasil
96
+ await this.registrar.deregister();
97
+ }
98
+ /**
99
+ * Set a custom health provider.
100
+ */
101
+ setHealthProvider(provider) {
102
+ this.healthMonitor.setHealthProvider(provider);
103
+ }
104
+ /**
105
+ * Returns the current runner state.
106
+ */
107
+ getState() {
108
+ return { ...this.state };
109
+ }
110
+ /**
111
+ * Returns whether the daemon is currently running.
112
+ */
113
+ isRunning() {
114
+ return this.started;
115
+ }
116
+ /**
117
+ * Returns whether the runner is registered with Yggdrasil.
118
+ */
119
+ isRegistered() {
120
+ return this.registrar.isRegistered();
121
+ }
122
+ /**
123
+ * Check for endpoint changes and notify Yggdrasil if detected.
124
+ */
125
+ async checkEndpoint() {
126
+ const update = await this.endpointDetector.detect();
127
+ if (update !== null) {
128
+ await this.registrar.updateEndpoint(update);
129
+ }
130
+ }
131
+ /**
132
+ * Register handlers for SIGTERM and SIGINT.
133
+ */
134
+ registerShutdownHandlers() {
135
+ const handler = async () => {
136
+ await this.stop();
137
+ };
138
+ process.on('SIGTERM', handler);
139
+ process.on('SIGINT', handler);
140
+ this.shutdownHandlers.push(() => {
141
+ process.removeListener('SIGTERM', handler);
142
+ process.removeListener('SIGINT', handler);
143
+ });
144
+ }
145
+ /**
146
+ * Resolve the provided config with defaults.
147
+ */
148
+ resolveConfig(config) {
149
+ return {
150
+ runnerId: config.runnerId ?? `runner-${nanoid(8)}`,
151
+ name: config.name ?? 'unknown',
152
+ yggdrasilUrl: config.yggdrasilUrl,
153
+ apiKey: config.apiKey ?? '',
154
+ capabilities: config.capabilities ?? [],
155
+ heartbeatInterval: config.heartbeatInterval ?? 30,
156
+ leaseTtl: config.leaseTtl ?? 60,
157
+ endpointProvider: config.endpointProvider ?? (() => Promise.resolve('')),
158
+ healthProvider: config.healthProvider ?? (() => Promise.resolve({ status: RunnerHealth.HEALTHY })),
159
+ detectLocalIp: config.detectLocalIp ?? true,
160
+ detectPublicIp: config.detectPublicIp ?? false,
161
+ labels: config.labels ?? {},
162
+ metadata: config.metadata ?? {},
163
+ };
164
+ }
165
+ /**
166
+ * Initialize the internal state.
167
+ */
168
+ initializeState() {
169
+ return {
170
+ runnerId: this.config.runnerId,
171
+ runnerName: this.config.name,
172
+ version: '0.1.0',
173
+ endpoint: 'pending',
174
+ lastHealth: RunnerHealth.HEALTHY,
175
+ running: false,
176
+ };
177
+ }
178
+ }
179
+ //# sourceMappingURL=ratatoskr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ratatoskr.js","sourceRoot":"","sources":["../../src/ratatoskr.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":""}
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ratatoskr runner entrypoint.
4
+ *
5
+ * Starts the Ratatoskr daemon to register and heartbeat with Yggdrasil.
6
+ */
7
+ import { Ratatoskr } from './index.js';
8
+ const yggdrasilUrl = process.env['YGGDRASIL_URL'] || 'http://orchestration-controller:3000';
9
+ const apiKey = process.env['API_KEY'] || '';
10
+ const runnerName = process.env['RUNNER_NAME'] || `ratatoskr-${process.env['HOSTNAME'] || 'unknown'}`;
11
+ const capabilities = (process.env['CAPABILITIES'] || 'http,health')
12
+ .split(',')
13
+ .map(c => c.trim())
14
+ .filter(c => c !== '');
15
+ const ratatoskr = new Ratatoskr({
16
+ yggdrasilUrl,
17
+ ...(apiKey ? { apiKey } : {}),
18
+ name: runnerName,
19
+ capabilities,
20
+ heartbeatInterval: 15,
21
+ leaseTtl: 45,
22
+ detectLocalIp: true,
23
+ detectPublicIp: false,
24
+ });
25
+ ratatoskr.start()
26
+ .then(() => {
27
+ console.log(`[Ratatoskr] Started — runner: ${ratatoskr.getState().runnerId}, yggdrasil: ${yggdrasilUrl}`);
28
+ })
29
+ .catch((err) => {
30
+ console.error('[Ratatoskr] Failed to start:', err);
31
+ process.exit(1);
32
+ });
33
+ // Keep the process alive
34
+ process.on('SIGTERM', async () => {
35
+ console.log('[Ratatoskr] Shutting down...');
36
+ await ratatoskr.stop();
37
+ process.exit(0);
38
+ });
39
+ process.on('SIGINT', async () => {
40
+ console.log('[Ratatoskr] Shutting down...');
41
+ await ratatoskr.stop();
42
+ process.exit(0);
43
+ });
44
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":";AAEA;;;;GAIG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,sCAAsC,CAAC;AAC5F,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;AAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,aAAa,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,SAAS,EAAE,CAAC;AACrG,MAAM,YAAY,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,aAAa,CAAC;KAChE,KAAK,CAAC,GAAG,CAAC;KACV,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;KAClB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;AAEzB,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC;IAC9B,YAAY;IACZ,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7B,IAAI,EAAE,UAAU;IAChB,YAAY;IACZ,iBAAiB,EAAE,EAAE;IACrB,QAAQ,EAAE,EAAE;IACZ,aAAa,EAAE,IAAI;IACnB,cAAc,EAAE,KAAK;CACtB,CAAC,CAAC;AAEH,SAAS,CAAC,KAAK,EAAE;KACd,IAAI,CAAC,GAAG,EAAE;IACT,OAAO,CAAC,GAAG,CAAC,iCAAiC,SAAS,CAAC,QAAQ,EAAE,CAAC,QAAQ,gBAAgB,YAAY,EAAE,CAAC,CAAC;AAC5G,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACb,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEL,yBAAyB;AACzB,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;IAC/B,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;IAC5C,MAAM,SAAS,CAAC,IAAI,EAAE,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;IAC9B,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;IAC5C,MAAM,SAAS,CAAC,IAAI,EAAE,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,49 @@
1
+ import type { EndpointUpdatePayload } from '../types/index.js';
2
+ /**
3
+ * Detects and monitors the runner's network endpoint.
4
+ *
5
+ * Tracks local IP, hostname, and optionally public IP, emitting
6
+ * endpoint changes so the registrar can notify Yggdrasil.
7
+ */
8
+ export declare class EndpointDetector {
9
+ private currentEndpoint;
10
+ private readonly port;
11
+ private readonly detectPublicIp;
12
+ private publicIpCache;
13
+ /**
14
+ * @param port - The port the runner serves on.
15
+ * @param detectPublicIp - Whether to fetch the public IP (default false).
16
+ */
17
+ constructor(port?: number, detectPublicIp?: boolean);
18
+ /**
19
+ * Returns the currently detected endpoint URL.
20
+ */
21
+ getCurrentEndpoint(): string;
22
+ /**
23
+ * Override the detected endpoint with a custom value.
24
+ * Used by custom endpoint providers.
25
+ */
26
+ setEndpoint(endpoint: string): void;
27
+ /**
28
+ * Performs a single endpoint detection and returns an update payload
29
+ * if the endpoint changed, or null if unchanged.
30
+ */
31
+ detect(): Promise<EndpointUpdatePayload | null>;
32
+ /**
33
+ * Resolves the best available endpoint for this runner.
34
+ */
35
+ private resolveEndpoint;
36
+ /**
37
+ * Builds a local endpoint from the first non-internal IPv4 address.
38
+ */
39
+ private buildLocalEndpoint;
40
+ /**
41
+ * Fetches the public IP from an external service.
42
+ */
43
+ private fetchPublicIp;
44
+ /**
45
+ * Returns the hostname of the current machine.
46
+ */
47
+ getHostname(): string;
48
+ }
49
+ //# sourceMappingURL=endpoint-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint-detector.d.ts","sourceRoot":"","sources":["../../../src/services/endpoint-detector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE/D;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,aAAa,CAAqB;IAE1C;;;OAGG;gBACS,IAAI,GAAE,MAAa,EAAE,cAAc,GAAE,OAAe;IAMhE;;OAEG;IACH,kBAAkB,IAAI,MAAM;IAI5B;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAInC;;;OAGG;IACG,MAAM,IAAI,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAiBrD;;OAEG;YACW,eAAe;IAa7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAiB1B;;OAEG;YACW,aAAa;IAc3B;;OAEG;IACH,WAAW,IAAI,MAAM;CAGtB"}
@@ -0,0 +1,105 @@
1
+ import * as os from 'os';
2
+ import axios from 'axios';
3
+ /**
4
+ * Detects and monitors the runner's network endpoint.
5
+ *
6
+ * Tracks local IP, hostname, and optionally public IP, emitting
7
+ * endpoint changes so the registrar can notify Yggdrasil.
8
+ */
9
+ export class EndpointDetector {
10
+ currentEndpoint;
11
+ port;
12
+ detectPublicIp;
13
+ publicIpCache;
14
+ /**
15
+ * @param port - The port the runner serves on.
16
+ * @param detectPublicIp - Whether to fetch the public IP (default false).
17
+ */
18
+ constructor(port = 8080, detectPublicIp = false) {
19
+ this.port = port;
20
+ this.detectPublicIp = detectPublicIp;
21
+ this.currentEndpoint = this.buildLocalEndpoint();
22
+ }
23
+ /**
24
+ * Returns the currently detected endpoint URL.
25
+ */
26
+ getCurrentEndpoint() {
27
+ return this.currentEndpoint;
28
+ }
29
+ /**
30
+ * Override the detected endpoint with a custom value.
31
+ * Used by custom endpoint providers.
32
+ */
33
+ setEndpoint(endpoint) {
34
+ this.currentEndpoint = endpoint;
35
+ }
36
+ /**
37
+ * Performs a single endpoint detection and returns an update payload
38
+ * if the endpoint changed, or null if unchanged.
39
+ */
40
+ async detect() {
41
+ const oldEndpoint = this.currentEndpoint;
42
+ const newEndpoint = await this.resolveEndpoint();
43
+ if (newEndpoint === oldEndpoint) {
44
+ return null;
45
+ }
46
+ this.currentEndpoint = newEndpoint;
47
+ return {
48
+ runnerId: '', // filled in by the caller
49
+ oldEndpoint,
50
+ newEndpoint,
51
+ };
52
+ }
53
+ /**
54
+ * Resolves the best available endpoint for this runner.
55
+ */
56
+ async resolveEndpoint() {
57
+ if (this.detectPublicIp) {
58
+ try {
59
+ const publicIp = await this.fetchPublicIp();
60
+ return `http://${publicIp}:${this.port}`;
61
+ }
62
+ catch {
63
+ // Fall through to local endpoint
64
+ }
65
+ }
66
+ return this.buildLocalEndpoint();
67
+ }
68
+ /**
69
+ * Builds a local endpoint from the first non-internal IPv4 address.
70
+ */
71
+ buildLocalEndpoint() {
72
+ const interfaces = os.networkInterfaces();
73
+ for (const name of Object.keys(interfaces)) {
74
+ const netInterfaces = interfaces[name];
75
+ if (!netInterfaces)
76
+ continue;
77
+ for (const net of netInterfaces) {
78
+ if (net.family === 'IPv4' && !net.internal) {
79
+ return `http://${net.address}:${this.port}`;
80
+ }
81
+ }
82
+ }
83
+ return `http://127.0.0.1:${this.port}`;
84
+ }
85
+ /**
86
+ * Fetches the public IP from an external service.
87
+ */
88
+ async fetchPublicIp() {
89
+ if (this.publicIpCache) {
90
+ return this.publicIpCache;
91
+ }
92
+ const response = await axios.get('https://api.ipify.org', {
93
+ timeout: 3000,
94
+ });
95
+ this.publicIpCache = response.data.trim();
96
+ return this.publicIpCache;
97
+ }
98
+ /**
99
+ * Returns the hostname of the current machine.
100
+ */
101
+ getHostname() {
102
+ return os.hostname();
103
+ }
104
+ }
105
+ //# sourceMappingURL=endpoint-detector.js.map