@dmsdc-ai/aigentry-telepty 0.5.9 → 0.6.1
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 +86 -0
- package/cli.js +404 -30
- package/cross-machine.js +124 -1
- package/daemon-control.js +9 -0
- package/daemon.js +495 -23
- package/install.js +156 -26
- package/package.json +5 -5
- package/src/audit/inject-log.js +234 -0
- package/src/audit/provenance.js +86 -0
- package/src/protocol/http-auth.js +36 -1
- package/src/submit-gate.js +130 -5
- package/src/transport/broker-client.js +498 -0
- package/src/transport/broker-protocol.js +155 -0
- package/src/transport/broker-server.js +531 -0
- package/src/win-resolve-executable.js +6 -1
|
@@ -21,6 +21,38 @@ function createVerifyJwt(JWT_SECRET) {
|
|
|
21
21
|
return verifyJwt;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function createBrokerAcl(aclTable = {}) {
|
|
25
|
+
return {
|
|
26
|
+
canInject(fromNode, toNode) {
|
|
27
|
+
if (!fromNode || !toNode) return false;
|
|
28
|
+
const allowedTargets = aclTable[fromNode];
|
|
29
|
+
if (Array.isArray(allowedTargets)) return allowedTargets.includes(toNode);
|
|
30
|
+
if (allowedTargets instanceof Set) return allowedTargets.has(toNode);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function signNodeJwt(secret, claims) {
|
|
37
|
+
if (!secret) throw new Error('JWT secret is required');
|
|
38
|
+
if (!claims || typeof claims !== 'object') throw new Error('JWT claims are required');
|
|
39
|
+
|
|
40
|
+
const headerB64 = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
41
|
+
const payloadB64 = Buffer.from(JSON.stringify(claims)).toString('base64url');
|
|
42
|
+
const sigB64 = crypto.createHmac('sha256', secret)
|
|
43
|
+
.update(`${headerB64}.${payloadB64}`)
|
|
44
|
+
.digest('base64url');
|
|
45
|
+
return `${headerB64}.${payloadB64}.${sigB64}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRevokedNode(revokedNodes, decodedJwtOrSub) {
|
|
49
|
+
const sub = typeof decodedJwtOrSub === 'string' ? decodedJwtOrSub : decodedJwtOrSub && decodedJwtOrSub.sub;
|
|
50
|
+
if (!sub || !revokedNodes) return false;
|
|
51
|
+
if (Array.isArray(revokedNodes)) return revokedNodes.includes(sub);
|
|
52
|
+
if (revokedNodes instanceof Set) return revokedNodes.has(sub);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
24
56
|
function createIsAllowedPeer(PEER_ALLOWLIST) {
|
|
25
57
|
function isAllowedPeer(ip) {
|
|
26
58
|
if (!ip) return false;
|
|
@@ -66,6 +98,9 @@ function createAuthMiddleware(options) {
|
|
|
66
98
|
|
|
67
99
|
module.exports = {
|
|
68
100
|
createAuthMiddleware,
|
|
101
|
+
createBrokerAcl,
|
|
69
102
|
createIsAllowedPeer,
|
|
70
|
-
createVerifyJwt
|
|
103
|
+
createVerifyJwt,
|
|
104
|
+
isRevokedNode,
|
|
105
|
+
signNodeJwt
|
|
71
106
|
};
|
package/src/submit-gate.js
CHANGED
|
@@ -198,6 +198,7 @@ async function confirmSubmitAccepted(session, bodyText, opts = {}) {
|
|
|
198
198
|
|
|
199
199
|
let lastVisibility = null;
|
|
200
200
|
let everVisible = false;
|
|
201
|
+
let everObservedOutput = false;
|
|
201
202
|
|
|
202
203
|
while (true) {
|
|
203
204
|
const state = getState ? getState() : null;
|
|
@@ -214,6 +215,7 @@ async function confirmSubmitAccepted(session, bodyText, opts = {}) {
|
|
|
214
215
|
|
|
215
216
|
const visibility = observeBodyVisibility(session, bodyText, opts);
|
|
216
217
|
lastVisibility = visibility;
|
|
218
|
+
if (visibility.observable && visibility.empty === false) everObservedOutput = true;
|
|
217
219
|
if (visibility.reason === 'empty_body') {
|
|
218
220
|
return {
|
|
219
221
|
accepted: true,
|
|
@@ -235,22 +237,67 @@ async function confirmSubmitAccepted(session, bodyText, opts = {}) {
|
|
|
235
237
|
}
|
|
236
238
|
if (visibility.visible) {
|
|
237
239
|
everVisible = true;
|
|
238
|
-
} else {
|
|
240
|
+
} else if (everVisible) {
|
|
241
|
+
// Body was visible then disappeared — the CR consumed the input line.
|
|
242
|
+
return {
|
|
243
|
+
accepted: true,
|
|
244
|
+
retryable: false,
|
|
245
|
+
waited_ms: now() - start,
|
|
246
|
+
reason: 'body_consumed',
|
|
247
|
+
visibility,
|
|
248
|
+
};
|
|
249
|
+
} else if (!getState) {
|
|
250
|
+
// No state probe → preserve the optimistic body-absent accept so callers
|
|
251
|
+
// without a sessionStateManager keep the prior screen-free behavior.
|
|
239
252
|
return {
|
|
240
253
|
accepted: true,
|
|
241
254
|
retryable: false,
|
|
242
255
|
waited_ms: now() - start,
|
|
243
|
-
reason:
|
|
256
|
+
reason: 'body_absent',
|
|
244
257
|
visibility,
|
|
245
258
|
};
|
|
246
259
|
}
|
|
260
|
+
// else: body never observably present AND a state probe IS available — do NOT
|
|
261
|
+
// optimistically accept on absence. A dropped CR on codex alt-screen renders
|
|
262
|
+
// the body OFF the outputRing tail, so absence is not positive submit evidence
|
|
263
|
+
// (#568 FM3). Keep polling within the window for the primary signal — a state
|
|
264
|
+
// transition idle→working/thinking. If none arrives, fall through to no_land.
|
|
247
265
|
|
|
248
266
|
if (now() - start >= timeoutMs) {
|
|
267
|
+
if (visibility.visible) {
|
|
268
|
+
// Body stayed in the input box the whole window — the CR was not consumed.
|
|
269
|
+
return {
|
|
270
|
+
accepted: false,
|
|
271
|
+
retryable: true,
|
|
272
|
+
waited_ms: now() - start,
|
|
273
|
+
reason: 'body_still_visible',
|
|
274
|
+
visibility,
|
|
275
|
+
state: state || undefined,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (everObservedOutput) {
|
|
279
|
+
// The terminal produced output (it is alive) but the body was never consumed
|
|
280
|
+
// AND no state transition occurred — the CR did not land (#568 FM3, e.g. a
|
|
281
|
+
// dropped CR on codex alt-screen). Truthful retryable failure, never a false
|
|
282
|
+
// success on an unsent CR.
|
|
283
|
+
return {
|
|
284
|
+
accepted: false,
|
|
285
|
+
retryable: true,
|
|
286
|
+
waited_ms: now() - start,
|
|
287
|
+
reason: 'no_land',
|
|
288
|
+
visibility,
|
|
289
|
+
state: state || undefined,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// No observable terminal output at all for the whole window — we have zero
|
|
293
|
+
// screen evidence either way. Preserve the long-standing optimistic accept
|
|
294
|
+
// (back-compat: a wrapped session that never echoes), but mark it ambiguous.
|
|
249
295
|
return {
|
|
250
|
-
accepted:
|
|
251
|
-
retryable:
|
|
296
|
+
accepted: true,
|
|
297
|
+
retryable: false,
|
|
252
298
|
waited_ms: now() - start,
|
|
253
|
-
reason: '
|
|
299
|
+
reason: 'no_observable',
|
|
300
|
+
ambiguous: true,
|
|
254
301
|
visibility,
|
|
255
302
|
state: state || undefined,
|
|
256
303
|
};
|
|
@@ -260,6 +307,78 @@ async function confirmSubmitAccepted(session, bodyText, opts = {}) {
|
|
|
260
307
|
}
|
|
261
308
|
}
|
|
262
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Render-gate the submit CR (#568). Resolve `ready` only when the input is
|
|
312
|
+
* settled enough to safely receive a bare 0x0D:
|
|
313
|
+
* - the injected body is echoed in the outputRing tail (the input is present), AND
|
|
314
|
+
* - the render has gone quiet — the tail is unchanged for ≥ quietWindowMs.
|
|
315
|
+
*
|
|
316
|
+
* This closes the FM1 busy-render race: the pre-#568 submit fired the CR with no
|
|
317
|
+
* readiness gate, so under load the CR landed mid-render and the TUI dropped it.
|
|
318
|
+
* The daemon applies the same gate before each retry CR (FM2).
|
|
319
|
+
*
|
|
320
|
+
* Bounded + best-effort: if the render never goes quiet within timeoutMs (e.g. a
|
|
321
|
+
* continuous spinner), resolve { ready:false, reason:'timeout' } so the caller
|
|
322
|
+
* STILL writes the CR — never worse than the pre-gate behavior. When the body
|
|
323
|
+
* never echoes into the tail (alt-screen / non-echoing TUI), settle on the quiet
|
|
324
|
+
* window alone once echoGraceMs has elapsed (reason:'settled_no_echo').
|
|
325
|
+
*
|
|
326
|
+
* Pure: outputRing-only, DI now/sleep — no I/O, no daemon coupling.
|
|
327
|
+
*
|
|
328
|
+
* @param {{ outputRing?: string[] }} session
|
|
329
|
+
* @param {string} bodyText
|
|
330
|
+
* @param {{ timeoutMs?: number, quietWindowMs?: number, echoGraceMs?: number, pollIntervalMs?: number, tailBytes?: number, stripAnsi?: Function, now?: Function, sleep?: Function }} [opts]
|
|
331
|
+
* @returns {Promise<{ ready: boolean, reason: string, echoed: boolean, settled: boolean, waited_ms: number }>}
|
|
332
|
+
*/
|
|
333
|
+
async function awaitInputSettled(session, bodyText, opts = {}) {
|
|
334
|
+
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : 1500;
|
|
335
|
+
const quietWindowMs = Number.isFinite(opts.quietWindowMs) ? opts.quietWindowMs : 100;
|
|
336
|
+
const echoGraceMs = Number.isFinite(opts.echoGraceMs) ? opts.echoGraceMs : 400;
|
|
337
|
+
const pollIntervalMs = Number.isFinite(opts.pollIntervalMs) ? opts.pollIntervalMs : 30;
|
|
338
|
+
const tailBytes = Number.isFinite(opts.tailBytes) ? opts.tailBytes : 8192;
|
|
339
|
+
const stripAnsi = typeof opts.stripAnsi === 'function' ? opts.stripAnsi : (s) => s;
|
|
340
|
+
const now = typeof opts.now === 'function' ? opts.now : () => Date.now();
|
|
341
|
+
const sleep = typeof opts.sleep === 'function' ? opts.sleep : (ms) => new Promise((r) => setTimeout(r, ms));
|
|
342
|
+
|
|
343
|
+
const needle = normalize(bodyText);
|
|
344
|
+
if (!needle) {
|
|
345
|
+
return { ready: true, reason: 'empty_body', echoed: false, settled: true, waited_ms: 0 };
|
|
346
|
+
}
|
|
347
|
+
if (!session || !Array.isArray(session.outputRing)) {
|
|
348
|
+
// No ring to observe — cannot gate; stay optimistic and never block the CR.
|
|
349
|
+
return { ready: true, reason: 'no_ring', echoed: false, settled: false, waited_ms: 0 };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const start = now();
|
|
353
|
+
let lastTail = null;
|
|
354
|
+
let lastChangeAt = start;
|
|
355
|
+
let everEchoed = false;
|
|
356
|
+
|
|
357
|
+
while (true) {
|
|
358
|
+
const tail = normalize(stripAnsi(readTail(session, tailBytes)));
|
|
359
|
+
if (tail.indexOf(needle) !== -1) everEchoed = true;
|
|
360
|
+
|
|
361
|
+
if (lastTail === null || tail !== lastTail) {
|
|
362
|
+
// Render still active — reset the quiet-window timer.
|
|
363
|
+
lastTail = tail;
|
|
364
|
+
lastChangeAt = now();
|
|
365
|
+
} else if (now() - lastChangeAt >= quietWindowMs) {
|
|
366
|
+
// Tail unchanged for the full quiet window → render settled.
|
|
367
|
+
if (everEchoed) {
|
|
368
|
+
return { ready: true, reason: 'settled', echoed: true, settled: true, waited_ms: now() - start };
|
|
369
|
+
}
|
|
370
|
+
if (now() - start >= echoGraceMs) {
|
|
371
|
+
return { ready: true, reason: 'settled_no_echo', echoed: false, settled: true, waited_ms: now() - start };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (now() - start >= timeoutMs) {
|
|
376
|
+
return { ready: false, reason: 'timeout', echoed: everEchoed, settled: false, waited_ms: now() - start };
|
|
377
|
+
}
|
|
378
|
+
await sleep(pollIntervalMs);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
263
382
|
function isAcceptedSubmitState(state, submittedAtMs) {
|
|
264
383
|
if (!state || !ACCEPTED_AFTER_SUBMIT_STATES.has(state.state)) return false;
|
|
265
384
|
if (!Number.isFinite(submittedAtMs)) {
|
|
@@ -292,6 +411,7 @@ function observeBodyVisibility(session, bodyText, opts = {}) {
|
|
|
292
411
|
observable: true,
|
|
293
412
|
visible: haystack.indexOf(needle) !== -1,
|
|
294
413
|
source: 'screen',
|
|
414
|
+
empty: haystack.length === 0,
|
|
295
415
|
};
|
|
296
416
|
}
|
|
297
417
|
|
|
@@ -305,6 +425,10 @@ function observeBodyVisibility(session, bodyText, opts = {}) {
|
|
|
305
425
|
observable: true,
|
|
306
426
|
visible: haystack.indexOf(needle) !== -1,
|
|
307
427
|
source: 'output_ring',
|
|
428
|
+
// #568: distinguish "terminal alive but body off-screen" (empty=false → a
|
|
429
|
+
// dropped CR is no_land) from "no screen evidence at all" (empty=true → stay
|
|
430
|
+
// optimistic). Used by confirmSubmitAccepted's bounded timeout fallback.
|
|
431
|
+
empty: haystack.length === 0,
|
|
308
432
|
};
|
|
309
433
|
}
|
|
310
434
|
|
|
@@ -425,6 +549,7 @@ function defaultReadScreen(workspaceId, lines) {
|
|
|
425
549
|
|
|
426
550
|
module.exports = {
|
|
427
551
|
awaitReplReady,
|
|
552
|
+
awaitInputSettled,
|
|
428
553
|
verifyBodyConsumed,
|
|
429
554
|
confirmSubmitAccepted,
|
|
430
555
|
observeBodyVisibility,
|