@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.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +11 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/ratatoskr.d.ts +71 -0
- package/dist/src/ratatoskr.d.ts.map +1 -0
- package/dist/src/ratatoskr.js +179 -0
- package/dist/src/ratatoskr.js.map +1 -0
- package/dist/src/runner.d.ts +3 -0
- package/dist/src/runner.d.ts.map +1 -0
- package/dist/src/runner.js +44 -0
- package/dist/src/runner.js.map +1 -0
- package/dist/src/services/endpoint-detector.d.ts +49 -0
- package/dist/src/services/endpoint-detector.d.ts.map +1 -0
- package/dist/src/services/endpoint-detector.js +105 -0
- package/dist/src/services/endpoint-detector.js.map +1 -0
- package/dist/src/services/health-monitor.d.ts +21 -0
- package/dist/src/services/health-monitor.d.ts.map +1 -0
- package/dist/src/services/health-monitor.js +40 -0
- package/dist/src/services/health-monitor.js.map +1 -0
- package/dist/src/services/heartbeat-sender.d.ts +29 -0
- package/dist/src/services/heartbeat-sender.d.ts.map +1 -0
- package/dist/src/services/heartbeat-sender.js +61 -0
- package/dist/src/services/heartbeat-sender.js.map +1 -0
- package/dist/src/services/lease-manager.d.ts +35 -0
- package/dist/src/services/lease-manager.d.ts.map +1 -0
- package/dist/src/services/lease-manager.js +48 -0
- package/dist/src/services/lease-manager.js.map +1 -0
- package/dist/src/services/registrar.d.ts +50 -0
- package/dist/src/services/registrar.d.ts.map +1 -0
- package/dist/src/services/registrar.js +102 -0
- package/dist/src/services/registrar.js.map +1 -0
- package/dist/src/services/retry-manager.d.ts +33 -0
- package/dist/src/services/retry-manager.d.ts.map +1 -0
- package/dist/src/services/retry-manager.js +58 -0
- package/dist/src/services/retry-manager.js.map +1 -0
- package/dist/src/transports/http-transport.d.ts +33 -0
- package/dist/src/transports/http-transport.d.ts.map +1 -0
- package/dist/src/transports/http-transport.js +53 -0
- package/dist/src/transports/http-transport.js.map +1 -0
- package/dist/src/transports/transport.d.ts +2 -0
- package/dist/src/transports/transport.d.ts.map +1 -0
- package/dist/src/transports/transport.js +2 -0
- package/dist/src/transports/transport.js.map +1 -0
- package/dist/src/types/index.d.ts +105 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +10 -0
- package/dist/src/types/index.js.map +1 -0
- 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 @@
|
|
|
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
|