@dmsdc-ai/aigentry-telepty 0.5.6 → 0.5.7

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/daemon.js CHANGED
@@ -139,6 +139,13 @@ app.use(express.json());
139
139
  // Peer allowlist: comma-separated IPs/CIDRs in TELEPTY_PEER_ALLOWLIST env
140
140
  const PEER_ALLOWLIST = (process.env.TELEPTY_PEER_ALLOWLIST || '').split(',').map(s => s.trim()).filter(Boolean);
141
141
 
142
+ // #533 Phase 2 — peer-lane inject guardrail. The orchestrator sid(s) define the
143
+ // ORCH LANE (always allowed). Space-separated; default matches aigentry-orchestrator
144
+ // bin/ask.sh so both ends agree on "who is the orchestrator" from one config. If this
145
+ // resolves empty the guardrail fails OPEN (see classifyPeerLaneInject).
146
+ const ORCHESTRATOR_SIDS = (process.env.AIGENTRY_ORCHESTRATOR_SIDS || 'orchestrator aigentry-orchestrator-claude')
147
+ .split(/\s+/).map(s => s.trim()).filter(Boolean);
148
+
142
149
  // Cross-machine bus relay: forward bus events to peer daemons
143
150
  const relayToPeers = createPeerRelay({
144
151
  relayPeers: relayPeersFromEnv(process.env),
@@ -368,6 +375,68 @@ function respondWithError(res, httpStatus, code, error, extra = {}) {
368
375
  return res.status(httpStatus).json(buildErrorBody(code, error, extra));
369
376
  }
370
377
 
378
+ // #533 Phase 2 — pure peer-lane inject policy verdict (self-contained; no parser
379
+ // dependency). The PEER LANE is sender ≠ orchestrator AND target ≠ orchestrator.
380
+ // On that lane the body MUST be a sanctioned compact-JSON envelope (the shape
381
+ // produced by aigentry-orchestrator bin/ask.sh build_envelope); anything else is
382
+ // out-of-policy peer→peer traffic (e.g. work-delegation) and is blocked.
383
+ // Returns { lane, decision, reason, kind, envelopePresent }:
384
+ // lane ∈ 'orchestrator' | 'peer' | 'disabled'
385
+ // decision ∈ 'allow' | 'block'
386
+ // kind ∈ 'ask-request' | 'ask-reply' | null
387
+ const PEER_INJECT_KINDS = new Set(['ask-request', 'ask-reply']);
388
+
389
+ function classifyPeerLaneInject({ from, to, prompt, orchestratorSids } = {}) {
390
+ const orchSet = Array.isArray(orchestratorSids) ? orchestratorSids : [];
391
+ // Fail-OPEN: with no known orchestrator sid we cannot tell the orch lane apart
392
+ // from the peer lane (every inject would look peer-lane), which would over-block
393
+ // legitimate orchestrator traffic and brick the mesh. Degrade to allow + warn;
394
+ // the Phase-1 orchestrator-side auditor still detects raw bypass (defense in depth).
395
+ if (orchSet.length === 0) {
396
+ return { lane: 'disabled', decision: 'allow', reason: 'orch-sid-unconfigured-fail-open', kind: null, envelopePresent: false };
397
+ }
398
+ // No sender → operator/CLI/multicast/broadcast, never peer-lane.
399
+ if (!from) {
400
+ return { lane: 'orchestrator', decision: 'allow', reason: 'no-sender', kind: null, envelopePresent: false };
401
+ }
402
+ // Orchestrator lane (either end is the orchestrator) → always allowed, untouched.
403
+ if (orchSet.includes(from) || orchSet.includes(to)) {
404
+ return { lane: 'orchestrator', decision: 'allow', reason: 'orch-lane', kind: null, envelopePresent: false };
405
+ }
406
+
407
+ // Peer lane: require a sanctioned envelope on the first non-empty line.
408
+ let env = null;
409
+ try {
410
+ const firstLine = String(prompt || '').split(/\r?\n/).map(l => l.trim()).find(l => l.length > 0);
411
+ if (firstLine) {
412
+ const parsed = JSON.parse(firstLine);
413
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) env = parsed;
414
+ }
415
+ } catch {
416
+ env = null;
417
+ }
418
+ if (!env) {
419
+ return { lane: 'peer', decision: 'block', reason: 'malformed-envelope', kind: null, envelopePresent: false };
420
+ }
421
+ if (!PEER_INJECT_KINDS.has(env.kind)) {
422
+ return { lane: 'peer', decision: 'block', reason: 'wrong-kind', kind: null, envelopePresent: true };
423
+ }
424
+ // Common required fields + per-kind payload (matches ask.sh build_envelope).
425
+ const nonEmptyStr = (v) => typeof v === 'string' && v.length > 0;
426
+ const baseOk = nonEmptyStr(env.from) && nonEmptyStr(env.to) && nonEmptyStr(env.thread_id) && Number.isInteger(env.round);
427
+ const payloadOk = env.kind === 'ask-request' ? nonEmptyStr(env.question) : nonEmptyStr(env.answer);
428
+ if (!baseOk || !payloadOk) {
429
+ return { lane: 'peer', decision: 'block', reason: 'invalid-field', kind: null, envelopePresent: true };
430
+ }
431
+ // Sender-consistency: the envelope's declared sender must match the inject's
432
+ // from (cheap anti-spoof). `to` is NOT cross-checked — the route resolves aliases,
433
+ // which would false-block legitimate aliased targets.
434
+ if (env.from !== from) {
435
+ return { lane: 'peer', decision: 'block', reason: 'from-mismatch', kind: null, envelopePresent: true };
436
+ }
437
+ return { lane: 'peer', decision: 'allow', reason: 'sanctioned-envelope', kind: env.kind, envelopePresent: true };
438
+ }
439
+
371
440
  function normalizeNullableText(value) {
372
441
  if (value === undefined || value === null) {
373
442
  return null;
@@ -2435,6 +2504,32 @@ app.post('/api/sessions/:id/inject', async (req, res) => {
2435
2504
  // Routing metadata stays in session/bus state, not in the visible prompt text.
2436
2505
  const finalPrompt = prompt;
2437
2506
  const inject_id = crypto.randomUUID();
2507
+
2508
+ // #533 Phase 2 — peer-lane inject guardrail (in-band hard block, before delivery).
2509
+ // Out-of-policy peer→peer injects (no sanctioned ask-request/ask-reply envelope)
2510
+ // are blocked here so raw work-delegation bypass is prevented, not just detected.
2511
+ // Orchestrator↔peer, broadcast/multicast (no `from`), and existing kinds are untouched.
2512
+ const peerVerdict = classifyPeerLaneInject({ from, to: requestedId, prompt, orchestratorSids: ORCHESTRATOR_SIDS });
2513
+ if (peerVerdict.decision === 'block') {
2514
+ broadcastSessionEvent('peer_inject_blocked', id, session, {
2515
+ extra: {
2516
+ target_agent: id,
2517
+ from: from || null,
2518
+ reason: peerVerdict.reason,
2519
+ attempted_kind: peerVerdict.kind,
2520
+ envelope_present: peerVerdict.envelopePresent,
2521
+ inject_id
2522
+ }
2523
+ });
2524
+ console.warn(`[PEER-GUARD] blocked peer inject ${from} → ${id} (${peerVerdict.reason})`);
2525
+ return respondWithError(res, 403, 'PEER_INJECT_BLOCKED',
2526
+ 'Peer-lane inject blocked: not a sanctioned ask-request/ask-reply envelope. Use bin/ask.sh.',
2527
+ { reason: peerVerdict.reason, sanctioned_channel: 'bin/ask.sh' });
2528
+ }
2529
+ if (peerVerdict.lane === 'disabled') {
2530
+ console.warn('[PEER-GUARD] orchestrator sid unconfigured (AIGENTRY_ORCHESTRATOR_SIDS empty) — peer guardrail disabled (fail-open)');
2531
+ }
2532
+
2438
2533
  try {
2439
2534
  const delivery = await deliverInjectionToSession(id, session, finalPrompt, {
2440
2535
  noEnter: !!no_enter,
@@ -3502,4 +3597,5 @@ module.exports = {
3502
3597
  scheduleBootstrapPromptPoll, // #29: arms the floor timer (deps DI: setTimeout/...)
3503
3598
  decideSurfaceGc, // #17: surface-liveness verdict→action (incl. INV-17 unknown→skip)
3504
3599
  applySurfaceMismatchProbe, // surface_mismatched debounce + payload helper (deps DI: emit/clock)
3600
+ classifyPeerLaneInject, // #533 Phase 2: pure peer-lane inject policy verdict
3505
3601
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
@@ -35,9 +35,9 @@
35
35
  ],
36
36
  "scripts": {
37
37
  "postinstall": "node scripts/postinstall.js",
38
- "test": "node --test test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.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/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.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 test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js && git diff --exit-code tests/snippet-protocol/v1/",
39
- "test:watch": "node --test --watch test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.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/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.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 test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js",
40
- "test:ci": "node --test --test-reporter=spec test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.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/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.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 test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js && git diff --exit-code tests/snippet-protocol/v1/",
38
+ "test": "node --test test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.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/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.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 test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js && git diff --exit-code tests/snippet-protocol/v1/",
39
+ "test:watch": "node --test --watch test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.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/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.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 test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js",
40
+ "test:ci": "node --test --test-reporter=spec test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.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/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.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 test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js && git diff --exit-code tests/snippet-protocol/v1/",
41
41
  "typecheck": "tsc --noEmit",
42
42
  "regen-fixtures": "node scripts/regen-snippet-fixtures.js"
43
43
  },
@@ -107,24 +107,6 @@ function cmuxSendText(sessionId, text) {
107
107
  }
108
108
  }
109
109
 
110
- // Send enter key to a cmux surface
111
- function cmuxSendEnter(sessionId) {
112
- const surface = findSurface(sessionId);
113
- if (!surface) return false;
114
-
115
- try {
116
- execSync(`cmux send-key --surface ${surface} return`, {
117
- timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
118
- });
119
- console.log(`[BACKEND] cmux send-key return to ${sessionId} (${surface})`);
120
- return true;
121
- } catch (err) {
122
- console.error(`[BACKEND] cmux send-key failed for ${sessionId}:`, err.message);
123
- surfaceCache.delete(sessionId);
124
- return false;
125
- }
126
- }
127
-
128
110
  // Invalidate cache for a session (e.g., when surface changes)
129
111
  function invalidateCache(sessionId) {
130
112
  surfaceCache.delete(sessionId);
@@ -534,7 +516,6 @@ module.exports = {
534
516
  detectTerminal,
535
517
  findSurface,
536
518
  cmuxSendText,
537
- cmuxSendEnter,
538
519
  refreshSurfaceCache,
539
520
  invalidateCache,
540
521
  clearCache,