@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.
@@ -1,6 +1,6 @@
1
1
  # Telepty Bus Event Schema Standard
2
2
 
3
- Version: 1.0 (2026-03-15)
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 isLocalhost = req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
41
- const isTailscale = req.ip && req.ip.startsWith('100.');
42
-
43
- if (isLocalhost || isTailscale) {
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 ${req.ip}`);
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
- const isLocalhost = req.socket.remoteAddress === '127.0.0.1' || req.socket.remoteAddress === '::1' || req.socket.remoteAddress === '::ffff:127.0.0.1';
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",