@dmsdc-ai/aigentry-telepty 0.1.51 → 0.1.53
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 +199 -0
- package/daemon.js +68 -10
- package/package.json +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Telepty Bus Event Schema Standard
|
|
2
|
+
|
|
3
|
+
Version: 2.0 (2026-03-15)
|
|
4
|
+
Agreed by: telepty, deliberation, devkit, brain, orchestrator
|
|
5
|
+
|
|
6
|
+
## Transport
|
|
7
|
+
|
|
8
|
+
- **HTTP**: `POST /api/bus/publish` with JSON body
|
|
9
|
+
- **WebSocket**: `ws://HOST:3848/api/bus` send JSON message
|
|
10
|
+
- Both paths trigger bus auto-router for routable events
|
|
11
|
+
|
|
12
|
+
## Envelope Structure (All Events)
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"version": 1,
|
|
17
|
+
"message_id": "string (UUID or prefixed ID)",
|
|
18
|
+
"kind": "string (event type)",
|
|
19
|
+
"source": "string (sender identifier)",
|
|
20
|
+
"source_host": "string (machine_id of sender, e.g. hostname or Tailscale IP)",
|
|
21
|
+
"target": "string | null (target session ID, optional @host suffix)",
|
|
22
|
+
"ts": "ISO 8601 timestamp"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Canonical Field Names
|
|
27
|
+
|
|
28
|
+
| Field | Type | Description |
|
|
29
|
+
|-------|------|-------------|
|
|
30
|
+
| `version` | number | Envelope schema version (currently 1) |
|
|
31
|
+
| `kind` | string | Event type (NOT `type` — `kind` is canonical) |
|
|
32
|
+
| `target` | string | Target telepty session ID. May include `@host` suffix for remote |
|
|
33
|
+
| `source` | string | Sender identifier (format: `project:session_id`) |
|
|
34
|
+
| `source_host` | string | Machine ID of sender (hostname or TELEPTY_MACHINE_ID) |
|
|
35
|
+
| `message_id` | string | Unique message identifier |
|
|
36
|
+
| `ts` | string | ISO 8601 timestamp |
|
|
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
|
+
|
|
62
|
+
## Routable Events (Auto-Router)
|
|
63
|
+
|
|
64
|
+
### `turn_request`
|
|
65
|
+
|
|
66
|
+
Published by deliberation to request a turn from a session. Telepty daemon auto-routes to target session PTY.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"message_id": "turn_request-<uuid>",
|
|
71
|
+
"session_id": "<deliberation_session_id>",
|
|
72
|
+
"project": "<project_name>",
|
|
73
|
+
"kind": "turn_request",
|
|
74
|
+
"source": "deliberation:<deliberation_session_id>",
|
|
75
|
+
"target": "<telepty_session_id>[@<host>]",
|
|
76
|
+
"reply_to": "<deliberation_session_id>",
|
|
77
|
+
"trace": ["project:<name>", "speaker:<id>", "turn:<turn_id>"],
|
|
78
|
+
"payload": {
|
|
79
|
+
"turn_id": "string",
|
|
80
|
+
"round": "number",
|
|
81
|
+
"max_rounds": "number",
|
|
82
|
+
"speaker": "string (target telepty session ID)",
|
|
83
|
+
"role": "string | null",
|
|
84
|
+
"prompt": "string (full prompt text — inject as-is to PTY)",
|
|
85
|
+
"prompt_sha1": "string (40-char SHA1)",
|
|
86
|
+
"history_entries": "number",
|
|
87
|
+
"transport_timeout_ms": "number",
|
|
88
|
+
"semantic_timeout_ms": "number"
|
|
89
|
+
},
|
|
90
|
+
"ts": "ISO 8601"
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Important Notes:**
|
|
95
|
+
- `session_id` is the DELIBERATION session ID, NOT the target telepty session
|
|
96
|
+
- `target` is the telepty session ID to inject into
|
|
97
|
+
- `payload.prompt` is the full text to write to PTY (no further processing needed)
|
|
98
|
+
- `@host` suffix on target: strip before resolving, use for remote routing
|
|
99
|
+
|
|
100
|
+
**Auto-Router Behavior:**
|
|
101
|
+
1. Daemon receives turn_request via HTTP POST or WS
|
|
102
|
+
2. Extracts `target` field, strips `@host` suffix
|
|
103
|
+
3. Resolves session via `resolveSessionAlias()`
|
|
104
|
+
4. Delivers `payload.prompt` to session PTY (kitty primary, WS fallback)
|
|
105
|
+
5. Emits `inject_written` ack on bus
|
|
106
|
+
|
|
107
|
+
### `inject_written` (ACK)
|
|
108
|
+
|
|
109
|
+
Emitted by telepty after successful auto-route delivery.
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"type": "inject_written",
|
|
114
|
+
"inject_id": "UUID",
|
|
115
|
+
"sender": "daemon",
|
|
116
|
+
"target_agent": "<session_id>",
|
|
117
|
+
"source_type": "bus_auto_route",
|
|
118
|
+
"delivered": true,
|
|
119
|
+
"timestamp": "ISO 8601"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Session Lifecycle Events
|
|
124
|
+
|
|
125
|
+
### `session_register`
|
|
126
|
+
```json
|
|
127
|
+
{ "type": "session_register", "sender": "daemon", "session_id": "string", "command": "string", "cwd": "string", "timestamp": "ISO 8601" }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `session.replaced`
|
|
131
|
+
```json
|
|
132
|
+
{ "type": "session.replaced", "sender": "daemon", "old_id": "string", "new_id": "string", "alias": "string", "timestamp": "ISO 8601" }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `session.idle`
|
|
136
|
+
```json
|
|
137
|
+
{ "type": "session.idle", "session_id": "string", "idleSeconds": "number", "lastActivityAt": "ISO 8601", "timestamp": "ISO 8601" }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `session_health` (periodic, every 10s)
|
|
141
|
+
```json
|
|
142
|
+
{ "type": "session_health", "session_id": "string", "payload": { "alive": true, "pid": "number|null", "type": "string", "clients": "number", "idleSeconds": "number|null" }, "timestamp": "ISO 8601" }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Inject Events
|
|
146
|
+
|
|
147
|
+
### `inject_written`
|
|
148
|
+
```json
|
|
149
|
+
{ "type": "inject_written", "inject_id": "UUID", "sender": "daemon", "target_agent": "string", "content": "string", "from": "string|null", "reply_to": "string|null", "thread_id": "string|null", "reply_expected": "boolean", "timestamp": "ISO 8601" }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `message_routed`
|
|
153
|
+
```json
|
|
154
|
+
{ "type": "message_routed", "message_id": "UUID", "from": "string", "to": "string", "reply_to": "string", "inject_id": "UUID", "deliberation_session_id": "string|null", "thread_id": "string|null", "timestamp": "ISO 8601" }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Handoff Events
|
|
158
|
+
|
|
159
|
+
### `handoff.created` / `handoff.claimed` / `handoff.executing` / `handoff.completed`
|
|
160
|
+
```json
|
|
161
|
+
{ "type": "handoff.<status>", "handoff_id": "UUID", "source_session_id": "string|null", "deliberation_id": "string|null", "auto_execute": "boolean", "task_count": "number", "timestamp": "ISO 8601" }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Thread Events
|
|
165
|
+
|
|
166
|
+
### `thread.opened`
|
|
167
|
+
```json
|
|
168
|
+
{ "type": "thread.opened", "thread_id": "UUID", "topic": "string", "orchestrator_session_id": "string|null", "participant_session_ids": ["string"], "timestamp": "ISO 8601" }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `thread.closed`
|
|
172
|
+
```json
|
|
173
|
+
{ "type": "thread.closed", "thread_id": "UUID", "topic": "string", "message_count": "number", "timestamp": "ISO 8601" }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Termination Signal Detection
|
|
177
|
+
|
|
178
|
+
Messages containing these strings suppress auto-reply guide footer:
|
|
179
|
+
- `no further reply needed`
|
|
180
|
+
- `thread closed` / `closed on X side`
|
|
181
|
+
- `ack received` / `ack-only`
|
|
182
|
+
- `회신 불필요` / `스레드 종료`
|
|
183
|
+
|
|
184
|
+
## Inject API Reference
|
|
185
|
+
|
|
186
|
+
### `POST /api/sessions/:id/inject`
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"prompt": "string (REQUIRED — canonical body field)",
|
|
191
|
+
"from": "string (sender session ID)",
|
|
192
|
+
"reply_to": "string (defaults to from if omitted)",
|
|
193
|
+
"thread_id": "string (optional)",
|
|
194
|
+
"reply_expected": "boolean (optional)",
|
|
195
|
+
"no_enter": "boolean (skip Enter after inject)"
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Note:** The canonical body field is `prompt`, NOT `text`, `content`, or `message`.
|
package/daemon.js
CHANGED
|
@@ -10,18 +10,54 @@ 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();
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const SESSION_PERSIST_PATH = require('path').join(os.homedir(), '.config', 'aigentry-telepty', 'sessions.json');
|
|
16
|
+
|
|
17
|
+
function persistSessions() {
|
|
18
|
+
try {
|
|
19
|
+
const data = {};
|
|
20
|
+
for (const [id, s] of Object.entries(sessions)) {
|
|
21
|
+
data[id] = { id, type: s.type, command: s.command, cwd: s.cwd, createdAt: s.createdAt, lastActivityAt: s.lastActivityAt || null };
|
|
22
|
+
}
|
|
23
|
+
fs.mkdirSync(require('path').dirname(SESSION_PERSIST_PATH), { recursive: true });
|
|
24
|
+
fs.writeFileSync(SESSION_PERSIST_PATH, JSON.stringify(data, null, 2));
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadPersistedSessions() {
|
|
29
|
+
try {
|
|
30
|
+
if (!fs.existsSync(SESSION_PERSIST_PATH)) return {};
|
|
31
|
+
return JSON.parse(fs.readFileSync(SESSION_PERSIST_PATH, 'utf8'));
|
|
32
|
+
} catch { return {}; }
|
|
33
|
+
}
|
|
13
34
|
|
|
14
35
|
const app = express();
|
|
15
36
|
app.use(cors());
|
|
16
37
|
app.use(express.json());
|
|
17
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
|
+
function isAllowedPeer(ip) {
|
|
43
|
+
if (!ip) return false;
|
|
44
|
+
const cleanIp = ip.replace('::ffff:', '');
|
|
45
|
+
// Localhost always allowed
|
|
46
|
+
if (cleanIp === '127.0.0.1' || ip === '::1') return true;
|
|
47
|
+
// Tailscale range (100.x.y.z)
|
|
48
|
+
if (cleanIp.startsWith('100.')) return true;
|
|
49
|
+
// Peer allowlist
|
|
50
|
+
if (PEER_ALLOWLIST.length > 0) return PEER_ALLOWLIST.includes(cleanIp);
|
|
51
|
+
// No allowlist = allow all authenticated
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
18
55
|
// Authentication Middleware
|
|
19
56
|
app.use((req, res, next) => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return next(); // Trust local and Tailscale networks
|
|
57
|
+
const clientIp = req.ip;
|
|
58
|
+
|
|
59
|
+
if (isAllowedPeer(clientIp)) {
|
|
60
|
+
return next(); // Trust local, Tailscale, and allowlisted peers
|
|
25
61
|
}
|
|
26
62
|
|
|
27
63
|
const token = req.headers['x-telepty-token'] || req.query.token;
|
|
@@ -29,7 +65,7 @@ app.use((req, res, next) => {
|
|
|
29
65
|
return next();
|
|
30
66
|
}
|
|
31
67
|
|
|
32
|
-
console.warn(`[AUTH] Rejected unauthorized request from ${
|
|
68
|
+
console.warn(`[AUTH] Rejected unauthorized request from ${clientIp}`);
|
|
33
69
|
res.status(401).json({ error: 'Unauthorized: Invalid or missing token.' });
|
|
34
70
|
});
|
|
35
71
|
|
|
@@ -48,6 +84,21 @@ if (!daemonClaim.claimed) {
|
|
|
48
84
|
const sessions = {};
|
|
49
85
|
const handoffs = {};
|
|
50
86
|
const threads = {};
|
|
87
|
+
|
|
88
|
+
// Restore persisted session metadata (wrapped sessions await reconnect)
|
|
89
|
+
const _persisted = loadPersistedSessions();
|
|
90
|
+
for (const [id, meta] of Object.entries(_persisted)) {
|
|
91
|
+
if (meta.type === 'wrapped') {
|
|
92
|
+
sessions[id] = {
|
|
93
|
+
id, type: 'wrapped', ptyProcess: null, ownerWs: null,
|
|
94
|
+
command: meta.command || 'wrapped', cwd: meta.cwd || process.cwd(),
|
|
95
|
+
createdAt: meta.createdAt || new Date().toISOString(),
|
|
96
|
+
lastActivityAt: meta.lastActivityAt || new Date().toISOString(),
|
|
97
|
+
clients: new Set(), isClosing: false
|
|
98
|
+
};
|
|
99
|
+
console.log(`[PERSIST] Restored session ${id} (awaiting reconnect)`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
51
102
|
const STRIPPED_SESSION_ENV_KEYS = [
|
|
52
103
|
'CLAUDECODE',
|
|
53
104
|
'CODEX_CI',
|
|
@@ -185,6 +236,7 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
185
236
|
});
|
|
186
237
|
|
|
187
238
|
console.log(`[SPAWN] Created session ${session_id} (${command})`);
|
|
239
|
+
persistSessions();
|
|
188
240
|
res.status(201).json({ session_id, command, cwd });
|
|
189
241
|
} catch (err) {
|
|
190
242
|
res.status(500).json({ error: err.message });
|
|
@@ -250,6 +302,7 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
250
302
|
});
|
|
251
303
|
|
|
252
304
|
console.log(`[REGISTER] Registered wrapped session ${session_id}`);
|
|
305
|
+
persistSessions();
|
|
253
306
|
res.status(201).json({ session_id, type: 'wrapped', command: sessionRecord.command, cwd });
|
|
254
307
|
});
|
|
255
308
|
|
|
@@ -258,8 +311,10 @@ app.get('/api/sessions', (req, res) => {
|
|
|
258
311
|
const now = Date.now();
|
|
259
312
|
let list = Object.entries(sessions).map(([id, session]) => {
|
|
260
313
|
const idleSeconds = session.lastActivityAt ? Math.floor((now - new Date(session.lastActivityAt).getTime()) / 1000) : null;
|
|
314
|
+
const projectId = session.cwd ? session.cwd.split('/').pop() : null;
|
|
261
315
|
return {
|
|
262
316
|
id,
|
|
317
|
+
locator: { machine_id: MACHINE_ID, session_id: id, project_id: projectId },
|
|
263
318
|
type: session.type || 'spawned',
|
|
264
319
|
command: session.command,
|
|
265
320
|
cwd: session.cwd,
|
|
@@ -281,8 +336,10 @@ app.get('/api/sessions/:id', (req, res) => {
|
|
|
281
336
|
if (!resolvedId) return res.status(404).json({ error: 'Session not found' });
|
|
282
337
|
const session = sessions[resolvedId];
|
|
283
338
|
const idleSeconds = session.lastActivityAt ? Math.floor((Date.now() - new Date(session.lastActivityAt).getTime()) / 1000) : null;
|
|
339
|
+
const projectId = session.cwd ? session.cwd.split('/').pop() : null;
|
|
284
340
|
res.json({
|
|
285
341
|
id: resolvedId,
|
|
342
|
+
locator: { machine_id: MACHINE_ID, session_id: resolvedId, project_id: projectId },
|
|
286
343
|
alias: requestedId !== resolvedId ? requestedId : null,
|
|
287
344
|
type: session.type || 'spawned',
|
|
288
345
|
command: session.command,
|
|
@@ -303,6 +360,7 @@ app.get('/api/meta', (req, res) => {
|
|
|
303
360
|
pid: process.pid,
|
|
304
361
|
host: HOST,
|
|
305
362
|
port: Number(PORT),
|
|
363
|
+
machine_id: MACHINE_ID,
|
|
306
364
|
capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon', 'handoff-inbox', 'deliberation-threads']
|
|
307
365
|
});
|
|
308
366
|
});
|
|
@@ -814,9 +872,11 @@ app.delete('/api/sessions/:id', (req, res) => {
|
|
|
814
872
|
session.clients.forEach(ws => ws.close(1000, 'Session destroyed'));
|
|
815
873
|
delete sessions[id];
|
|
816
874
|
console.log(`[KILL] Wrapped session ${id} removed`);
|
|
875
|
+
persistSessions();
|
|
817
876
|
} else {
|
|
818
877
|
session.ptyProcess.kill();
|
|
819
878
|
console.log(`[KILL] Session ${id} forcefully closed`);
|
|
879
|
+
persistSessions();
|
|
820
880
|
}
|
|
821
881
|
res.json({ success: true, status: 'closing' });
|
|
822
882
|
} catch (err) {
|
|
@@ -885,6 +945,7 @@ function busAutoRoute(msg) {
|
|
|
885
945
|
type: 'inject_written',
|
|
886
946
|
inject_id,
|
|
887
947
|
sender: 'daemon',
|
|
948
|
+
source_host: MACHINE_ID,
|
|
888
949
|
target_agent: targetId,
|
|
889
950
|
source_type: 'bus_auto_route',
|
|
890
951
|
delivered,
|
|
@@ -1340,10 +1401,7 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
1340
1401
|
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
1341
1402
|
const token = url.searchParams.get('token');
|
|
1342
1403
|
|
|
1343
|
-
|
|
1344
|
-
const isTailscale = req.socket.remoteAddress && req.socket.remoteAddress.startsWith('100.');
|
|
1345
|
-
|
|
1346
|
-
if (!isLocalhost && !isTailscale && token !== EXPECTED_TOKEN) {
|
|
1404
|
+
if (!isAllowedPeer(req.socket.remoteAddress) && token !== EXPECTED_TOKEN) {
|
|
1347
1405
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
1348
1406
|
socket.destroy();
|
|
1349
1407
|
return;
|