@agentlip/hub 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/README.md +126 -0
- package/package.json +36 -0
- package/src/agentlipd.ts +309 -0
- package/src/apiV1.ts +1468 -0
- package/src/authMiddleware.ts +134 -0
- package/src/authToken.ts +32 -0
- package/src/bodyParser.ts +272 -0
- package/src/config.ts +273 -0
- package/src/derivedStaleness.ts +255 -0
- package/src/extractorDerived.ts +374 -0
- package/src/index.ts +878 -0
- package/src/linkifierDerived.ts +407 -0
- package/src/lock.ts +172 -0
- package/src/pluginRuntime.ts +402 -0
- package/src/pluginWorker.ts +296 -0
- package/src/rateLimiter.ts +286 -0
- package/src/serverJson.ts +138 -0
- package/src/ui.ts +843 -0
- package/src/wsEndpoint.ts +481 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @agentlip/hub
|
|
2
|
+
|
|
3
|
+
Bun-based HTTP + WebSocket hub daemon for Agentlip local coordination.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### Phase 0 (Current)
|
|
8
|
+
|
|
9
|
+
- ✅ `GET /health` endpoint (unauthenticated)
|
|
10
|
+
- ✅ Localhost-only bind validation
|
|
11
|
+
- ✅ `startHub()` API with configurable options
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
### `startHub(options?)`
|
|
16
|
+
|
|
17
|
+
Start the Agentlip hub HTTP server.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { startHub } from "@agentlip/hub";
|
|
21
|
+
|
|
22
|
+
const hub = await startHub({
|
|
23
|
+
host: "127.0.0.1", // Default: 127.0.0.1
|
|
24
|
+
port: 8080, // Default: 0 (random port)
|
|
25
|
+
instanceId: "...", // Default: auto-generated UUID
|
|
26
|
+
dbId: "...", // Default: "unknown"
|
|
27
|
+
schemaVersion: 1, // Default: 0
|
|
28
|
+
allowUnsafeNetwork: false, // Default: false
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log(`Hub running on ${hub.host}:${hub.port}`);
|
|
32
|
+
|
|
33
|
+
// Stop server
|
|
34
|
+
await hub.stop();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `assertLocalhostBind(host, options?)`
|
|
38
|
+
|
|
39
|
+
Validates that a bind host is localhost-only.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { assertLocalhostBind } from "@agentlip/hub";
|
|
43
|
+
|
|
44
|
+
// These pass:
|
|
45
|
+
assertLocalhostBind("127.0.0.1");
|
|
46
|
+
assertLocalhostBind("::1");
|
|
47
|
+
assertLocalhostBind("localhost");
|
|
48
|
+
|
|
49
|
+
// These throw unless allowUnsafeNetwork is true:
|
|
50
|
+
assertLocalhostBind("0.0.0.0"); // Error
|
|
51
|
+
assertLocalhostBind("0.0.0.0", { allowUnsafeNetwork: true }); // OK
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Endpoints
|
|
55
|
+
|
|
56
|
+
### `GET /health`
|
|
57
|
+
|
|
58
|
+
Unauthenticated health check endpoint. Always returns 200 when hub is responsive.
|
|
59
|
+
|
|
60
|
+
**Response:**
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"status": "ok",
|
|
64
|
+
"instance_id": "abc123-def456",
|
|
65
|
+
"db_id": "workspace-db-uuid",
|
|
66
|
+
"schema_version": 1,
|
|
67
|
+
"protocol_version": "v1",
|
|
68
|
+
"pid": 12345,
|
|
69
|
+
"uptime_seconds": 3600
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Security
|
|
74
|
+
|
|
75
|
+
By default, the hub only binds to localhost (`127.0.0.1` or `::1`). Attempts to bind to `0.0.0.0` or other network interfaces will fail unless `allowUnsafeNetwork: true` is explicitly set.
|
|
76
|
+
|
|
77
|
+
This prevents accidental network exposure of the hub.
|
|
78
|
+
|
|
79
|
+
## Manual Verification
|
|
80
|
+
|
|
81
|
+
Start a test server:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cd packages/hub
|
|
85
|
+
bun run verify-health.ts
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or test manually:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Terminal 1: Start hub on random port
|
|
92
|
+
bun -e "import {startHub} from './src/index.ts'; const h = await startHub(); console.log('Port:', h.port); await new Promise(r => setTimeout(r, 60000))"
|
|
93
|
+
|
|
94
|
+
# Terminal 2: Test health endpoint (replace PORT with actual port from Terminal 1)
|
|
95
|
+
curl http://127.0.0.1:PORT/health | jq
|
|
96
|
+
|
|
97
|
+
# Should output:
|
|
98
|
+
# {
|
|
99
|
+
# "status": "ok",
|
|
100
|
+
# "instance_id": "...",
|
|
101
|
+
# "db_id": "unknown",
|
|
102
|
+
# "schema_version": 0,
|
|
103
|
+
# "protocol_version": "v1",
|
|
104
|
+
# "pid": ...,
|
|
105
|
+
# "uptime_seconds": ...
|
|
106
|
+
# }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Test bind validation:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Should fail (0.0.0.0 not allowed by default)
|
|
113
|
+
bun -e "import {startHub} from './src/index.ts'; await startHub({host: '0.0.0.0'})"
|
|
114
|
+
|
|
115
|
+
# Should succeed (explicit unsafe flag)
|
|
116
|
+
bun -e "import {startHub} from './src/index.ts'; const h = await startHub({host: '0.0.0.0', allowUnsafeNetwork: true}); console.log('Port:', h.port)"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Future Work
|
|
120
|
+
|
|
121
|
+
- Authentication (auth token validation)
|
|
122
|
+
- WebSocket endpoint (`/ws`)
|
|
123
|
+
- REST API routes (`/api/v1/*`)
|
|
124
|
+
- Writer lock management
|
|
125
|
+
- server.json persistence
|
|
126
|
+
- Database integration (db_id, schema_version from meta table)
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentlip/hub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "HTTP and WebSocket hub daemon for Agentlip",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/phosphorco/agentlip",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/phosphorco/agentlip.git",
|
|
11
|
+
"directory": "packages/hub"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"bun": ">=1.0.0"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src/**/*.ts",
|
|
21
|
+
"!src/**/*.test.ts",
|
|
22
|
+
"!src/integrationHarness.ts"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./src/index.ts",
|
|
26
|
+
"./agentlipd": "./src/agentlipd.ts"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"agentlipd": "./src/agentlipd.ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@agentlip/protocol": "^0.1.0",
|
|
33
|
+
"@agentlip/workspace": "^0.1.0",
|
|
34
|
+
"@agentlip/kernel": "^0.1.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/agentlipd.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* agentlipd CLI - daemon control utilities
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* - status: check hub health and validate against on-disk DB
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Runtime guard: require Bun
|
|
10
|
+
if (typeof Bun === "undefined") {
|
|
11
|
+
console.error("Error: @agentlip/hub requires Bun runtime (https://bun.sh)");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
import { readServerJson } from "./serverJson.js";
|
|
16
|
+
import { discoverWorkspaceRoot } from "@agentlip/workspace";
|
|
17
|
+
import { openDb } from "@agentlip/kernel";
|
|
18
|
+
import type { HealthResponse } from "@agentlip/protocol";
|
|
19
|
+
|
|
20
|
+
interface StatusOptions {
|
|
21
|
+
workspace?: string;
|
|
22
|
+
json?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface StatusResult {
|
|
26
|
+
status: "running" | "not_running" | "stale" | "unreachable" | "db_mismatch";
|
|
27
|
+
instance_id?: string;
|
|
28
|
+
db_id?: string;
|
|
29
|
+
schema_version?: number;
|
|
30
|
+
protocol_version?: string;
|
|
31
|
+
port?: number;
|
|
32
|
+
pid?: number;
|
|
33
|
+
uptime_seconds?: number;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read db_id from on-disk meta table.
|
|
39
|
+
* Returns null if DB doesn't exist or meta table not initialized.
|
|
40
|
+
*/
|
|
41
|
+
async function readDbIdFromDisk(dbPath: string): Promise<string | null> {
|
|
42
|
+
try {
|
|
43
|
+
const db = openDb({ dbPath, readonly: true });
|
|
44
|
+
try {
|
|
45
|
+
const row = db
|
|
46
|
+
.query<{ value: string }, []>(
|
|
47
|
+
"SELECT value FROM meta WHERE key = 'db_id'"
|
|
48
|
+
)
|
|
49
|
+
.get();
|
|
50
|
+
return row?.value ?? null;
|
|
51
|
+
} finally {
|
|
52
|
+
db.close();
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Fetch health endpoint with timeout.
|
|
61
|
+
*/
|
|
62
|
+
async function fetchHealthWithTimeout(
|
|
63
|
+
url: string,
|
|
64
|
+
timeoutMs: number = 5000
|
|
65
|
+
): Promise<HealthResponse> {
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(url, {
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
method: "GET",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`HTTP ${response.status}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (await response.json()) as HealthResponse;
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check hub status: read server.json, call /health, validate db_id.
|
|
87
|
+
*/
|
|
88
|
+
export async function checkStatus(
|
|
89
|
+
options: StatusOptions = {}
|
|
90
|
+
): Promise<StatusResult> {
|
|
91
|
+
// Discover workspace root
|
|
92
|
+
const discovered = await discoverWorkspaceRoot(options.workspace);
|
|
93
|
+
if (!discovered) {
|
|
94
|
+
return {
|
|
95
|
+
status: "not_running",
|
|
96
|
+
error: "No workspace found (no .agentlip/db.sqlite3 in current directory tree)",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { root: workspaceRoot, dbPath } = discovered;
|
|
101
|
+
|
|
102
|
+
// Read server.json
|
|
103
|
+
const serverJson = await readServerJson({ workspaceRoot });
|
|
104
|
+
if (!serverJson) {
|
|
105
|
+
return {
|
|
106
|
+
status: "not_running",
|
|
107
|
+
error: "No hub running (server.json not found)",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Read db_id from on-disk DB
|
|
112
|
+
const diskDbId = await readDbIdFromDisk(dbPath);
|
|
113
|
+
|
|
114
|
+
// Call /health endpoint
|
|
115
|
+
const healthUrl = `http://${serverJson.host}:${serverJson.port}/health`;
|
|
116
|
+
let health: HealthResponse;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
health = await fetchHealthWithTimeout(healthUrl, 5000);
|
|
120
|
+
} catch (err: any) {
|
|
121
|
+
// Hub unreachable - server.json is stale
|
|
122
|
+
return {
|
|
123
|
+
status: "unreachable",
|
|
124
|
+
port: serverJson.port,
|
|
125
|
+
pid: serverJson.pid,
|
|
126
|
+
error: `Hub unreachable at ${healthUrl} (server.json may be stale): ${err.message}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Validate db_id matches
|
|
131
|
+
if (diskDbId && health.db_id !== diskDbId) {
|
|
132
|
+
return {
|
|
133
|
+
status: "db_mismatch",
|
|
134
|
+
instance_id: health.instance_id,
|
|
135
|
+
db_id: health.db_id,
|
|
136
|
+
schema_version: health.schema_version,
|
|
137
|
+
protocol_version: health.protocol_version,
|
|
138
|
+
port: serverJson.port,
|
|
139
|
+
pid: health.pid,
|
|
140
|
+
uptime_seconds: health.uptime_seconds,
|
|
141
|
+
error: `DB ID mismatch: hub reports ${health.db_id}, disk has ${diskDbId}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// All good!
|
|
146
|
+
return {
|
|
147
|
+
status: "running",
|
|
148
|
+
instance_id: health.instance_id,
|
|
149
|
+
db_id: health.db_id,
|
|
150
|
+
schema_version: health.schema_version,
|
|
151
|
+
protocol_version: health.protocol_version,
|
|
152
|
+
port: serverJson.port,
|
|
153
|
+
pid: health.pid,
|
|
154
|
+
uptime_seconds: health.uptime_seconds,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Print status result in human-readable format.
|
|
160
|
+
*/
|
|
161
|
+
function printHumanStatus(result: StatusResult): void {
|
|
162
|
+
switch (result.status) {
|
|
163
|
+
case "running":
|
|
164
|
+
console.log("✓ Hub is running");
|
|
165
|
+
console.log(` Instance ID: ${result.instance_id}`);
|
|
166
|
+
console.log(` Database ID: ${result.db_id}`);
|
|
167
|
+
console.log(` Schema Version: ${result.schema_version}`);
|
|
168
|
+
console.log(` Protocol Version: ${result.protocol_version}`);
|
|
169
|
+
console.log(` Port: ${result.port}`);
|
|
170
|
+
console.log(` PID: ${result.pid}`);
|
|
171
|
+
console.log(` Uptime: ${result.uptime_seconds}s`);
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case "not_running":
|
|
175
|
+
console.log("✗ Hub is not running");
|
|
176
|
+
if (result.error) {
|
|
177
|
+
console.log(` ${result.error}`);
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case "unreachable":
|
|
182
|
+
console.log("✗ Hub is unreachable (stale server.json?)");
|
|
183
|
+
if (result.port) {
|
|
184
|
+
console.log(` Port: ${result.port}`);
|
|
185
|
+
}
|
|
186
|
+
if (result.pid) {
|
|
187
|
+
console.log(` PID: ${result.pid}`);
|
|
188
|
+
}
|
|
189
|
+
if (result.error) {
|
|
190
|
+
console.log(` Error: ${result.error}`);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case "stale":
|
|
195
|
+
console.log("✗ Hub server.json is stale");
|
|
196
|
+
if (result.error) {
|
|
197
|
+
console.log(` ${result.error}`);
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case "db_mismatch":
|
|
202
|
+
console.log("✗ Database ID mismatch");
|
|
203
|
+
console.log(` Hub reports: ${result.db_id}`);
|
|
204
|
+
console.log(` Instance ID: ${result.instance_id}`);
|
|
205
|
+
console.log(` Port: ${result.port}`);
|
|
206
|
+
console.log(` PID: ${result.pid}`);
|
|
207
|
+
if (result.error) {
|
|
208
|
+
console.log(` Error: ${result.error}`);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Main CLI entry point.
|
|
216
|
+
*/
|
|
217
|
+
function printHelp(): void {
|
|
218
|
+
console.log("Usage: agentlipd <command> [options]");
|
|
219
|
+
console.log();
|
|
220
|
+
console.log("Commands:");
|
|
221
|
+
console.log(" status Check hub health using server.json and validate db_id");
|
|
222
|
+
console.log();
|
|
223
|
+
console.log("Run: agentlipd status --help");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function printStatusHelp(): void {
|
|
227
|
+
console.log("Usage: agentlipd status [--workspace <path>] [--json]");
|
|
228
|
+
console.log();
|
|
229
|
+
console.log("Check hub health and validate against on-disk database.");
|
|
230
|
+
console.log();
|
|
231
|
+
console.log("Options:");
|
|
232
|
+
console.log(" --workspace <path> Explicit workspace root (default: auto-discover)");
|
|
233
|
+
console.log(" --json Output as JSON");
|
|
234
|
+
console.log(" --help, -h Show this help");
|
|
235
|
+
console.log();
|
|
236
|
+
console.log("Exit codes:");
|
|
237
|
+
console.log(" 0 Running");
|
|
238
|
+
console.log(" 3 Not running / unreachable");
|
|
239
|
+
console.log(" 1 Other errors (DB mismatch, etc.)");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function main(argv: string[] = process.argv.slice(2)) {
|
|
243
|
+
const [command, ...args] = argv;
|
|
244
|
+
|
|
245
|
+
if (!command || command === "--help" || command === "-h") {
|
|
246
|
+
printHelp();
|
|
247
|
+
process.exit(0);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (command !== "status") {
|
|
251
|
+
console.error(`Unknown command: ${command}`);
|
|
252
|
+
console.error("Use --help for usage information");
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const options: StatusOptions = {};
|
|
257
|
+
|
|
258
|
+
// Parse status args
|
|
259
|
+
for (let i = 0; i < args.length; i++) {
|
|
260
|
+
const arg = args[i];
|
|
261
|
+
if (arg === "--workspace" || arg === "-w") {
|
|
262
|
+
const value = args[++i];
|
|
263
|
+
if (!value) {
|
|
264
|
+
console.error("--workspace requires a value");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
options.workspace = value;
|
|
268
|
+
} else if (arg === "--json") {
|
|
269
|
+
options.json = true;
|
|
270
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
271
|
+
printStatusHelp();
|
|
272
|
+
process.exit(0);
|
|
273
|
+
} else {
|
|
274
|
+
console.error(`Unknown argument: ${arg}`);
|
|
275
|
+
console.error("Use --help for usage information");
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result = await checkStatus(options);
|
|
281
|
+
|
|
282
|
+
if (options.json) {
|
|
283
|
+
console.log(JSON.stringify(result, null, 2));
|
|
284
|
+
} else {
|
|
285
|
+
printHumanStatus(result);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (result.status === "running") {
|
|
289
|
+
process.exit(0);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (
|
|
293
|
+
result.status === "not_running" ||
|
|
294
|
+
result.status === "unreachable" ||
|
|
295
|
+
result.status === "stale"
|
|
296
|
+
) {
|
|
297
|
+
process.exit(3);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Run if executed directly
|
|
304
|
+
if (import.meta.main) {
|
|
305
|
+
main().catch((err) => {
|
|
306
|
+
console.error("Fatal error:", err);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
});
|
|
309
|
+
}
|