@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 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
- return pickSessionTarget(sessionRef, sessions, REMOTE_HOST);
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 = activePeers.get(name);
209
+ const peer = getSshPeerHandle(name);
179
210
  if (!peer) return [];
180
211
 
181
212
  try {
182
- const output = execSync(
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 = activePeers.get(name);
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 = activePeers.get(name);
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.1",
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": [
@@ -38,7 +38,12 @@ function pickSessionTarget(sessionRef, sessions, defaultHost = '127.0.0.1') {
38
38
  }
39
39
 
40
40
  if (parsed.host) {
41
- const exactMatch = sessions.find((session) => session.id === parsed.id && session.host === parsed.host);
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