muuuuse 1.4.2 → 2.1.0
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/README.md +3 -1
- package/package.json +1 -1
- package/src/agents.js +306 -11
- package/src/cli.js +3 -0
- package/src/runtime.js +593 -39
- package/src/util.js +70 -4
package/src/runtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
2
|
const { execFileSync } = require("node:child_process");
|
|
3
|
+
const path = require("node:path");
|
|
3
4
|
const pty = require("node-pty");
|
|
4
5
|
|
|
5
6
|
const {
|
|
@@ -23,18 +24,32 @@ const {
|
|
|
23
24
|
getSessionPaths,
|
|
24
25
|
hashText,
|
|
25
26
|
isPidAlive,
|
|
27
|
+
loadOrCreateSeatIdentity,
|
|
26
28
|
listSessionNames,
|
|
27
29
|
readAppendedText,
|
|
28
30
|
readJson,
|
|
29
31
|
sanitizeRelayText,
|
|
32
|
+
signText,
|
|
30
33
|
sleep,
|
|
34
|
+
verifyText,
|
|
31
35
|
writeJson,
|
|
32
36
|
} = require("./util");
|
|
33
37
|
|
|
34
38
|
const TYPE_DELAY_MS = 70;
|
|
35
39
|
const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
|
|
40
|
+
const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
|
|
41
|
+
const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
|
|
36
42
|
const MAX_RECENT_INBOUND_RELAYS = 12;
|
|
43
|
+
const MAX_RECENT_EMITTED_ANSWERS = 48;
|
|
44
|
+
const MAX_RELAY_CHAIN_HOP = 1;
|
|
37
45
|
const STOP_FORCE_KILL_MS = 1200;
|
|
46
|
+
const SEAT_JOIN_WAIT_MS = 3000;
|
|
47
|
+
const SEAT_JOIN_POLL_MS = 60;
|
|
48
|
+
const CHILD_ENV_DROP_KEYS = [
|
|
49
|
+
"CODEX_CI",
|
|
50
|
+
"CODEX_MANAGED_BY_NPM",
|
|
51
|
+
"CODEX_THREAD_ID",
|
|
52
|
+
];
|
|
38
53
|
|
|
39
54
|
function resolveShell() {
|
|
40
55
|
const shell = String(process.env.SHELL || "").trim();
|
|
@@ -49,16 +64,139 @@ function resolveShellArgs(shellPath) {
|
|
|
49
64
|
return [];
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
function resolveChildTerm() {
|
|
53
|
-
const inherited = String(
|
|
67
|
+
function resolveChildTerm(sourceEnv = process.env) {
|
|
68
|
+
const inherited = String(sourceEnv.TERM || "").trim();
|
|
54
69
|
if (inherited && inherited.toLowerCase() !== "dumb") {
|
|
55
70
|
return inherited;
|
|
56
71
|
}
|
|
57
72
|
return "xterm-256color";
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
function
|
|
61
|
-
|
|
75
|
+
function sanitizeChildPath(pathValue, homeDir) {
|
|
76
|
+
const arg0Root = path.join(homeDir, ".codex", "tmp", "arg0");
|
|
77
|
+
const entries = String(pathValue || "")
|
|
78
|
+
.split(path.delimiter)
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.filter((entry) => {
|
|
81
|
+
const resolved = path.resolve(entry);
|
|
82
|
+
return resolved !== arg0Root && !resolved.startsWith(`${arg0Root}${path.sep}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return entries.join(path.delimiter);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildChildEnv(seatId, sessionName, cwd, baseEnv = process.env) {
|
|
89
|
+
const env = { ...baseEnv };
|
|
90
|
+
for (const key of CHILD_ENV_DROP_KEYS) {
|
|
91
|
+
delete env[key];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const homeDir = String(env.HOME || "").trim() || process.env.HOME || "/root";
|
|
95
|
+
env.PATH = sanitizeChildPath(env.PATH, homeDir);
|
|
96
|
+
env.PWD = cwd;
|
|
97
|
+
env.TERM = resolveChildTerm(baseEnv);
|
|
98
|
+
env.MUUUUSE_SEAT = String(seatId);
|
|
99
|
+
env.MUUUUSE_SESSION = sessionName;
|
|
100
|
+
return env;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeWorkingPath(currentPath = process.cwd()) {
|
|
104
|
+
try {
|
|
105
|
+
return fs.realpathSync(currentPath);
|
|
106
|
+
} catch {
|
|
107
|
+
return path.resolve(currentPath);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function matchesWorkingPath(leftPath, rightPath) {
|
|
112
|
+
if (!leftPath || !rightPath) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return normalizeWorkingPath(leftPath) === normalizeWorkingPath(rightPath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createSessionName(currentPath = process.cwd()) {
|
|
120
|
+
return `${getDefaultSessionName(currentPath)}-${createId(6)}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sleepSync(ms) {
|
|
124
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function findJoinableSessionName(currentPath = process.cwd()) {
|
|
132
|
+
const candidates = listSessionNames()
|
|
133
|
+
.map((sessionName) => {
|
|
134
|
+
const sessionPaths = getSessionPaths(sessionName);
|
|
135
|
+
const controller = readJson(sessionPaths.controllerPath, null);
|
|
136
|
+
const seat1Paths = getSeatPaths(sessionName, 1);
|
|
137
|
+
const seat2Paths = getSeatPaths(sessionName, 2);
|
|
138
|
+
const seat1Meta = readJson(seat1Paths.metaPath, null);
|
|
139
|
+
const seat1Status = readJson(seat1Paths.statusPath, null);
|
|
140
|
+
const seat2Meta = readJson(seat2Paths.metaPath, null);
|
|
141
|
+
const seat2Status = readJson(seat2Paths.statusPath, null);
|
|
142
|
+
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
143
|
+
|
|
144
|
+
const cwd = controller?.cwd || seat1Status?.cwd || seat1Meta?.cwd || seat2Status?.cwd || seat2Meta?.cwd || null;
|
|
145
|
+
if (!matchesWorkingPath(cwd, currentPath)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const seat1WrapperPid = seat1Status?.pid || seat1Meta?.pid || null;
|
|
150
|
+
const seat1ChildPid = seat1Status?.childPid || seat1Meta?.childPid || null;
|
|
151
|
+
const seat2WrapperPid = seat2Status?.pid || seat2Meta?.pid || null;
|
|
152
|
+
const seat2ChildPid = seat2Status?.childPid || seat2Meta?.childPid || null;
|
|
153
|
+
const seat1Live = isPidAlive(seat1WrapperPid) || isPidAlive(seat1ChildPid);
|
|
154
|
+
const seat2Live = isPidAlive(seat2WrapperPid) || isPidAlive(seat2ChildPid);
|
|
155
|
+
const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
|
|
156
|
+
const createdAtMs = Date.parse(controller?.createdAt || seat1Meta?.startedAt || seat1Status?.updatedAt || "");
|
|
157
|
+
|
|
158
|
+
if (!seat1Live || seat2Live) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
sessionName,
|
|
168
|
+
createdAtMs,
|
|
169
|
+
};
|
|
170
|
+
})
|
|
171
|
+
.filter((entry) => entry !== null)
|
|
172
|
+
.sort((left, right) => right.createdAtMs - left.createdAtMs);
|
|
173
|
+
|
|
174
|
+
return candidates[0]?.sessionName || null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function waitForJoinableSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
|
|
178
|
+
const deadline = Date.now() + timeoutMs;
|
|
179
|
+
while (Date.now() <= deadline) {
|
|
180
|
+
const sessionName = findJoinableSessionName(currentPath);
|
|
181
|
+
if (sessionName) {
|
|
182
|
+
return sessionName;
|
|
183
|
+
}
|
|
184
|
+
sleepSync(SEAT_JOIN_POLL_MS);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
|
|
191
|
+
if (seatId === 1) {
|
|
192
|
+
return createSessionName(currentPath);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (seatId === 2) {
|
|
196
|
+
return waitForJoinableSessionName(currentPath);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return createSessionName(currentPath);
|
|
62
200
|
}
|
|
63
201
|
|
|
64
202
|
function parseAnswerEntries(text) {
|
|
@@ -118,20 +256,25 @@ function getChildProcesses(rootPid) {
|
|
|
118
256
|
.filter((entry) => entry !== null);
|
|
119
257
|
|
|
120
258
|
const descendants = [];
|
|
121
|
-
const queue = [rootPid];
|
|
122
|
-
const seen = new Set(
|
|
259
|
+
const queue = [{ pid: rootPid, depth: 0 }];
|
|
260
|
+
const seen = new Set([rootPid]);
|
|
123
261
|
|
|
124
262
|
while (queue.length > 0) {
|
|
125
|
-
const
|
|
263
|
+
const current = queue.shift();
|
|
264
|
+
const parentPid = current.pid;
|
|
126
265
|
for (const process of processes) {
|
|
127
266
|
if (process.ppid !== parentPid || seen.has(process.pid)) {
|
|
128
267
|
continue;
|
|
129
268
|
}
|
|
130
269
|
seen.add(process.pid);
|
|
131
|
-
queue.push(
|
|
270
|
+
queue.push({
|
|
271
|
+
pid: process.pid,
|
|
272
|
+
depth: current.depth + 1,
|
|
273
|
+
});
|
|
132
274
|
descendants.push({
|
|
133
275
|
...process,
|
|
134
276
|
cwd: readProcessCwd(process.pid),
|
|
277
|
+
depth: current.depth + 1,
|
|
135
278
|
});
|
|
136
279
|
}
|
|
137
280
|
}
|
|
@@ -142,23 +285,107 @@ function getChildProcesses(rootPid) {
|
|
|
142
285
|
}
|
|
143
286
|
}
|
|
144
287
|
|
|
145
|
-
function
|
|
288
|
+
function getProcessFamilyPids(processes, rootPid) {
|
|
289
|
+
if (!Number.isInteger(rootPid) || rootPid <= 0) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const related = new Set([rootPid]);
|
|
294
|
+
const queue = [rootPid];
|
|
295
|
+
|
|
296
|
+
while (queue.length > 0) {
|
|
297
|
+
const parentPid = queue.shift();
|
|
298
|
+
for (const process of processes) {
|
|
299
|
+
if (process.ppid !== parentPid || related.has(process.pid)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
related.add(process.pid);
|
|
304
|
+
queue.push(process.pid);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return [...related];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, processStartedAtMs, seatContext = {}) {
|
|
146
312
|
if (!currentPath) {
|
|
147
313
|
return null;
|
|
148
314
|
}
|
|
149
315
|
|
|
316
|
+
const options = {
|
|
317
|
+
pid: agentPid,
|
|
318
|
+
pids: seatContext.agentPids,
|
|
319
|
+
captureSinceMs,
|
|
320
|
+
seatId: seatContext.seatId,
|
|
321
|
+
sessionName: seatContext.sessionName,
|
|
322
|
+
};
|
|
323
|
+
|
|
150
324
|
if (agentType === "codex") {
|
|
151
|
-
return selectCodexSessionFile(currentPath, processStartedAtMs);
|
|
325
|
+
return selectCodexSessionFile(currentPath, processStartedAtMs, options);
|
|
152
326
|
}
|
|
153
327
|
if (agentType === "claude") {
|
|
154
|
-
return selectClaudeSessionFile(currentPath, processStartedAtMs);
|
|
328
|
+
return selectClaudeSessionFile(currentPath, processStartedAtMs, options);
|
|
155
329
|
}
|
|
156
330
|
if (agentType === "gemini") {
|
|
157
|
-
return selectGeminiSessionFile(currentPath, processStartedAtMs);
|
|
331
|
+
return selectGeminiSessionFile(currentPath, processStartedAtMs, options);
|
|
158
332
|
}
|
|
159
333
|
return null;
|
|
160
334
|
}
|
|
161
335
|
|
|
336
|
+
function buildClaimMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
337
|
+
return JSON.stringify({
|
|
338
|
+
type: "muuuuse_pair_claim",
|
|
339
|
+
sessionName,
|
|
340
|
+
challenge,
|
|
341
|
+
seat1PublicKey,
|
|
342
|
+
seat2PublicKey,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function buildAckMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
347
|
+
return JSON.stringify({
|
|
348
|
+
type: "muuuuse_pair_ack",
|
|
349
|
+
sessionName,
|
|
350
|
+
challenge,
|
|
351
|
+
seat1PublicKey,
|
|
352
|
+
seat2PublicKey,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
357
|
+
return JSON.stringify({
|
|
358
|
+
type: "muuuuse_answer",
|
|
359
|
+
sessionName,
|
|
360
|
+
challenge,
|
|
361
|
+
chainId: entry.chainId,
|
|
362
|
+
hop: entry.hop,
|
|
363
|
+
id: entry.id,
|
|
364
|
+
seatId: entry.seatId,
|
|
365
|
+
origin: entry.origin,
|
|
366
|
+
createdAt: entry.createdAt,
|
|
367
|
+
text: entry.text,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function readSeatChallenge(paths, sessionName) {
|
|
372
|
+
const record = readJson(paths.challengePath, null);
|
|
373
|
+
if (
|
|
374
|
+
!record ||
|
|
375
|
+
record.sessionName !== sessionName ||
|
|
376
|
+
typeof record.challenge !== "string" ||
|
|
377
|
+
typeof record.publicKey !== "string"
|
|
378
|
+
) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
challenge: record.challenge,
|
|
384
|
+
publicKey: record.publicKey.trim(),
|
|
385
|
+
createdAt: record.createdAt || null,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
162
389
|
async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
163
390
|
const lines = String(text || "").replace(/\r/g, "").split("\n");
|
|
164
391
|
|
|
@@ -208,8 +435,11 @@ class ArmedSeat {
|
|
|
208
435
|
constructor(options) {
|
|
209
436
|
this.seatId = options.seatId;
|
|
210
437
|
this.partnerSeatId = options.seatId === 1 ? 2 : 1;
|
|
211
|
-
this.cwd = options.cwd;
|
|
212
|
-
this.sessionName = resolveSessionName(this.cwd);
|
|
438
|
+
this.cwd = normalizeWorkingPath(options.cwd);
|
|
439
|
+
this.sessionName = resolveSessionName(this.cwd, this.seatId);
|
|
440
|
+
if (!this.sessionName) {
|
|
441
|
+
throw new Error("No armed `muuuuse 1` seat is waiting in this cwd. Run `muuuuse 1` first.");
|
|
442
|
+
}
|
|
213
443
|
this.sessionPaths = getSessionPaths(this.sessionName);
|
|
214
444
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
215
445
|
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
@@ -226,7 +456,17 @@ class ArmedSeat {
|
|
|
226
456
|
this.stdinCleanup = null;
|
|
227
457
|
this.resizeCleanup = null;
|
|
228
458
|
this.forceKillTimer = null;
|
|
459
|
+
this.identity = null;
|
|
460
|
+
this.lastUserInputAtMs = 0;
|
|
461
|
+
this.pendingInboundContext = null;
|
|
229
462
|
this.recentInboundRelays = [];
|
|
463
|
+
this.recentEmittedAnswers = [];
|
|
464
|
+
this.trustState = {
|
|
465
|
+
challenge: null,
|
|
466
|
+
peerPublicKey: null,
|
|
467
|
+
phase: this.seatId === 1 ? "waiting_for_peer_signature" : "waiting_for_seat1_key",
|
|
468
|
+
pairedAt: null,
|
|
469
|
+
};
|
|
230
470
|
this.liveState = {
|
|
231
471
|
type: null,
|
|
232
472
|
pid: null,
|
|
@@ -240,6 +480,20 @@ class ArmedSeat {
|
|
|
240
480
|
};
|
|
241
481
|
}
|
|
242
482
|
|
|
483
|
+
writeController(extra = {}) {
|
|
484
|
+
const current = readJson(this.sessionPaths.controllerPath, {});
|
|
485
|
+
writeJson(this.sessionPaths.controllerPath, {
|
|
486
|
+
sessionName: this.sessionName,
|
|
487
|
+
cwd: this.cwd,
|
|
488
|
+
createdAt: current.createdAt || this.startedAt,
|
|
489
|
+
updatedAt: new Date().toISOString(),
|
|
490
|
+
seat1Pid: this.seatId === 1 ? process.pid : current.seat1Pid || null,
|
|
491
|
+
seat2Pid: this.seatId === 2 ? process.pid : current.seat2Pid || null,
|
|
492
|
+
pid: this.seatId === 1 ? process.pid : current.pid || null,
|
|
493
|
+
...extra,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
243
497
|
log(message) {
|
|
244
498
|
process.stderr.write(`${message}\n`);
|
|
245
499
|
}
|
|
@@ -272,29 +526,193 @@ class ArmedSeat {
|
|
|
272
526
|
});
|
|
273
527
|
}
|
|
274
528
|
|
|
529
|
+
initializeTrustMaterial() {
|
|
530
|
+
this.identity = loadOrCreateSeatIdentity(this.paths);
|
|
531
|
+
|
|
532
|
+
if (this.seatId !== 1) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
writeJson(this.paths.challengePath, {
|
|
537
|
+
sessionName: this.sessionName,
|
|
538
|
+
challenge: createId(48),
|
|
539
|
+
publicKey: this.identity.publicKey,
|
|
540
|
+
createdAt: new Date().toISOString(),
|
|
541
|
+
});
|
|
542
|
+
this.trustState.challenge = readSeatChallenge(this.paths, this.sessionName)?.challenge || null;
|
|
543
|
+
this.trustState.peerPublicKey = null;
|
|
544
|
+
this.trustState.phase = "waiting_for_peer_signature";
|
|
545
|
+
this.trustState.pairedAt = null;
|
|
546
|
+
fs.rmSync(this.paths.ackPath, { force: true });
|
|
547
|
+
fs.rmSync(this.partnerPaths.claimPath, { force: true });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
syncTrustState() {
|
|
551
|
+
if (!this.identity) {
|
|
552
|
+
this.initializeTrustMaterial();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (this.seatId === 1) {
|
|
556
|
+
this.syncSeatOneTrust();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
this.syncSeatTwoTrust();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
syncSeatOneTrust() {
|
|
564
|
+
const challengeRecord = readSeatChallenge(this.paths, this.sessionName);
|
|
565
|
+
if (!challengeRecord || challengeRecord.publicKey !== this.identity.publicKey) {
|
|
566
|
+
this.trustState = {
|
|
567
|
+
challenge: null,
|
|
568
|
+
peerPublicKey: null,
|
|
569
|
+
phase: "waiting_for_peer_signature",
|
|
570
|
+
pairedAt: null,
|
|
571
|
+
};
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
this.trustState.challenge = challengeRecord.challenge;
|
|
576
|
+
const claim = readJson(this.partnerPaths.claimPath, null);
|
|
577
|
+
if (
|
|
578
|
+
!claim ||
|
|
579
|
+
claim.sessionName !== this.sessionName ||
|
|
580
|
+
claim.challenge !== challengeRecord.challenge ||
|
|
581
|
+
typeof claim.publicKey !== "string" ||
|
|
582
|
+
typeof claim.signature !== "string" ||
|
|
583
|
+
!verifyText(
|
|
584
|
+
buildClaimMessage(
|
|
585
|
+
this.sessionName,
|
|
586
|
+
challengeRecord.challenge,
|
|
587
|
+
this.identity.publicKey,
|
|
588
|
+
claim.publicKey.trim()
|
|
589
|
+
),
|
|
590
|
+
claim.signature,
|
|
591
|
+
claim.publicKey
|
|
592
|
+
)
|
|
593
|
+
) {
|
|
594
|
+
this.trustState.peerPublicKey = null;
|
|
595
|
+
this.trustState.phase = "waiting_for_peer_signature";
|
|
596
|
+
this.trustState.pairedAt = null;
|
|
597
|
+
fs.rmSync(this.paths.ackPath, { force: true });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const peerPublicKey = claim.publicKey.trim();
|
|
602
|
+
const ackMessage = buildAckMessage(this.sessionName, challengeRecord.challenge, this.identity.publicKey, peerPublicKey);
|
|
603
|
+
const currentAck = readJson(this.paths.ackPath, null);
|
|
604
|
+
const ackIsValid = Boolean(
|
|
605
|
+
currentAck &&
|
|
606
|
+
currentAck.sessionName === this.sessionName &&
|
|
607
|
+
currentAck.challenge === challengeRecord.challenge &&
|
|
608
|
+
currentAck.publicKey === this.identity.publicKey &&
|
|
609
|
+
currentAck.peerPublicKey === peerPublicKey &&
|
|
610
|
+
typeof currentAck.signature === "string" &&
|
|
611
|
+
verifyText(ackMessage, currentAck.signature, this.identity.publicKey)
|
|
612
|
+
);
|
|
613
|
+
if (!ackIsValid) {
|
|
614
|
+
writeJson(this.paths.ackPath, {
|
|
615
|
+
sessionName: this.sessionName,
|
|
616
|
+
challenge: challengeRecord.challenge,
|
|
617
|
+
publicKey: this.identity.publicKey,
|
|
618
|
+
peerPublicKey,
|
|
619
|
+
signature: signText(ackMessage, this.identity.privateKey),
|
|
620
|
+
signedAt: new Date().toISOString(),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const ackRecord = ackIsValid ? currentAck : readJson(this.paths.ackPath, null);
|
|
625
|
+
this.trustState.peerPublicKey = peerPublicKey;
|
|
626
|
+
this.trustState.phase = "paired";
|
|
627
|
+
this.trustState.pairedAt = ackRecord?.signedAt || new Date().toISOString();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
syncSeatTwoTrust() {
|
|
631
|
+
const challengeRecord = readSeatChallenge(this.partnerPaths, this.sessionName);
|
|
632
|
+
if (!challengeRecord) {
|
|
633
|
+
this.trustState = {
|
|
634
|
+
challenge: null,
|
|
635
|
+
peerPublicKey: null,
|
|
636
|
+
phase: "waiting_for_seat1_key",
|
|
637
|
+
pairedAt: null,
|
|
638
|
+
};
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const challenge = challengeRecord.challenge;
|
|
643
|
+
const peerPublicKey = challengeRecord.publicKey;
|
|
644
|
+
const claimPayload = {
|
|
645
|
+
sessionName: this.sessionName,
|
|
646
|
+
challenge,
|
|
647
|
+
publicKey: this.identity.publicKey,
|
|
648
|
+
};
|
|
649
|
+
const claimSignature = signText(
|
|
650
|
+
buildClaimMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
651
|
+
this.identity.privateKey
|
|
652
|
+
);
|
|
653
|
+
const currentClaim = readJson(this.paths.claimPath, null);
|
|
654
|
+
if (
|
|
655
|
+
!currentClaim ||
|
|
656
|
+
currentClaim.sessionName !== claimPayload.sessionName ||
|
|
657
|
+
currentClaim.challenge !== claimPayload.challenge ||
|
|
658
|
+
currentClaim.publicKey !== claimPayload.publicKey ||
|
|
659
|
+
currentClaim.signature !== claimSignature
|
|
660
|
+
) {
|
|
661
|
+
writeJson(this.paths.claimPath, {
|
|
662
|
+
...claimPayload,
|
|
663
|
+
signature: claimSignature,
|
|
664
|
+
signedAt: new Date().toISOString(),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const ack = readJson(this.partnerPaths.ackPath, null);
|
|
669
|
+
const paired = Boolean(
|
|
670
|
+
ack &&
|
|
671
|
+
ack.sessionName === this.sessionName &&
|
|
672
|
+
ack.challenge === challenge &&
|
|
673
|
+
ack.peerPublicKey === this.identity.publicKey &&
|
|
674
|
+
ack.publicKey === peerPublicKey &&
|
|
675
|
+
typeof ack.signature === "string" &&
|
|
676
|
+
verifyText(
|
|
677
|
+
buildAckMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
678
|
+
ack.signature,
|
|
679
|
+
peerPublicKey
|
|
680
|
+
)
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
this.trustState.challenge = challenge;
|
|
684
|
+
this.trustState.peerPublicKey = peerPublicKey;
|
|
685
|
+
this.trustState.phase = paired ? "paired" : "waiting_for_pair_ack";
|
|
686
|
+
this.trustState.pairedAt = paired ? (ack.signedAt || new Date().toISOString()) : null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
isPaired() {
|
|
690
|
+
return this.trustState.phase === "paired" &&
|
|
691
|
+
typeof this.trustState.challenge === "string" &&
|
|
692
|
+
typeof this.trustState.peerPublicKey === "string";
|
|
693
|
+
}
|
|
694
|
+
|
|
275
695
|
launchShell() {
|
|
276
696
|
ensureDir(this.paths.dir);
|
|
277
697
|
fs.rmSync(this.paths.pipePath, { force: true });
|
|
278
698
|
clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
|
|
699
|
+
this.initializeTrustMaterial();
|
|
700
|
+
this.writeController();
|
|
279
701
|
|
|
280
702
|
const shell = resolveShell();
|
|
281
703
|
const shellArgs = resolveShellArgs(shell);
|
|
704
|
+
const childEnv = buildChildEnv(this.seatId, this.sessionName, this.cwd);
|
|
282
705
|
this.child = pty.spawn(shell, shellArgs, {
|
|
283
706
|
cols: process.stdout.columns || 120,
|
|
284
707
|
rows: process.stdout.rows || 36,
|
|
285
708
|
cwd: this.cwd,
|
|
286
|
-
env:
|
|
287
|
-
|
|
288
|
-
TERM: resolveChildTerm(),
|
|
289
|
-
MUUUUSE_SEAT: String(this.seatId),
|
|
290
|
-
MUUUUSE_SESSION: this.sessionName,
|
|
291
|
-
},
|
|
292
|
-
name: resolveChildTerm(),
|
|
709
|
+
env: childEnv,
|
|
710
|
+
name: childEnv.TERM,
|
|
293
711
|
});
|
|
294
712
|
|
|
295
713
|
this.childPid = this.child.pid;
|
|
296
714
|
this.writeMeta();
|
|
297
|
-
this.writeStatus({ state: "running" });
|
|
715
|
+
this.writeStatus({ state: "running", trust: this.trustState.phase });
|
|
298
716
|
|
|
299
717
|
this.child.onData((data) => {
|
|
300
718
|
fs.appendFileSync(this.paths.pipePath, data);
|
|
@@ -314,6 +732,8 @@ class ArmedSeat {
|
|
|
314
732
|
|
|
315
733
|
installStdinProxy() {
|
|
316
734
|
const handleData = (chunk) => {
|
|
735
|
+
this.lastUserInputAtMs = Date.now();
|
|
736
|
+
this.pendingInboundContext = null;
|
|
317
737
|
if (!this.child) {
|
|
318
738
|
return;
|
|
319
739
|
}
|
|
@@ -441,7 +861,7 @@ class ArmedSeat {
|
|
|
441
861
|
async pullPartnerEvents() {
|
|
442
862
|
const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
|
|
443
863
|
this.partnerOffset = nextOffset;
|
|
444
|
-
if (!text.trim() || !this.child || this.stopped) {
|
|
864
|
+
if (!text.trim() || !this.child || this.stopped || !this.isPaired()) {
|
|
445
865
|
return;
|
|
446
866
|
}
|
|
447
867
|
|
|
@@ -453,7 +873,22 @@ class ArmedSeat {
|
|
|
453
873
|
}
|
|
454
874
|
|
|
455
875
|
const payload = sanitizeRelayText(entry.text);
|
|
456
|
-
|
|
876
|
+
const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
|
|
877
|
+
chainId: entry.chainId || entry.id,
|
|
878
|
+
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
879
|
+
id: entry.id,
|
|
880
|
+
seatId: entry.seatId,
|
|
881
|
+
origin: entry.origin || "unknown",
|
|
882
|
+
createdAt: entry.createdAt,
|
|
883
|
+
text: payload,
|
|
884
|
+
});
|
|
885
|
+
if (
|
|
886
|
+
!payload ||
|
|
887
|
+
entry.challenge !== this.trustState.challenge ||
|
|
888
|
+
entry.publicKey !== this.trustState.peerPublicKey ||
|
|
889
|
+
typeof entry.signature !== "string" ||
|
|
890
|
+
!verifyText(signaturePayload, entry.signature, this.trustState.peerPublicKey)
|
|
891
|
+
) {
|
|
457
892
|
continue;
|
|
458
893
|
}
|
|
459
894
|
|
|
@@ -472,6 +907,14 @@ class ArmedSeat {
|
|
|
472
907
|
return;
|
|
473
908
|
}
|
|
474
909
|
|
|
910
|
+
const deliveredAtMs = Date.now();
|
|
911
|
+
this.pendingInboundContext = {
|
|
912
|
+
chainId: entry.chainId || entry.id,
|
|
913
|
+
deliveredAtMs,
|
|
914
|
+
expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
|
|
915
|
+
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
916
|
+
relayUsed: false,
|
|
917
|
+
};
|
|
475
918
|
this.relayCount += 1;
|
|
476
919
|
this.rememberInboundRelay(payload);
|
|
477
920
|
this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
|
|
@@ -503,6 +946,37 @@ class ArmedSeat {
|
|
|
503
946
|
);
|
|
504
947
|
}
|
|
505
948
|
|
|
949
|
+
pruneRecentEmittedAnswers(now = Date.now()) {
|
|
950
|
+
this.recentEmittedAnswers = this.recentEmittedAnswers.filter(
|
|
951
|
+
(entry) => now - entry.timestampMs <= EMITTED_ANSWER_TTL_MS
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
hasRecentEmittedAnswer(answerKey) {
|
|
956
|
+
if (!answerKey) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
this.pruneRecentEmittedAnswers();
|
|
961
|
+
return this.recentEmittedAnswers.some((entry) => entry.key === answerKey);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
rememberEmittedAnswer(answerKey) {
|
|
965
|
+
if (!answerKey) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
this.pruneRecentEmittedAnswers();
|
|
970
|
+
this.recentEmittedAnswers.push({
|
|
971
|
+
key: answerKey,
|
|
972
|
+
timestampMs: Date.now(),
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
if (this.recentEmittedAnswers.length > MAX_RECENT_EMITTED_ANSWERS) {
|
|
976
|
+
this.recentEmittedAnswers = this.recentEmittedAnswers.slice(-MAX_RECENT_EMITTED_ANSWERS);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
506
980
|
takeMirroredInboundRelay(payload) {
|
|
507
981
|
const normalized = sanitizeRelayText(payload);
|
|
508
982
|
if (!normalized) {
|
|
@@ -520,8 +994,28 @@ class ArmedSeat {
|
|
|
520
994
|
return match;
|
|
521
995
|
}
|
|
522
996
|
|
|
997
|
+
getPendingInboundContext() {
|
|
998
|
+
const context = this.pendingInboundContext;
|
|
999
|
+
if (!context) {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (context.expiresAtMs <= Date.now()) {
|
|
1004
|
+
this.pendingInboundContext = null;
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (this.lastUserInputAtMs > context.deliveredAtMs) {
|
|
1009
|
+
this.pendingInboundContext = null;
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return context;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
523
1016
|
collectLiveAnswers() {
|
|
524
|
-
const
|
|
1017
|
+
const childProcesses = getChildProcesses(this.childPid);
|
|
1018
|
+
const detectedAgent = detectAgent(childProcesses);
|
|
525
1019
|
if (!detectedAgent) {
|
|
526
1020
|
this.liveState = {
|
|
527
1021
|
type: null,
|
|
@@ -567,17 +1061,24 @@ class ArmedSeat {
|
|
|
567
1061
|
};
|
|
568
1062
|
}
|
|
569
1063
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
this.
|
|
1064
|
+
const agentPids = getProcessFamilyPids(childProcesses, detectedAgent.pid);
|
|
1065
|
+
const resolvedSessionFile = resolveSessionFile(
|
|
1066
|
+
detectedAgent.type,
|
|
1067
|
+
detectedAgent.pid,
|
|
1068
|
+
currentPath,
|
|
1069
|
+
this.liveState.captureSinceMs,
|
|
1070
|
+
detectedAgent.processStartedAtMs,
|
|
1071
|
+
{
|
|
1072
|
+
agentPids,
|
|
1073
|
+
seatId: this.seatId,
|
|
1074
|
+
sessionName: this.sessionName,
|
|
580
1075
|
}
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
if (resolvedSessionFile && resolvedSessionFile !== this.liveState.sessionFile) {
|
|
1079
|
+
this.liveState.sessionFile = resolvedSessionFile;
|
|
1080
|
+
this.liveState.offset = 0;
|
|
1081
|
+
this.liveState.lastMessageId = null;
|
|
581
1082
|
}
|
|
582
1083
|
|
|
583
1084
|
if (!this.liveState.sessionFile) {
|
|
@@ -643,7 +1144,13 @@ class ArmedSeat {
|
|
|
643
1144
|
}
|
|
644
1145
|
|
|
645
1146
|
const payload = sanitizeRelayText(entry.text);
|
|
646
|
-
if (!payload) {
|
|
1147
|
+
if (!payload || !this.identity || !this.trustState.challenge) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const answerKey = buildAnswerKey(entry, payload);
|
|
1152
|
+
if (this.hasRecentEmittedAnswer(answerKey)) {
|
|
1153
|
+
this.log(`[${this.seatId}] suppressed duplicate final answer: ${previewText(payload)}`);
|
|
647
1154
|
return;
|
|
648
1155
|
}
|
|
649
1156
|
|
|
@@ -653,14 +1160,39 @@ class ArmedSeat {
|
|
|
653
1160
|
return;
|
|
654
1161
|
}
|
|
655
1162
|
|
|
656
|
-
|
|
657
|
-
|
|
1163
|
+
const pendingInboundContext = this.getPendingInboundContext();
|
|
1164
|
+
if (pendingInboundContext && pendingInboundContext.hop >= MAX_RELAY_CHAIN_HOP) {
|
|
1165
|
+
this.log(`[${this.seatId}] suppressed relay loop: ${previewText(payload)}`);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (pendingInboundContext?.relayUsed) {
|
|
1170
|
+
this.log(`[${this.seatId}] suppressed extra queued relay output: ${previewText(payload)}`);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const entryId = entry.id || createId(12);
|
|
1175
|
+
const signedEntry = {
|
|
1176
|
+
id: entryId,
|
|
658
1177
|
type: "answer",
|
|
659
1178
|
seatId: this.seatId,
|
|
660
1179
|
origin: entry.origin || "unknown",
|
|
661
1180
|
text: payload,
|
|
662
1181
|
createdAt: entry.createdAt || new Date().toISOString(),
|
|
663
|
-
|
|
1182
|
+
chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
|
|
1183
|
+
hop: pendingInboundContext ? pendingInboundContext.hop + 1 : 0,
|
|
1184
|
+
challenge: this.trustState.challenge,
|
|
1185
|
+
publicKey: this.identity.publicKey,
|
|
1186
|
+
};
|
|
1187
|
+
signedEntry.signature = signText(
|
|
1188
|
+
buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
|
|
1189
|
+
this.identity.privateKey
|
|
1190
|
+
);
|
|
1191
|
+
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
1192
|
+
this.rememberEmittedAnswer(answerKey);
|
|
1193
|
+
if (pendingInboundContext) {
|
|
1194
|
+
pendingInboundContext.relayUsed = true;
|
|
1195
|
+
}
|
|
664
1196
|
|
|
665
1197
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
666
1198
|
}
|
|
@@ -670,11 +1202,13 @@ class ArmedSeat {
|
|
|
670
1202
|
this.writeStatus({
|
|
671
1203
|
state: "stopping",
|
|
672
1204
|
partnerLive: this.partnerIsLive(),
|
|
1205
|
+
trust: this.trustState.phase,
|
|
673
1206
|
});
|
|
674
1207
|
this.requestStop("stop_requested");
|
|
675
1208
|
return;
|
|
676
1209
|
}
|
|
677
1210
|
|
|
1211
|
+
this.syncTrustState();
|
|
678
1212
|
await this.pullPartnerEvents();
|
|
679
1213
|
if (this.stopped || this.stopRequested()) {
|
|
680
1214
|
this.requestStop("stop_requested");
|
|
@@ -693,6 +1227,8 @@ class ArmedSeat {
|
|
|
693
1227
|
log: live.log,
|
|
694
1228
|
lastAnswerAt: live.lastAnswerAt,
|
|
695
1229
|
partnerLive: this.partnerIsLive(),
|
|
1230
|
+
trust: this.trustState.phase,
|
|
1231
|
+
challengeReady: Boolean(this.trustState.challenge),
|
|
696
1232
|
});
|
|
697
1233
|
}
|
|
698
1234
|
|
|
@@ -704,6 +1240,11 @@ class ArmedSeat {
|
|
|
704
1240
|
|
|
705
1241
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
706
1242
|
this.log("Use this shell normally. Codex, Claude, and Gemini final answers relay automatically from their local session logs.");
|
|
1243
|
+
if (this.seatId === 1) {
|
|
1244
|
+
this.log("Seat 1 generated the session key and is waiting for seat 2 to sign it.");
|
|
1245
|
+
} else {
|
|
1246
|
+
this.log("Seat 2 will sign the session key from seat 1, then relay goes live.");
|
|
1247
|
+
}
|
|
707
1248
|
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
708
1249
|
|
|
709
1250
|
try {
|
|
@@ -764,6 +1305,17 @@ function previewText(text, maxLength = 88) {
|
|
|
764
1305
|
return `${compact.slice(0, maxLength - 3)}...`;
|
|
765
1306
|
}
|
|
766
1307
|
|
|
1308
|
+
function buildAnswerKey(entry, payload) {
|
|
1309
|
+
const origin = String(entry.origin || "unknown").trim() || "unknown";
|
|
1310
|
+
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
1311
|
+
if (id) {
|
|
1312
|
+
return `${origin}:${id}`;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
const createdAt = typeof entry.createdAt === "string" ? entry.createdAt : "";
|
|
1316
|
+
return `${origin}:${createdAt}:${hashText(payload)}`;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
767
1319
|
function buildSeatReport(sessionName, seatId) {
|
|
768
1320
|
const paths = getSeatPaths(sessionName, seatId);
|
|
769
1321
|
const daemon = readJson(paths.daemonPath, null);
|
|
@@ -797,6 +1349,7 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
797
1349
|
relayCount: status?.relayCount || 0,
|
|
798
1350
|
log: status?.log || null,
|
|
799
1351
|
startedAt: meta?.startedAt || null,
|
|
1352
|
+
trust: status?.trust || null,
|
|
800
1353
|
updatedAt: status?.updatedAt || null,
|
|
801
1354
|
lastAnswerAt: status?.lastAnswerAt || null,
|
|
802
1355
|
partnerLive: Boolean(status?.partnerLive),
|
|
@@ -871,6 +1424,7 @@ function stopAllSessions() {
|
|
|
871
1424
|
|
|
872
1425
|
module.exports = {
|
|
873
1426
|
ArmedSeat,
|
|
1427
|
+
buildChildEnv,
|
|
874
1428
|
getStatusReport,
|
|
875
1429
|
resolveSessionName,
|
|
876
1430
|
stopAllSessions,
|