muuuuse 1.4.2 → 1.5.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 +75 -3
- package/src/cli.js +3 -0
- package/src/runtime.js +275 -10
- package/src/util.js +70 -4
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
It does one job:
|
|
6
6
|
- arm terminal one with `muuuuse 1`
|
|
7
7
|
- arm terminal two with `muuuuse 2`
|
|
8
|
+
- have seat 1 generate a session key and seat 2 sign it
|
|
8
9
|
- watch Codex, Claude, or Gemini for real final answers
|
|
9
10
|
- inject that final answer into the other armed terminal
|
|
10
11
|
- keep looping until you stop it
|
|
@@ -32,7 +33,7 @@ Terminal 2:
|
|
|
32
33
|
muuuuse 2
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
Now both shells are armed. Use
|
|
36
|
+
Now both shells are armed. `muuuuse 1` generates the session key, `muuuuse 2` signs it, and only that signed pair relays. Use those shells normally.
|
|
36
37
|
|
|
37
38
|
If you want Codex in one and Gemini in the other, start them inside the armed shells:
|
|
38
39
|
|
|
@@ -62,6 +63,7 @@ muuuuse stop
|
|
|
62
63
|
|
|
63
64
|
- no tmux
|
|
64
65
|
- state lives under `~/.muuuuse`
|
|
66
|
+
- only the signed armed pair can exchange relay events
|
|
65
67
|
- supported final-answer detection is built for Codex, Claude, and Gemini
|
|
66
68
|
- `codeman` remains the larger transport/control layer; `muuuuse` stays local and minimal
|
|
67
69
|
|
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -128,6 +128,57 @@ function selectSessionCandidatePath(candidates, currentPath, processStartedAtMs)
|
|
|
128
128
|
return null;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
function listOpenFilePaths(pid, rootPath) {
|
|
132
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fdRoot = `/proc/${pid}/fd`;
|
|
137
|
+
try {
|
|
138
|
+
const rootPrefix = path.resolve(rootPath);
|
|
139
|
+
const openPaths = fs.readdirSync(fdRoot)
|
|
140
|
+
.map((entry) => {
|
|
141
|
+
try {
|
|
142
|
+
return fs.realpathSync(path.join(fdRoot, entry));
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.filter((entry) => typeof entry === "string")
|
|
148
|
+
.filter((entry) => entry.startsWith(rootPrefix));
|
|
149
|
+
|
|
150
|
+
return [...new Set(openPaths)];
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function selectLiveSessionCandidatePath(candidates, currentPath, captureSinceMs = null) {
|
|
157
|
+
const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
|
|
158
|
+
if (cwdMatches.length === 0) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const primary = cwdMatches.some((candidate) => candidate.isSubagent === false)
|
|
163
|
+
? cwdMatches.filter((candidate) => candidate.isSubagent === false)
|
|
164
|
+
: cwdMatches;
|
|
165
|
+
|
|
166
|
+
const recent = Number.isFinite(captureSinceMs)
|
|
167
|
+
? primary.filter((candidate) => Number.isFinite(candidate.mtimeMs) && candidate.mtimeMs >= captureSinceMs - SESSION_START_EARLY_TOLERANCE_MS)
|
|
168
|
+
: primary;
|
|
169
|
+
const ranked = (recent.length > 0 ? recent : primary)
|
|
170
|
+
.slice()
|
|
171
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs || right.startedAtMs - left.startedAtMs);
|
|
172
|
+
|
|
173
|
+
return ranked[0]?.path || null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readOpenSessionCandidates(pid, rootPath, reader) {
|
|
177
|
+
return listOpenFilePaths(pid, rootPath)
|
|
178
|
+
.map((filePath) => reader(filePath))
|
|
179
|
+
.filter((candidate) => candidate !== null);
|
|
180
|
+
}
|
|
181
|
+
|
|
131
182
|
function readCodexCandidate(filePath) {
|
|
132
183
|
try {
|
|
133
184
|
const [firstLine] = readFirstLines(filePath, 1);
|
|
@@ -143,6 +194,7 @@ function readCodexCandidate(filePath) {
|
|
|
143
194
|
return {
|
|
144
195
|
path: filePath,
|
|
145
196
|
cwd: entry.payload.cwd,
|
|
197
|
+
isSubagent: Boolean(entry.payload?.source?.subagent),
|
|
146
198
|
startedAtMs: Date.parse(entry.payload.timestamp),
|
|
147
199
|
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
148
200
|
};
|
|
@@ -151,7 +203,13 @@ function readCodexCandidate(filePath) {
|
|
|
151
203
|
}
|
|
152
204
|
}
|
|
153
205
|
|
|
154
|
-
function selectCodexSessionFile(currentPath, processStartedAtMs) {
|
|
206
|
+
function selectCodexSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
207
|
+
const liveCandidates = readOpenSessionCandidates(options.pid, CODEX_ROOT, readCodexCandidate);
|
|
208
|
+
const livePath = selectLiveSessionCandidatePath(liveCandidates, currentPath, options.captureSinceMs);
|
|
209
|
+
if (livePath) {
|
|
210
|
+
return livePath;
|
|
211
|
+
}
|
|
212
|
+
|
|
155
213
|
const candidates = walkFiles(CODEX_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
156
214
|
.map((filePath) => readCodexCandidate(filePath))
|
|
157
215
|
.filter((candidate) => candidate !== null);
|
|
@@ -252,7 +310,13 @@ function readClaudeCandidate(filePath) {
|
|
|
252
310
|
}
|
|
253
311
|
}
|
|
254
312
|
|
|
255
|
-
function selectClaudeSessionFile(currentPath, processStartedAtMs) {
|
|
313
|
+
function selectClaudeSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
314
|
+
const liveCandidates = readOpenSessionCandidates(options.pid, CLAUDE_ROOT, readClaudeCandidate);
|
|
315
|
+
const livePath = selectLiveSessionCandidatePath(liveCandidates, currentPath, options.captureSinceMs);
|
|
316
|
+
if (livePath) {
|
|
317
|
+
return livePath;
|
|
318
|
+
}
|
|
319
|
+
|
|
256
320
|
const candidates = walkFiles(CLAUDE_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
257
321
|
.map((filePath) => readClaudeCandidate(filePath))
|
|
258
322
|
.filter((candidate) => candidate !== null);
|
|
@@ -331,8 +395,15 @@ function readGeminiCandidate(filePath) {
|
|
|
331
395
|
}
|
|
332
396
|
}
|
|
333
397
|
|
|
334
|
-
function selectGeminiSessionFile(currentPath, processStartedAtMs) {
|
|
398
|
+
function selectGeminiSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
335
399
|
const projectHash = createHash("sha256").update(currentPath).digest("hex");
|
|
400
|
+
const liveCandidates = readOpenSessionCandidates(options.pid, GEMINI_ROOT, readGeminiCandidate)
|
|
401
|
+
.filter((candidate) => candidate.projectHash === projectHash);
|
|
402
|
+
const livePath = selectLiveSessionCandidatePath(liveCandidates, projectHash, options.captureSinceMs);
|
|
403
|
+
if (livePath) {
|
|
404
|
+
return livePath;
|
|
405
|
+
}
|
|
406
|
+
|
|
336
407
|
const candidates = walkFiles(GEMINI_ROOT, (filePath) => filePath.endsWith(".json"))
|
|
337
408
|
.map((filePath) => readGeminiCandidate(filePath))
|
|
338
409
|
.filter((candidate) => candidate !== null && candidate.projectHash === projectHash);
|
|
@@ -384,6 +455,7 @@ module.exports = {
|
|
|
384
455
|
readClaudeAnswers,
|
|
385
456
|
readCodexAnswers,
|
|
386
457
|
readGeminiAnswers,
|
|
458
|
+
selectLiveSessionCandidatePath,
|
|
387
459
|
selectSessionCandidatePath,
|
|
388
460
|
selectClaudeSessionFile,
|
|
389
461
|
selectCodexSessionFile,
|
package/src/cli.js
CHANGED
package/src/runtime.js
CHANGED
|
@@ -23,11 +23,14 @@ const {
|
|
|
23
23
|
getSessionPaths,
|
|
24
24
|
hashText,
|
|
25
25
|
isPidAlive,
|
|
26
|
+
loadOrCreateSeatIdentity,
|
|
26
27
|
listSessionNames,
|
|
27
28
|
readAppendedText,
|
|
28
29
|
readJson,
|
|
29
30
|
sanitizeRelayText,
|
|
31
|
+
signText,
|
|
30
32
|
sleep,
|
|
33
|
+
verifyText,
|
|
31
34
|
writeJson,
|
|
32
35
|
} = require("./util");
|
|
33
36
|
|
|
@@ -142,23 +145,79 @@ function getChildProcesses(rootPid) {
|
|
|
142
145
|
}
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
function resolveSessionFile(agentType, currentPath, processStartedAtMs) {
|
|
148
|
+
function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, processStartedAtMs) {
|
|
146
149
|
if (!currentPath) {
|
|
147
150
|
return null;
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
const options = {
|
|
154
|
+
pid: agentPid,
|
|
155
|
+
captureSinceMs,
|
|
156
|
+
};
|
|
157
|
+
|
|
150
158
|
if (agentType === "codex") {
|
|
151
|
-
return selectCodexSessionFile(currentPath, processStartedAtMs);
|
|
159
|
+
return selectCodexSessionFile(currentPath, processStartedAtMs, options);
|
|
152
160
|
}
|
|
153
161
|
if (agentType === "claude") {
|
|
154
|
-
return selectClaudeSessionFile(currentPath, processStartedAtMs);
|
|
162
|
+
return selectClaudeSessionFile(currentPath, processStartedAtMs, options);
|
|
155
163
|
}
|
|
156
164
|
if (agentType === "gemini") {
|
|
157
|
-
return selectGeminiSessionFile(currentPath, processStartedAtMs);
|
|
165
|
+
return selectGeminiSessionFile(currentPath, processStartedAtMs, options);
|
|
158
166
|
}
|
|
159
167
|
return null;
|
|
160
168
|
}
|
|
161
169
|
|
|
170
|
+
function buildClaimMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
171
|
+
return JSON.stringify({
|
|
172
|
+
type: "muuuuse_pair_claim",
|
|
173
|
+
sessionName,
|
|
174
|
+
challenge,
|
|
175
|
+
seat1PublicKey,
|
|
176
|
+
seat2PublicKey,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildAckMessage(sessionName, challenge, seat1PublicKey, seat2PublicKey) {
|
|
181
|
+
return JSON.stringify({
|
|
182
|
+
type: "muuuuse_pair_ack",
|
|
183
|
+
sessionName,
|
|
184
|
+
challenge,
|
|
185
|
+
seat1PublicKey,
|
|
186
|
+
seat2PublicKey,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
191
|
+
return JSON.stringify({
|
|
192
|
+
type: "muuuuse_answer",
|
|
193
|
+
sessionName,
|
|
194
|
+
challenge,
|
|
195
|
+
id: entry.id,
|
|
196
|
+
seatId: entry.seatId,
|
|
197
|
+
origin: entry.origin,
|
|
198
|
+
createdAt: entry.createdAt,
|
|
199
|
+
text: entry.text,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readSeatChallenge(paths, sessionName) {
|
|
204
|
+
const record = readJson(paths.challengePath, null);
|
|
205
|
+
if (
|
|
206
|
+
!record ||
|
|
207
|
+
record.sessionName !== sessionName ||
|
|
208
|
+
typeof record.challenge !== "string" ||
|
|
209
|
+
typeof record.publicKey !== "string"
|
|
210
|
+
) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
challenge: record.challenge,
|
|
216
|
+
publicKey: record.publicKey.trim(),
|
|
217
|
+
createdAt: record.createdAt || null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
162
221
|
async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
163
222
|
const lines = String(text || "").replace(/\r/g, "").split("\n");
|
|
164
223
|
|
|
@@ -226,7 +285,14 @@ class ArmedSeat {
|
|
|
226
285
|
this.stdinCleanup = null;
|
|
227
286
|
this.resizeCleanup = null;
|
|
228
287
|
this.forceKillTimer = null;
|
|
288
|
+
this.identity = null;
|
|
229
289
|
this.recentInboundRelays = [];
|
|
290
|
+
this.trustState = {
|
|
291
|
+
challenge: null,
|
|
292
|
+
peerPublicKey: null,
|
|
293
|
+
phase: this.seatId === 1 ? "waiting_for_peer_signature" : "waiting_for_seat1_key",
|
|
294
|
+
pairedAt: null,
|
|
295
|
+
};
|
|
230
296
|
this.liveState = {
|
|
231
297
|
type: null,
|
|
232
298
|
pid: null,
|
|
@@ -272,10 +338,177 @@ class ArmedSeat {
|
|
|
272
338
|
});
|
|
273
339
|
}
|
|
274
340
|
|
|
341
|
+
initializeTrustMaterial() {
|
|
342
|
+
this.identity = loadOrCreateSeatIdentity(this.paths);
|
|
343
|
+
|
|
344
|
+
if (this.seatId !== 1) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
writeJson(this.paths.challengePath, {
|
|
349
|
+
sessionName: this.sessionName,
|
|
350
|
+
challenge: createId(48),
|
|
351
|
+
publicKey: this.identity.publicKey,
|
|
352
|
+
createdAt: new Date().toISOString(),
|
|
353
|
+
});
|
|
354
|
+
this.trustState.challenge = readSeatChallenge(this.paths, this.sessionName)?.challenge || null;
|
|
355
|
+
this.trustState.peerPublicKey = null;
|
|
356
|
+
this.trustState.phase = "waiting_for_peer_signature";
|
|
357
|
+
this.trustState.pairedAt = null;
|
|
358
|
+
fs.rmSync(this.paths.ackPath, { force: true });
|
|
359
|
+
fs.rmSync(this.partnerPaths.claimPath, { force: true });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
syncTrustState() {
|
|
363
|
+
if (!this.identity) {
|
|
364
|
+
this.initializeTrustMaterial();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (this.seatId === 1) {
|
|
368
|
+
this.syncSeatOneTrust();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.syncSeatTwoTrust();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
syncSeatOneTrust() {
|
|
376
|
+
const challengeRecord = readSeatChallenge(this.paths, this.sessionName);
|
|
377
|
+
if (!challengeRecord || challengeRecord.publicKey !== this.identity.publicKey) {
|
|
378
|
+
this.trustState = {
|
|
379
|
+
challenge: null,
|
|
380
|
+
peerPublicKey: null,
|
|
381
|
+
phase: "waiting_for_peer_signature",
|
|
382
|
+
pairedAt: null,
|
|
383
|
+
};
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
this.trustState.challenge = challengeRecord.challenge;
|
|
388
|
+
const claim = readJson(this.partnerPaths.claimPath, null);
|
|
389
|
+
if (
|
|
390
|
+
!claim ||
|
|
391
|
+
claim.sessionName !== this.sessionName ||
|
|
392
|
+
claim.challenge !== challengeRecord.challenge ||
|
|
393
|
+
typeof claim.publicKey !== "string" ||
|
|
394
|
+
typeof claim.signature !== "string" ||
|
|
395
|
+
!verifyText(
|
|
396
|
+
buildClaimMessage(
|
|
397
|
+
this.sessionName,
|
|
398
|
+
challengeRecord.challenge,
|
|
399
|
+
this.identity.publicKey,
|
|
400
|
+
claim.publicKey.trim()
|
|
401
|
+
),
|
|
402
|
+
claim.signature,
|
|
403
|
+
claim.publicKey
|
|
404
|
+
)
|
|
405
|
+
) {
|
|
406
|
+
this.trustState.peerPublicKey = null;
|
|
407
|
+
this.trustState.phase = "waiting_for_peer_signature";
|
|
408
|
+
this.trustState.pairedAt = null;
|
|
409
|
+
fs.rmSync(this.paths.ackPath, { force: true });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const peerPublicKey = claim.publicKey.trim();
|
|
414
|
+
const ackMessage = buildAckMessage(this.sessionName, challengeRecord.challenge, this.identity.publicKey, peerPublicKey);
|
|
415
|
+
const currentAck = readJson(this.paths.ackPath, null);
|
|
416
|
+
const ackIsValid = Boolean(
|
|
417
|
+
currentAck &&
|
|
418
|
+
currentAck.sessionName === this.sessionName &&
|
|
419
|
+
currentAck.challenge === challengeRecord.challenge &&
|
|
420
|
+
currentAck.publicKey === this.identity.publicKey &&
|
|
421
|
+
currentAck.peerPublicKey === peerPublicKey &&
|
|
422
|
+
typeof currentAck.signature === "string" &&
|
|
423
|
+
verifyText(ackMessage, currentAck.signature, this.identity.publicKey)
|
|
424
|
+
);
|
|
425
|
+
if (!ackIsValid) {
|
|
426
|
+
writeJson(this.paths.ackPath, {
|
|
427
|
+
sessionName: this.sessionName,
|
|
428
|
+
challenge: challengeRecord.challenge,
|
|
429
|
+
publicKey: this.identity.publicKey,
|
|
430
|
+
peerPublicKey,
|
|
431
|
+
signature: signText(ackMessage, this.identity.privateKey),
|
|
432
|
+
signedAt: new Date().toISOString(),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const ackRecord = ackIsValid ? currentAck : readJson(this.paths.ackPath, null);
|
|
437
|
+
this.trustState.peerPublicKey = peerPublicKey;
|
|
438
|
+
this.trustState.phase = "paired";
|
|
439
|
+
this.trustState.pairedAt = ackRecord?.signedAt || new Date().toISOString();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
syncSeatTwoTrust() {
|
|
443
|
+
const challengeRecord = readSeatChallenge(this.partnerPaths, this.sessionName);
|
|
444
|
+
if (!challengeRecord) {
|
|
445
|
+
this.trustState = {
|
|
446
|
+
challenge: null,
|
|
447
|
+
peerPublicKey: null,
|
|
448
|
+
phase: "waiting_for_seat1_key",
|
|
449
|
+
pairedAt: null,
|
|
450
|
+
};
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const challenge = challengeRecord.challenge;
|
|
455
|
+
const peerPublicKey = challengeRecord.publicKey;
|
|
456
|
+
const claimPayload = {
|
|
457
|
+
sessionName: this.sessionName,
|
|
458
|
+
challenge,
|
|
459
|
+
publicKey: this.identity.publicKey,
|
|
460
|
+
};
|
|
461
|
+
const claimSignature = signText(
|
|
462
|
+
buildClaimMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
463
|
+
this.identity.privateKey
|
|
464
|
+
);
|
|
465
|
+
const currentClaim = readJson(this.paths.claimPath, null);
|
|
466
|
+
if (
|
|
467
|
+
!currentClaim ||
|
|
468
|
+
currentClaim.sessionName !== claimPayload.sessionName ||
|
|
469
|
+
currentClaim.challenge !== claimPayload.challenge ||
|
|
470
|
+
currentClaim.publicKey !== claimPayload.publicKey ||
|
|
471
|
+
currentClaim.signature !== claimSignature
|
|
472
|
+
) {
|
|
473
|
+
writeJson(this.paths.claimPath, {
|
|
474
|
+
...claimPayload,
|
|
475
|
+
signature: claimSignature,
|
|
476
|
+
signedAt: new Date().toISOString(),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const ack = readJson(this.partnerPaths.ackPath, null);
|
|
481
|
+
const paired = Boolean(
|
|
482
|
+
ack &&
|
|
483
|
+
ack.sessionName === this.sessionName &&
|
|
484
|
+
ack.challenge === challenge &&
|
|
485
|
+
ack.peerPublicKey === this.identity.publicKey &&
|
|
486
|
+
ack.publicKey === peerPublicKey &&
|
|
487
|
+
typeof ack.signature === "string" &&
|
|
488
|
+
verifyText(
|
|
489
|
+
buildAckMessage(this.sessionName, challenge, peerPublicKey, this.identity.publicKey),
|
|
490
|
+
ack.signature,
|
|
491
|
+
peerPublicKey
|
|
492
|
+
)
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
this.trustState.challenge = challenge;
|
|
496
|
+
this.trustState.peerPublicKey = peerPublicKey;
|
|
497
|
+
this.trustState.phase = paired ? "paired" : "waiting_for_pair_ack";
|
|
498
|
+
this.trustState.pairedAt = paired ? (ack.signedAt || new Date().toISOString()) : null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
isPaired() {
|
|
502
|
+
return this.trustState.phase === "paired" &&
|
|
503
|
+
typeof this.trustState.challenge === "string" &&
|
|
504
|
+
typeof this.trustState.peerPublicKey === "string";
|
|
505
|
+
}
|
|
506
|
+
|
|
275
507
|
launchShell() {
|
|
276
508
|
ensureDir(this.paths.dir);
|
|
277
509
|
fs.rmSync(this.paths.pipePath, { force: true });
|
|
278
510
|
clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
|
|
511
|
+
this.initializeTrustMaterial();
|
|
279
512
|
|
|
280
513
|
const shell = resolveShell();
|
|
281
514
|
const shellArgs = resolveShellArgs(shell);
|
|
@@ -294,7 +527,7 @@ class ArmedSeat {
|
|
|
294
527
|
|
|
295
528
|
this.childPid = this.child.pid;
|
|
296
529
|
this.writeMeta();
|
|
297
|
-
this.writeStatus({ state: "running" });
|
|
530
|
+
this.writeStatus({ state: "running", trust: this.trustState.phase });
|
|
298
531
|
|
|
299
532
|
this.child.onData((data) => {
|
|
300
533
|
fs.appendFileSync(this.paths.pipePath, data);
|
|
@@ -441,7 +674,7 @@ class ArmedSeat {
|
|
|
441
674
|
async pullPartnerEvents() {
|
|
442
675
|
const { nextOffset, text } = readAppendedText(this.partnerPaths.eventsPath, this.partnerOffset);
|
|
443
676
|
this.partnerOffset = nextOffset;
|
|
444
|
-
if (!text.trim() || !this.child || this.stopped) {
|
|
677
|
+
if (!text.trim() || !this.child || this.stopped || !this.isPaired()) {
|
|
445
678
|
return;
|
|
446
679
|
}
|
|
447
680
|
|
|
@@ -453,7 +686,20 @@ class ArmedSeat {
|
|
|
453
686
|
}
|
|
454
687
|
|
|
455
688
|
const payload = sanitizeRelayText(entry.text);
|
|
456
|
-
|
|
689
|
+
const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
|
|
690
|
+
id: entry.id,
|
|
691
|
+
seatId: entry.seatId,
|
|
692
|
+
origin: entry.origin || "unknown",
|
|
693
|
+
createdAt: entry.createdAt,
|
|
694
|
+
text: payload,
|
|
695
|
+
});
|
|
696
|
+
if (
|
|
697
|
+
!payload ||
|
|
698
|
+
entry.challenge !== this.trustState.challenge ||
|
|
699
|
+
entry.publicKey !== this.trustState.peerPublicKey ||
|
|
700
|
+
typeof entry.signature !== "string" ||
|
|
701
|
+
!verifyText(signaturePayload, entry.signature, this.trustState.peerPublicKey)
|
|
702
|
+
) {
|
|
457
703
|
continue;
|
|
458
704
|
}
|
|
459
705
|
|
|
@@ -570,7 +816,9 @@ class ArmedSeat {
|
|
|
570
816
|
if (!this.liveState.sessionFile) {
|
|
571
817
|
this.liveState.sessionFile = resolveSessionFile(
|
|
572
818
|
detectedAgent.type,
|
|
819
|
+
detectedAgent.pid,
|
|
573
820
|
currentPath,
|
|
821
|
+
this.liveState.captureSinceMs,
|
|
574
822
|
detectedAgent.processStartedAtMs
|
|
575
823
|
);
|
|
576
824
|
|
|
@@ -643,7 +891,7 @@ class ArmedSeat {
|
|
|
643
891
|
}
|
|
644
892
|
|
|
645
893
|
const payload = sanitizeRelayText(entry.text);
|
|
646
|
-
if (!payload) {
|
|
894
|
+
if (!payload || !this.identity || !this.trustState.challenge) {
|
|
647
895
|
return;
|
|
648
896
|
}
|
|
649
897
|
|
|
@@ -653,14 +901,21 @@ class ArmedSeat {
|
|
|
653
901
|
return;
|
|
654
902
|
}
|
|
655
903
|
|
|
656
|
-
|
|
904
|
+
const signedEntry = {
|
|
657
905
|
id: entry.id || createId(12),
|
|
658
906
|
type: "answer",
|
|
659
907
|
seatId: this.seatId,
|
|
660
908
|
origin: entry.origin || "unknown",
|
|
661
909
|
text: payload,
|
|
662
910
|
createdAt: entry.createdAt || new Date().toISOString(),
|
|
663
|
-
|
|
911
|
+
challenge: this.trustState.challenge,
|
|
912
|
+
publicKey: this.identity.publicKey,
|
|
913
|
+
};
|
|
914
|
+
signedEntry.signature = signText(
|
|
915
|
+
buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
|
|
916
|
+
this.identity.privateKey
|
|
917
|
+
);
|
|
918
|
+
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
664
919
|
|
|
665
920
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
666
921
|
}
|
|
@@ -670,11 +925,13 @@ class ArmedSeat {
|
|
|
670
925
|
this.writeStatus({
|
|
671
926
|
state: "stopping",
|
|
672
927
|
partnerLive: this.partnerIsLive(),
|
|
928
|
+
trust: this.trustState.phase,
|
|
673
929
|
});
|
|
674
930
|
this.requestStop("stop_requested");
|
|
675
931
|
return;
|
|
676
932
|
}
|
|
677
933
|
|
|
934
|
+
this.syncTrustState();
|
|
678
935
|
await this.pullPartnerEvents();
|
|
679
936
|
if (this.stopped || this.stopRequested()) {
|
|
680
937
|
this.requestStop("stop_requested");
|
|
@@ -693,6 +950,8 @@ class ArmedSeat {
|
|
|
693
950
|
log: live.log,
|
|
694
951
|
lastAnswerAt: live.lastAnswerAt,
|
|
695
952
|
partnerLive: this.partnerIsLive(),
|
|
953
|
+
trust: this.trustState.phase,
|
|
954
|
+
challengeReady: Boolean(this.trustState.challenge),
|
|
696
955
|
});
|
|
697
956
|
}
|
|
698
957
|
|
|
@@ -704,6 +963,11 @@ class ArmedSeat {
|
|
|
704
963
|
|
|
705
964
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
706
965
|
this.log("Use this shell normally. Codex, Claude, and Gemini final answers relay automatically from their local session logs.");
|
|
966
|
+
if (this.seatId === 1) {
|
|
967
|
+
this.log("Seat 1 generated the session key and is waiting for seat 2 to sign it.");
|
|
968
|
+
} else {
|
|
969
|
+
this.log("Seat 2 will sign the session key from seat 1, then relay goes live.");
|
|
970
|
+
}
|
|
707
971
|
this.log("Run `muuuuse status` or `muuuuse stop` from any terminal.");
|
|
708
972
|
|
|
709
973
|
try {
|
|
@@ -797,6 +1061,7 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
797
1061
|
relayCount: status?.relayCount || 0,
|
|
798
1062
|
log: status?.log || null,
|
|
799
1063
|
startedAt: meta?.startedAt || null,
|
|
1064
|
+
trust: status?.trust || null,
|
|
800
1065
|
updatedAt: status?.updatedAt || null,
|
|
801
1066
|
lastAnswerAt: status?.lastAnswerAt || null,
|
|
802
1067
|
partnerLive: Boolean(status?.partnerLive),
|
package/src/util.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
createHash,
|
|
3
|
+
generateKeyPairSync,
|
|
4
|
+
randomBytes,
|
|
5
|
+
sign: cryptoSign,
|
|
6
|
+
verify: cryptoVerify,
|
|
7
|
+
} = require("node:crypto");
|
|
2
8
|
const fs = require("node:fs");
|
|
3
9
|
const os = require("node:os");
|
|
4
10
|
const path = require("node:path");
|
|
@@ -28,6 +34,13 @@ function writeJson(filePath, value) {
|
|
|
28
34
|
fs.renameSync(tempPath, filePath);
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
function writeSecret(filePath, value, mode = 0o600) {
|
|
38
|
+
ensureDir(path.dirname(filePath));
|
|
39
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
40
|
+
fs.writeFileSync(tempPath, String(value), { mode });
|
|
41
|
+
fs.renameSync(tempPath, filePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
function readJson(filePath, fallback = null) {
|
|
32
45
|
try {
|
|
33
46
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
@@ -161,15 +174,63 @@ function getSeatDir(sessionName, seatId) {
|
|
|
161
174
|
function getSeatPaths(sessionName, seatId) {
|
|
162
175
|
const dir = getSeatDir(sessionName, seatId);
|
|
163
176
|
return {
|
|
177
|
+
ackPath: path.join(dir, "ack.json"),
|
|
178
|
+
challengePath: path.join(dir, "challenge.json"),
|
|
179
|
+
claimPath: path.join(dir, "claim.json"),
|
|
164
180
|
dir,
|
|
165
181
|
daemonPath: path.join(dir, "daemon.json"),
|
|
166
182
|
eventsPath: path.join(dir, "events.jsonl"),
|
|
183
|
+
privateKeyPath: path.join(dir, "id_ed25519"),
|
|
184
|
+
publicKeyPath: path.join(dir, "id_ed25519.pub"),
|
|
167
185
|
metaPath: path.join(dir, "meta.json"),
|
|
168
186
|
pipePath: path.join(dir, "pipe.log"),
|
|
169
187
|
statusPath: path.join(dir, "status.json"),
|
|
170
188
|
};
|
|
171
189
|
}
|
|
172
190
|
|
|
191
|
+
function loadOrCreateSeatIdentity(paths) {
|
|
192
|
+
try {
|
|
193
|
+
const privateKey = fs.readFileSync(paths.privateKeyPath, "utf8").trim();
|
|
194
|
+
const publicKey = fs.readFileSync(paths.publicKeyPath, "utf8").trim();
|
|
195
|
+
if (privateKey && publicKey) {
|
|
196
|
+
return { privateKey, publicKey };
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// generate below
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
203
|
+
const privateKeyPem = privateKey.export({ format: "pem", type: "pkcs8" });
|
|
204
|
+
const publicKeyPem = publicKey.export({ format: "pem", type: "spki" });
|
|
205
|
+
writeSecret(paths.privateKeyPath, privateKeyPem);
|
|
206
|
+
writeSecret(paths.publicKeyPath, publicKeyPem, 0o644);
|
|
207
|
+
return {
|
|
208
|
+
privateKey: String(privateKeyPem).trim(),
|
|
209
|
+
publicKey: String(publicKeyPem).trim(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function signText(text, privateKey) {
|
|
214
|
+
return cryptoSign(null, Buffer.from(String(text || ""), "utf8"), privateKey).toString("base64");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function verifyText(text, signature, publicKey) {
|
|
218
|
+
if (!signature || !publicKey) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
return cryptoVerify(
|
|
224
|
+
null,
|
|
225
|
+
Buffer.from(String(text || ""), "utf8"),
|
|
226
|
+
publicKey,
|
|
227
|
+
Buffer.from(String(signature || ""), "base64")
|
|
228
|
+
);
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
173
234
|
function listSessionNames() {
|
|
174
235
|
const sessionsRoot = path.join(getStateRoot(), "sessions");
|
|
175
236
|
try {
|
|
@@ -195,9 +256,10 @@ function usage() {
|
|
|
195
256
|
"Flow:",
|
|
196
257
|
" 1. Run `muuuuse 1` in terminal one.",
|
|
197
258
|
" 2. Run `muuuuse 2` in terminal two.",
|
|
198
|
-
" 3.
|
|
199
|
-
" 4.
|
|
200
|
-
" 5.
|
|
259
|
+
" 3. Seat 1 generates the session key and seat 2 signs it automatically.",
|
|
260
|
+
" 4. Use those armed shells normally.",
|
|
261
|
+
" 5. Codex, Claude, and Gemini final answers relay automatically from their local session logs.",
|
|
262
|
+
" 6. Run `muuuuse status` or `muuuuse stop` from any shell.",
|
|
201
263
|
"",
|
|
202
264
|
"Notes:",
|
|
203
265
|
" - No tmux.",
|
|
@@ -215,6 +277,7 @@ module.exports = {
|
|
|
215
277
|
ensureDir,
|
|
216
278
|
getDefaultSessionName,
|
|
217
279
|
getFileSize,
|
|
280
|
+
loadOrCreateSeatIdentity,
|
|
218
281
|
getSeatPaths,
|
|
219
282
|
getSessionPaths,
|
|
220
283
|
getStateRoot,
|
|
@@ -224,7 +287,10 @@ module.exports = {
|
|
|
224
287
|
readAppendedText,
|
|
225
288
|
readJson,
|
|
226
289
|
sanitizeRelayText,
|
|
290
|
+
signText,
|
|
227
291
|
sleep,
|
|
228
292
|
usage,
|
|
293
|
+
verifyText,
|
|
229
294
|
writeJson,
|
|
295
|
+
writeSecret,
|
|
230
296
|
};
|