@dmsdc-ai/aigentry-telepty 0.4.1 → 0.4.2
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/CHANGELOG.md +34 -0
- package/cli.js +22 -1
- package/cross-machine.js +47 -8
- package/package.json +4 -4
- package/session-routing.js +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.4.2] - 2026-05-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **#28** — SSH-peer routing for `telepty inject` / `list` / `enter`
|
|
10
|
+
cross-machine: file-backed `peers.json` fallback resolves the prior
|
|
11
|
+
`fetch failed` against SSH peers in fresh CLI subprocesses. Previously
|
|
12
|
+
`cross-machine.js` consulted only the in-memory `activePeers` Map, which
|
|
13
|
+
is process-local and empty for every CLI subprocess spawned after
|
|
14
|
+
`telepty connect`. New: `listSshPeers` + `getSshPeerHandle` helpers
|
|
15
|
+
(`cross-machine.js`) make SSH-peer discovery/inject symmetric with the
|
|
16
|
+
existing HTTP-peer path; `pickSessionTarget` (`session-routing.js`)
|
|
17
|
+
matches `<id>@<peerName>` against the peer alias; `resolveSessionTarget`
|
|
18
|
+
(`cli.js`) enriches synthetic targets with `peerName` when the host
|
|
19
|
+
matches a known SSH peer. 7 new unit tests
|
|
20
|
+
(`test/cross-machine-ssh-routing.test.js`) + 1 new peer-alias test
|
|
21
|
+
(`test/session-routing.test.js`). Scope: `inject` / `list` / `enter`;
|
|
22
|
+
`attach` / `read-screen` / `rename` / `destroy` / `state` /
|
|
23
|
+
`session info` share the same gap but are deferred to v0.4.3+.
|
|
24
|
+
|
|
25
|
+
### Notes
|
|
26
|
+
|
|
27
|
+
- **Snyk SAST scan on changed files** — `cross-machine.js` +
|
|
28
|
+
`session-routing.js` + `test/cross-machine-ssh-routing.test.js` +
|
|
29
|
+
`test/session-routing.test.js` = **0 findings** (At-Inception clean).
|
|
30
|
+
`cli.js` shows **5 pre-existing findings** (2 Medium Command Injection
|
|
31
|
+
at `execSync` L469 + `pty.spawn` L1096, 3 Low Path Traversal at
|
|
32
|
+
L2308/L2310/L2619) with **identical fingerprints** vs HEAD~1 (5/5
|
|
33
|
+
verified). Line numbers for sinks below L543 shifted +21 from the
|
|
34
|
+
`resolveSessionTarget` enrichment block (cli.js L543–L566) — logical
|
|
35
|
+
source→sink unchanged; no new sink call sites added. Out of #28
|
|
36
|
+
surgical scope. Tracked in **dmsdc-ai/aigentry-telepty#26** for
|
|
37
|
+
follow-up PR.
|
|
38
|
+
|
|
5
39
|
## [0.4.1] - 2026-05-17
|
|
6
40
|
|
|
7
41
|
### Fixed
|
package/cli.js
CHANGED
|
@@ -542,7 +542,28 @@ function isRemoteSession(session) {
|
|
|
542
542
|
|
|
543
543
|
async function resolveSessionTarget(sessionRef, options = {}) {
|
|
544
544
|
const sessions = options.sessions || await discoverSessions({ silent: true });
|
|
545
|
-
|
|
545
|
+
const target = pickSessionTarget(sessionRef, sessions, REMOTE_HOST);
|
|
546
|
+
// When <id>@<peerName> uses an SSH peer alias (e.g. `winserver`) and the
|
|
547
|
+
// session is not in `sessions` (discovery missed it, ControlMaster expired,
|
|
548
|
+
// or remote has no such session), pickSessionTarget returns a synthetic
|
|
549
|
+
// target with no peerName/remote flag. Detect SSH peer alias here so the
|
|
550
|
+
// caller routes through cross-machine.remoteInject (SSH path) rather than
|
|
551
|
+
// falling into the HTTP fetch path with `http://winserver:3848/...`. #411
|
|
552
|
+
if (
|
|
553
|
+
target &&
|
|
554
|
+
!target.peerName &&
|
|
555
|
+
target.host &&
|
|
556
|
+
target.host !== '127.0.0.1' &&
|
|
557
|
+
!target.host.includes(':') &&
|
|
558
|
+
!target.host.includes('@')
|
|
559
|
+
) {
|
|
560
|
+
const sshPeer = crossMachine.getSshPeerHandle(target.host);
|
|
561
|
+
if (sshPeer) {
|
|
562
|
+
target.peerName = target.host;
|
|
563
|
+
target.remote = true;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return target;
|
|
546
567
|
}
|
|
547
568
|
|
|
548
569
|
async function ensureDaemonRunning(options = {}) {
|
package/cross-machine.js
CHANGED
|
@@ -37,6 +37,37 @@ function savePeers(data) {
|
|
|
37
37
|
// In-memory active peers
|
|
38
38
|
const activePeers = new Map(); // name -> { target, controlSocket, connectedAt, machineId }
|
|
39
39
|
|
|
40
|
+
// File-backed SSH peer enumeration. Required because CLI subprocesses (fresh
|
|
41
|
+
// node procs) start with an empty `activePeers` Map — only the process that
|
|
42
|
+
// called connect() has it populated. peers.json is the cross-process source of
|
|
43
|
+
// truth. controlPath(target) is deterministic, so any process can reuse the
|
|
44
|
+
// ControlMaster socket established by an earlier connect() as long as
|
|
45
|
+
// ControlPersist hasn't expired. See #411.
|
|
46
|
+
function listSshPeers() {
|
|
47
|
+
const peers = loadPeers().peers || {};
|
|
48
|
+
return Object.entries(peers)
|
|
49
|
+
.filter(([, entry]) => getPeerTransport(entry) === 'ssh' && entry && entry.target)
|
|
50
|
+
.map(([name, entry]) => ({
|
|
51
|
+
name,
|
|
52
|
+
target: entry.target,
|
|
53
|
+
machineId: entry.machineId || name,
|
|
54
|
+
lastConnected: entry.lastConnected
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getSshPeerHandle(name) {
|
|
59
|
+
if (activePeers.has(name)) return activePeers.get(name);
|
|
60
|
+
const peers = loadPeers().peers || {};
|
|
61
|
+
const entry = peers[name];
|
|
62
|
+
if (!entry || getPeerTransport(entry) !== 'ssh' || !entry.target) return null;
|
|
63
|
+
return {
|
|
64
|
+
target: entry.target,
|
|
65
|
+
controlSocket: controlPath(entry.target),
|
|
66
|
+
name,
|
|
67
|
+
machineId: entry.machineId || name
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
40
71
|
function shellQuote(value) {
|
|
41
72
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
42
73
|
}
|
|
@@ -175,14 +206,11 @@ function disconnectAll() {
|
|
|
175
206
|
* @returns {Array} sessions with host info
|
|
176
207
|
*/
|
|
177
208
|
function listRemoteSessions(name) {
|
|
178
|
-
const peer =
|
|
209
|
+
const peer = getSshPeerHandle(name);
|
|
179
210
|
if (!peer) return [];
|
|
180
211
|
|
|
181
212
|
try {
|
|
182
|
-
const output =
|
|
183
|
-
`ssh -o ControlPath=${peer.controlSocket} ${peer.target} "telepty list --json"`,
|
|
184
|
-
{ timeout: 10000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
185
|
-
);
|
|
213
|
+
const output = runRemoteCommand(peer, 'telepty list --json', { timeout: 10000 });
|
|
186
214
|
const sessions = JSON.parse(output);
|
|
187
215
|
return sessions.map(s => ({ ...s, host: peer.target, peerName: name, remote: true }));
|
|
188
216
|
} catch {
|
|
@@ -191,13 +219,21 @@ function listRemoteSessions(name) {
|
|
|
191
219
|
}
|
|
192
220
|
|
|
193
221
|
/**
|
|
194
|
-
* Discover sessions across all connected peers
|
|
222
|
+
* Discover sessions across all connected peers, including SSH peers that are
|
|
223
|
+
* persisted in peers.json but not in this process's activePeers Map. Fresh
|
|
224
|
+
* CLI subprocesses depend on the file-backed path — #411.
|
|
195
225
|
* @returns {Array} all remote sessions
|
|
196
226
|
*/
|
|
197
227
|
function discoverAllRemoteSessions() {
|
|
198
228
|
const allSessions = [];
|
|
229
|
+
const seen = new Set();
|
|
199
230
|
for (const [name] of activePeers) {
|
|
200
231
|
allSessions.push(...listRemoteSessions(name));
|
|
232
|
+
seen.add(name);
|
|
233
|
+
}
|
|
234
|
+
for (const peer of listSshPeers()) {
|
|
235
|
+
if (seen.has(peer.name)) continue;
|
|
236
|
+
allSessions.push(...listRemoteSessions(peer.name));
|
|
201
237
|
}
|
|
202
238
|
return allSessions;
|
|
203
239
|
}
|
|
@@ -206,7 +242,7 @@ function discoverAllRemoteSessions() {
|
|
|
206
242
|
* Inject text into a remote session via SSH.
|
|
207
243
|
*/
|
|
208
244
|
function remoteInject(name, sessionId, prompt, options = {}) {
|
|
209
|
-
const peer =
|
|
245
|
+
const peer = getSshPeerHandle(name);
|
|
210
246
|
if (!peer) return { success: false, error: `Not connected to ${name}` };
|
|
211
247
|
|
|
212
248
|
try {
|
|
@@ -227,7 +263,7 @@ function remoteInject(name, sessionId, prompt, options = {}) {
|
|
|
227
263
|
}
|
|
228
264
|
|
|
229
265
|
function remoteEnsureSharedContext(name, descriptor) {
|
|
230
|
-
const peer =
|
|
266
|
+
const peer = getSshPeerHandle(name);
|
|
231
267
|
if (!peer) return { success: false, error: `Not connected to ${name}` };
|
|
232
268
|
|
|
233
269
|
try {
|
|
@@ -454,6 +490,9 @@ module.exports = {
|
|
|
454
490
|
listHttpPeers,
|
|
455
491
|
listHttpRemoteSessions,
|
|
456
492
|
discoverHttpRemoteSessions,
|
|
493
|
+
// File-backed SSH peer enumeration (cross-process — #411)
|
|
494
|
+
listSshPeers,
|
|
495
|
+
getSshPeerHandle,
|
|
457
496
|
getPeerTransport,
|
|
458
497
|
PEERS_PATH
|
|
459
498
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmsdc-ai/aigentry-telepty",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"main": "daemon.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aigentry-telepty": "install.js",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"CHANGELOG.md"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
|
-
"test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js test/win-resolve-executable.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
37
|
-
"test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js test/win-resolve-executable.test.js",
|
|
38
|
-
"test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js test/win-resolve-executable.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
36
|
+
"test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
37
|
+
"test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js",
|
|
38
|
+
"test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
39
39
|
"regen-fixtures": "node scripts/regen-snippet-fixtures.js"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
package/session-routing.js
CHANGED
|
@@ -38,7 +38,12 @@ function pickSessionTarget(sessionRef, sessions, defaultHost = '127.0.0.1') {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (parsed.host) {
|
|
41
|
-
|
|
41
|
+
// Match by host or by peerName alias (e.g. `sid@winserver` where
|
|
42
|
+
// winserver is the SSH peer name and session.host is `user@FQDN`). #411
|
|
43
|
+
const exactMatch = sessions.find((session) =>
|
|
44
|
+
session.id === parsed.id &&
|
|
45
|
+
(session.host === parsed.host || session.peerName === parsed.host)
|
|
46
|
+
);
|
|
42
47
|
return exactMatch || { id: parsed.id, host: parsed.host };
|
|
43
48
|
}
|
|
44
49
|
|