@dmsdc-ai/aigentry-telepty 0.1.52 → 0.1.54
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/BUS_EVENT_SCHEMA.md +29 -1
- package/cli.js +7 -0
- package/daemon.js +63 -10
- package/package.json +1 -1
package/BUS_EVENT_SCHEMA.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Telepty Bus Event Schema Standard
|
|
2
2
|
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0 (2026-03-15)
|
|
4
4
|
Agreed by: telepty, deliberation, devkit, brain, orchestrator
|
|
5
5
|
|
|
6
6
|
## Transport
|
|
@@ -13,9 +13,11 @@ Agreed by: telepty, deliberation, devkit, brain, orchestrator
|
|
|
13
13
|
|
|
14
14
|
```json
|
|
15
15
|
{
|
|
16
|
+
"version": 1,
|
|
16
17
|
"message_id": "string (UUID or prefixed ID)",
|
|
17
18
|
"kind": "string (event type)",
|
|
18
19
|
"source": "string (sender identifier)",
|
|
20
|
+
"source_host": "string (machine_id of sender, e.g. hostname or Tailscale IP)",
|
|
19
21
|
"target": "string | null (target session ID, optional @host suffix)",
|
|
20
22
|
"ts": "ISO 8601 timestamp"
|
|
21
23
|
}
|
|
@@ -25,12 +27,38 @@ Agreed by: telepty, deliberation, devkit, brain, orchestrator
|
|
|
25
27
|
|
|
26
28
|
| Field | Type | Description |
|
|
27
29
|
|-------|------|-------------|
|
|
30
|
+
| `version` | number | Envelope schema version (currently 1) |
|
|
28
31
|
| `kind` | string | Event type (NOT `type` — `kind` is canonical) |
|
|
29
32
|
| `target` | string | Target telepty session ID. May include `@host` suffix for remote |
|
|
30
33
|
| `source` | string | Sender identifier (format: `project:session_id`) |
|
|
34
|
+
| `source_host` | string | Machine ID of sender (hostname or TELEPTY_MACHINE_ID) |
|
|
31
35
|
| `message_id` | string | Unique message identifier |
|
|
32
36
|
| `ts` | string | ISO 8601 timestamp |
|
|
33
37
|
|
|
38
|
+
## Cross-Machine Addressing
|
|
39
|
+
|
|
40
|
+
### Session Locator
|
|
41
|
+
Every session is uniquely identified by a locator triple:
|
|
42
|
+
```json
|
|
43
|
+
{ "machine_id": "hostname", "session_id": "aigentry-devkit-001", "project_id": "aigentry-devkit" }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Remote Target Format
|
|
47
|
+
`target` field supports `@host` suffix: `aigentry-devkit-001@100.100.100.5`
|
|
48
|
+
- Router strips suffix, resolves session on local daemon
|
|
49
|
+
- For cross-machine relay (P3), daemon forwards to target host
|
|
50
|
+
|
|
51
|
+
### Machine ID
|
|
52
|
+
- Default: `os.hostname()`
|
|
53
|
+
- Override: `TELEPTY_MACHINE_ID` env var
|
|
54
|
+
- Exposed in: `GET /api/meta` (`machine_id` field), session `locator` object, bus event `source_host`
|
|
55
|
+
|
|
56
|
+
### Peer Auth
|
|
57
|
+
- Localhost: always trusted
|
|
58
|
+
- Tailscale (100.x.y.z): trusted by default
|
|
59
|
+
- Custom peers: `TELEPTY_PEER_ALLOWLIST=ip1,ip2` env var
|
|
60
|
+
- All others: require `x-telepty-token` header
|
|
61
|
+
|
|
34
62
|
## Routable Events (Auto-Router)
|
|
35
63
|
|
|
36
64
|
### `turn_request`
|
package/cli.js
CHANGED
|
@@ -208,6 +208,13 @@ function getDiscoveryHosts() {
|
|
|
208
208
|
.filter(Boolean);
|
|
209
209
|
extraHosts.forEach((host) => hosts.add(host));
|
|
210
210
|
|
|
211
|
+
// Also include relay peers for cross-machine session discovery
|
|
212
|
+
const relayPeers = String(process.env.TELEPTY_RELAY_PEERS || '')
|
|
213
|
+
.split(',')
|
|
214
|
+
.map((host) => host.trim())
|
|
215
|
+
.filter(Boolean);
|
|
216
|
+
relayPeers.forEach((host) => hosts.add(host));
|
|
217
|
+
|
|
211
218
|
if (REMOTE_HOST && REMOTE_HOST !== '127.0.0.1') {
|
|
212
219
|
return Array.from(hosts);
|
|
213
220
|
}
|
package/daemon.js
CHANGED
|
@@ -10,6 +10,7 @@ const { claimDaemonState, clearDaemonState } = require('./daemon-control');
|
|
|
10
10
|
|
|
11
11
|
const config = getConfig();
|
|
12
12
|
const EXPECTED_TOKEN = config.authToken;
|
|
13
|
+
const MACHINE_ID = process.env.TELEPTY_MACHINE_ID || os.hostname();
|
|
13
14
|
const fs = require('fs');
|
|
14
15
|
const SESSION_PERSIST_PATH = require('path').join(os.homedir(), '.config', 'aigentry-telepty', 'sessions.json');
|
|
15
16
|
|
|
@@ -35,13 +36,58 @@ const app = express();
|
|
|
35
36
|
app.use(cors());
|
|
36
37
|
app.use(express.json());
|
|
37
38
|
|
|
39
|
+
// Peer allowlist: comma-separated IPs/CIDRs in TELEPTY_PEER_ALLOWLIST env
|
|
40
|
+
const PEER_ALLOWLIST = (process.env.TELEPTY_PEER_ALLOWLIST || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
41
|
+
|
|
42
|
+
// Cross-machine bus relay: forward bus events to peer daemons
|
|
43
|
+
const RELAY_PEERS = (process.env.TELEPTY_RELAY_PEERS || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
44
|
+
const RELAY_SEEN = new Set(); // dedup by message_id
|
|
45
|
+
|
|
46
|
+
function relayToPeers(msg) {
|
|
47
|
+
if (RELAY_PEERS.length === 0) return;
|
|
48
|
+
if (!msg.message_id) msg.message_id = crypto.randomUUID();
|
|
49
|
+
if (RELAY_SEEN.has(msg.message_id)) return; // already relayed
|
|
50
|
+
RELAY_SEEN.add(msg.message_id);
|
|
51
|
+
// Prevent unbounded growth
|
|
52
|
+
if (RELAY_SEEN.size > 10000) {
|
|
53
|
+
const arr = [...RELAY_SEEN];
|
|
54
|
+
arr.splice(0, 5000);
|
|
55
|
+
RELAY_SEEN.clear();
|
|
56
|
+
arr.forEach(id => RELAY_SEEN.add(id));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
msg.source_host = msg.source_host || MACHINE_ID;
|
|
60
|
+
msg._relayed_from = MACHINE_ID;
|
|
61
|
+
|
|
62
|
+
for (const peer of RELAY_PEERS) {
|
|
63
|
+
fetch(`http://${peer}:${PORT}/api/bus/publish`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json', 'x-telepty-token': EXPECTED_TOKEN },
|
|
66
|
+
body: JSON.stringify(msg),
|
|
67
|
+
signal: AbortSignal.timeout(3000)
|
|
68
|
+
}).catch(() => {}); // fire-and-forget
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isAllowedPeer(ip) {
|
|
73
|
+
if (!ip) return false;
|
|
74
|
+
const cleanIp = ip.replace('::ffff:', '');
|
|
75
|
+
// Localhost always allowed
|
|
76
|
+
if (cleanIp === '127.0.0.1' || ip === '::1') return true;
|
|
77
|
+
// Tailscale range (100.x.y.z)
|
|
78
|
+
if (cleanIp.startsWith('100.')) return true;
|
|
79
|
+
// Peer allowlist
|
|
80
|
+
if (PEER_ALLOWLIST.length > 0) return PEER_ALLOWLIST.includes(cleanIp);
|
|
81
|
+
// No allowlist = allow all authenticated
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
38
85
|
// Authentication Middleware
|
|
39
86
|
app.use((req, res, next) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return next(); // Trust local and Tailscale networks
|
|
87
|
+
const clientIp = req.ip;
|
|
88
|
+
|
|
89
|
+
if (isAllowedPeer(clientIp)) {
|
|
90
|
+
return next(); // Trust local, Tailscale, and allowlisted peers
|
|
45
91
|
}
|
|
46
92
|
|
|
47
93
|
const token = req.headers['x-telepty-token'] || req.query.token;
|
|
@@ -49,7 +95,7 @@ app.use((req, res, next) => {
|
|
|
49
95
|
return next();
|
|
50
96
|
}
|
|
51
97
|
|
|
52
|
-
console.warn(`[AUTH] Rejected unauthorized request from ${
|
|
98
|
+
console.warn(`[AUTH] Rejected unauthorized request from ${clientIp}`);
|
|
53
99
|
res.status(401).json({ error: 'Unauthorized: Invalid or missing token.' });
|
|
54
100
|
});
|
|
55
101
|
|
|
@@ -295,8 +341,10 @@ app.get('/api/sessions', (req, res) => {
|
|
|
295
341
|
const now = Date.now();
|
|
296
342
|
let list = Object.entries(sessions).map(([id, session]) => {
|
|
297
343
|
const idleSeconds = session.lastActivityAt ? Math.floor((now - new Date(session.lastActivityAt).getTime()) / 1000) : null;
|
|
344
|
+
const projectId = session.cwd ? session.cwd.split('/').pop() : null;
|
|
298
345
|
return {
|
|
299
346
|
id,
|
|
347
|
+
locator: { machine_id: MACHINE_ID, session_id: id, project_id: projectId },
|
|
300
348
|
type: session.type || 'spawned',
|
|
301
349
|
command: session.command,
|
|
302
350
|
cwd: session.cwd,
|
|
@@ -318,8 +366,10 @@ app.get('/api/sessions/:id', (req, res) => {
|
|
|
318
366
|
if (!resolvedId) return res.status(404).json({ error: 'Session not found' });
|
|
319
367
|
const session = sessions[resolvedId];
|
|
320
368
|
const idleSeconds = session.lastActivityAt ? Math.floor((Date.now() - new Date(session.lastActivityAt).getTime()) / 1000) : null;
|
|
369
|
+
const projectId = session.cwd ? session.cwd.split('/').pop() : null;
|
|
321
370
|
res.json({
|
|
322
371
|
id: resolvedId,
|
|
372
|
+
locator: { machine_id: MACHINE_ID, session_id: resolvedId, project_id: projectId },
|
|
323
373
|
alias: requestedId !== resolvedId ? requestedId : null,
|
|
324
374
|
type: session.type || 'spawned',
|
|
325
375
|
command: session.command,
|
|
@@ -340,6 +390,7 @@ app.get('/api/meta', (req, res) => {
|
|
|
340
390
|
pid: process.pid,
|
|
341
391
|
host: HOST,
|
|
342
392
|
port: Number(PORT),
|
|
393
|
+
machine_id: MACHINE_ID,
|
|
343
394
|
capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon', 'handoff-inbox', 'deliberation-threads']
|
|
344
395
|
});
|
|
345
396
|
});
|
|
@@ -924,6 +975,7 @@ function busAutoRoute(msg) {
|
|
|
924
975
|
type: 'inject_written',
|
|
925
976
|
inject_id,
|
|
926
977
|
sender: 'daemon',
|
|
978
|
+
source_host: MACHINE_ID,
|
|
927
979
|
target_agent: targetId,
|
|
928
980
|
source_type: 'bus_auto_route',
|
|
929
981
|
delivered,
|
|
@@ -954,6 +1006,8 @@ app.post('/api/bus/publish', (req, res) => {
|
|
|
954
1006
|
|
|
955
1007
|
// Auto-route if this is a turn_request
|
|
956
1008
|
busAutoRoute(payload);
|
|
1009
|
+
// Relay to peer daemons (dedup prevents loops)
|
|
1010
|
+
if (!payload._relayed_from) relayToPeers(payload);
|
|
957
1011
|
|
|
958
1012
|
res.json({ success: true, delivered: deliveredCount });
|
|
959
1013
|
});
|
|
@@ -1364,6 +1418,8 @@ busWss.on('connection', (ws, req) => {
|
|
|
1364
1418
|
|
|
1365
1419
|
// Auto-route turn_request events (shared logic with HTTP publish)
|
|
1366
1420
|
busAutoRoute(msg);
|
|
1421
|
+
// Relay to peer daemons (dedup prevents loops)
|
|
1422
|
+
if (!msg._relayed_from) relayToPeers(msg);
|
|
1367
1423
|
} catch (e) {
|
|
1368
1424
|
console.error('[BUS] Invalid message format', e);
|
|
1369
1425
|
}
|
|
@@ -1379,10 +1435,7 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
1379
1435
|
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
1380
1436
|
const token = url.searchParams.get('token');
|
|
1381
1437
|
|
|
1382
|
-
|
|
1383
|
-
const isTailscale = req.socket.remoteAddress && req.socket.remoteAddress.startsWith('100.');
|
|
1384
|
-
|
|
1385
|
-
if (!isLocalhost && !isTailscale && token !== EXPECTED_TOKEN) {
|
|
1438
|
+
if (!isAllowedPeer(req.socket.remoteAddress) && token !== EXPECTED_TOKEN) {
|
|
1386
1439
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
1387
1440
|
socket.destroy();
|
|
1388
1441
|
return;
|