privateer-agent 0.15.4 → 0.15.6
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 +11 -0
- package/bin/boot-bundle.d.mts +2 -0
- package/bin/boot-bundle.mjs +44 -0
- package/bin/fresh-supervisor.d.mts +5 -1
- package/bin/fresh-supervisor.mjs +47 -10
- package/bin/privateer-launch.mjs +27 -3
- package/bin/privateer-subagent.mjs +5 -2
- package/bin/privateer-verify.mjs +12 -0
- package/bin/session-recovery.d.mts +53 -0
- package/bin/session-recovery.mjs +240 -0
- package/bin/session-seal-preload.mjs +17 -0
- package/bin/session-seal.d.mts +25 -0
- package/bin/session-seal.mjs +242 -0
- package/dist-boot/boot.mjs +3 -0
- package/dist-boot/c-27JKSMWL.mjs +1460 -0
- package/dist-boot/c-2BMRFV24.mjs +269 -0
- package/dist-boot/c-2CTF6JA3.mjs +166 -0
- package/dist-boot/c-2G3BBHRJ.mjs +84 -0
- package/dist-boot/c-2IDLKXX6.mjs +250 -0
- package/dist-boot/c-2NJD5VYM.mjs +8 -0
- package/dist-boot/c-2S5EEQPW.mjs +1602 -0
- package/dist-boot/c-2WRDCUVI.mjs +25 -0
- package/dist-boot/c-2YN3M256.mjs +14 -0
- package/dist-boot/c-37GM3GMU.mjs +187 -0
- package/dist-boot/c-3STKRSVO.mjs +2480 -0
- package/dist-boot/c-3TPV6TQQ.mjs +2320 -0
- package/dist-boot/c-3UVHLQDJ.mjs +225 -0
- package/dist-boot/c-42PUUZYC.mjs +697 -0
- package/dist-boot/c-4D5UMWVW.mjs +683 -0
- package/dist-boot/c-4ETZRMTA.mjs +46 -0
- package/dist-boot/c-4QT25BD5.mjs +1482 -0
- package/dist-boot/c-4WVR32AP.mjs +2579 -0
- package/dist-boot/c-5EMM567G.mjs +152 -0
- package/dist-boot/c-5FA4CV63.mjs +145 -0
- package/dist-boot/c-5GV3X3RB.mjs +14 -0
- package/dist-boot/c-5JCEPRIY.mjs +52 -0
- package/dist-boot/c-5NDKIIWT.mjs +16 -0
- package/dist-boot/c-5QL3AR26.mjs +4665 -0
- package/dist-boot/c-5XY6MFKN.mjs +5142 -0
- package/dist-boot/c-65UZIZC7.mjs +2897 -0
- package/dist-boot/c-6FUXKHOK.mjs +1969 -0
- package/dist-boot/c-6JHWQE4V.mjs +18242 -0
- package/dist-boot/c-6LOBR4I6.mjs +5280 -0
- package/dist-boot/c-6QHOZXKQ.mjs +14394 -0
- package/dist-boot/c-6ZO2SADQ.mjs +178 -0
- package/dist-boot/c-6ZORVVZP.mjs +1449 -0
- package/dist-boot/c-76QH4UCP.mjs +2441 -0
- package/dist-boot/c-77GO5L5Y.mjs +20982 -0
- package/dist-boot/c-7M23XFTN.mjs +5052 -0
- package/dist-boot/c-7TC367E2.mjs +32 -0
- package/dist-boot/c-7UOG7POX.mjs +1524 -0
- package/dist-boot/c-7VHP3FLN.mjs +376 -0
- package/dist-boot/c-A73JHZP2.mjs +13207 -0
- package/dist-boot/c-ABXULAFL.mjs +71 -0
- package/dist-boot/c-ANDD2AQO.mjs +74 -0
- package/dist-boot/c-AOY77AZL.mjs +1430 -0
- package/dist-boot/c-AXEPZ22I.mjs +108 -0
- package/dist-boot/c-B2Y7VNHU.mjs +427 -0
- package/dist-boot/c-B7VJUH73.mjs +77 -0
- package/dist-boot/c-BFW6CDVL.mjs +24 -0
- package/dist-boot/c-BHNIF2AR.mjs +188 -0
- package/dist-boot/c-BKF2O6BL.mjs +23 -0
- package/dist-boot/c-BOZXFTZN.mjs +8 -0
- package/dist-boot/c-BRNYJWZL.mjs +28 -0
- package/dist-boot/c-C3N4Z2HO.mjs +93 -0
- package/dist-boot/c-C4IV7ZFG.mjs +717 -0
- package/dist-boot/c-C5NK6UYV.mjs +793 -0
- package/dist-boot/c-C67644MB.mjs +1659 -0
- package/dist-boot/c-C6S2YB53.mjs +620 -0
- package/dist-boot/c-CDBEJSBF.mjs +37 -0
- package/dist-boot/c-CG47HBKU.mjs +435 -0
- package/dist-boot/c-CKCTWCT5.mjs +51 -0
- package/dist-boot/c-CN77DCGF.mjs +60 -0
- package/dist-boot/c-CRBXNKFO.mjs +1795 -0
- package/dist-boot/c-CRIHASQ6.mjs +973 -0
- package/dist-boot/c-CU4NGAUO.mjs +3786 -0
- package/dist-boot/c-CWRFLGQU.mjs +1883 -0
- package/dist-boot/c-CYQB2USO.mjs +32 -0
- package/dist-boot/c-DEE67DJO.mjs +379 -0
- package/dist-boot/c-DIFDBYOP.mjs +1258 -0
- package/dist-boot/c-DOSGVXDZ.mjs +34879 -0
- package/dist-boot/c-DR7XQI32.mjs +381 -0
- package/dist-boot/c-DTGPSB46.mjs +14666 -0
- package/dist-boot/c-DTHDNA4P.mjs +320 -0
- package/dist-boot/c-E4Z3Y5B5.mjs +3864 -0
- package/dist-boot/c-E66Z2TI7.mjs +203 -0
- package/dist-boot/c-EBU6RLRV.mjs +1527 -0
- package/dist-boot/c-EBZZNGSF.mjs +432 -0
- package/dist-boot/c-EC3LJ2KF.mjs +1032 -0
- package/dist-boot/c-EQBKXWNX.mjs +15829 -0
- package/dist-boot/c-EUEPIW4S.mjs +574 -0
- package/dist-boot/c-EX4YVWAT.mjs +378 -0
- package/dist-boot/c-EYZGSAJU.mjs +98 -0
- package/dist-boot/c-F75VRKY4.mjs +172 -0
- package/dist-boot/c-FH4NXKHV.mjs +177 -0
- package/dist-boot/c-FHNUCTEM.mjs +140 -0
- package/dist-boot/c-FJ5T42DS.mjs +5349 -0
- package/dist-boot/c-FPCNRL7G.mjs +1728 -0
- package/dist-boot/c-G4DLN7Q4.mjs +8 -0
- package/dist-boot/c-G56GYQFZ.mjs +2507 -0
- package/dist-boot/c-GLIOERDQ.mjs +1200 -0
- package/dist-boot/c-GMJ5HUBM.mjs +14 -0
- package/dist-boot/c-H3HSGMAQ.mjs +461 -0
- package/dist-boot/c-HIKTO6P7.mjs +275 -0
- package/dist-boot/c-I2MCJ34O.mjs +464 -0
- package/dist-boot/c-I3C6LBW4.mjs +765 -0
- package/dist-boot/c-I54OKQHW.mjs +387 -0
- package/dist-boot/c-IFSP3R4D.mjs +80 -0
- package/dist-boot/c-IZK77VB7.mjs +171 -0
- package/dist-boot/c-J3Y25UGE.mjs +54 -0
- package/dist-boot/c-J5IESS7W.mjs +27750 -0
- package/dist-boot/c-JJWZBY7L.mjs +254 -0
- package/dist-boot/c-JKTFXTGF.mjs +463 -0
- package/dist-boot/c-JN57AXQ4.mjs +77 -0
- package/dist-boot/c-KFIO6LUW.mjs +1096 -0
- package/dist-boot/c-KIEMEFWW.mjs +478 -0
- package/dist-boot/c-KIVXWBYD.mjs +28 -0
- package/dist-boot/c-KP6CRIP4.mjs +580 -0
- package/dist-boot/c-L26VOJX4.mjs +11558 -0
- package/dist-boot/c-L5GHWG5G.mjs +5988 -0
- package/dist-boot/c-M7NSQAKK.mjs +353 -0
- package/dist-boot/c-MCEWOPBW.mjs +56 -0
- package/dist-boot/c-MGPIEG7I.mjs +20 -0
- package/dist-boot/c-NREXJV3M.mjs +393 -0
- package/dist-boot/c-O5PP6CTX.mjs +162 -0
- package/dist-boot/c-OBQDHSXM.mjs +1774 -0
- package/dist-boot/c-OLQHHDD4.mjs +21 -0
- package/dist-boot/c-OXW4JQQH.mjs +319 -0
- package/dist-boot/c-P3D7ZUWN.mjs +1288 -0
- package/dist-boot/c-P4PQZVV5.mjs +948 -0
- package/dist-boot/c-P63T3XL2.mjs +26373 -0
- package/dist-boot/c-P6N5DQGZ.mjs +194 -0
- package/dist-boot/c-P7MLKQFC.mjs +58 -0
- package/dist-boot/c-PB3PZJL5.mjs +1276 -0
- package/dist-boot/c-PTN4S6G2.mjs +1259 -0
- package/dist-boot/c-PYCE5J7U.mjs +1083 -0
- package/dist-boot/c-Q4HYCRLB.mjs +19410 -0
- package/dist-boot/c-QUXMETGG.mjs +68 -0
- package/dist-boot/c-RDMJZCOX.mjs +356 -0
- package/dist-boot/c-ROGJK76M.mjs +17 -0
- package/dist-boot/c-RQJJ26T4.mjs +708 -0
- package/dist-boot/c-RSKCUPJF.mjs +7559 -0
- package/dist-boot/c-RVJU4O6P.mjs +37 -0
- package/dist-boot/c-S2RVHR3M.mjs +165 -0
- package/dist-boot/c-SKMK3PEE.mjs +91120 -0
- package/dist-boot/c-TI7SM4EG.mjs +20 -0
- package/dist-boot/c-TILJ6GXD.mjs +1235 -0
- package/dist-boot/c-TT6BCUJD.mjs +114 -0
- package/dist-boot/c-U4ITOXAN.mjs +2107 -0
- package/dist-boot/c-UGERXVO2.mjs +7120 -0
- package/dist-boot/c-UPBH5AXY.mjs +1363 -0
- package/dist-boot/c-US2PUNZL.mjs +597 -0
- package/dist-boot/c-UU4WZPPE.mjs +20 -0
- package/dist-boot/c-UWVIYPQS.mjs +39 -0
- package/dist-boot/c-V4RHQBPQ.mjs +17683 -0
- package/dist-boot/c-V5MRVK47.mjs +351 -0
- package/dist-boot/c-V6CL266R.mjs +1755 -0
- package/dist-boot/c-VJTMFKYE.mjs +14975 -0
- package/dist-boot/c-VJTRUKIB.mjs +773 -0
- package/dist-boot/c-W4IFSHPW.mjs +43 -0
- package/dist-boot/c-WETGB2CF.mjs +113 -0
- package/dist-boot/c-WO3QSZUU.mjs +1337 -0
- package/dist-boot/c-WQLH73TY.mjs +242 -0
- package/dist-boot/c-XL735JW5.mjs +500 -0
- package/dist-boot/c-Y7HADL5B.mjs +1107 -0
- package/dist-boot/c-YGQHSVSK.mjs +41 -0
- package/dist-boot/c-YMP2AMEB.mjs +10350 -0
- package/dist-boot/c-YRDO2WEF.mjs +12012 -0
- package/dist-boot/c-YUSTRATJ.mjs +8 -0
- package/dist-boot/c-Z2262R22.mjs +2697 -0
- package/dist-boot/c-Z4ALQMJC.mjs +709 -0
- package/dist-boot/c-Z5ZUQVUY.mjs +62 -0
- package/dist-boot/c-ZDZUKXF7.mjs +874 -0
- package/dist-boot/c-ZJYLGIC3.mjs +32 -0
- package/dist-boot/c-ZNA5CDMI.mjs +77 -0
- package/dist-boot/c-ZSO6DI3M.mjs +204 -0
- package/dist-boot/c-ZWUVHKMK.mjs +100 -0
- package/dist-boot/c-ZY4MESEW.mjs +832 -0
- package/dist-boot/c-ZZ5REI4L.mjs +329 -0
- package/dist-boot/cli.mjs +51 -0
- package/dist-boot/ext-anthropic-attribution.mjs +207 -0
- package/dist-boot/ext-pi-background-tasks.mjs +7234 -0
- package/dist-boot/ext-pi-subagents.mjs +37 -0
- package/dist-boot/ext-privateer-account.mjs +4426 -0
- package/dist-boot/ext-privateer-brand.mjs +5274 -0
- package/dist-boot/ext-privateer-codemode.mjs +37 -0
- package/dist-boot/ext-privateer-computer.mjs +626 -0
- package/dist-boot/ext-privateer-connect.mjs +1862 -0
- package/dist-boot/ext-privateer-context.mjs +323 -0
- package/dist-boot/ext-privateer-desktop.mjs +92 -0
- package/dist-boot/ext-privateer-final-audit.mjs +48 -0
- package/dist-boot/ext-privateer-fresh.mjs +81 -0
- package/dist-boot/ext-privateer-gate.mjs +5959 -0
- package/dist-boot/ext-privateer-hints.mjs +236 -0
- package/dist-boot/ext-privateer-media.mjs +2626 -0
- package/dist-boot/ext-privateer-models.mjs +3397 -0
- package/dist-boot/ext-privateer-posture.mjs +3006 -0
- package/dist-boot/ext-privateer-privacy.mjs +4578 -0
- package/dist-boot/ext-privateer-recovery.mjs +287 -0
- package/dist-boot/ext-privateer-speak.mjs +4754 -0
- package/dist-boot/ext-privateer-terminals.mjs +725 -0
- package/dist-boot/ext-privateer-tools.mjs +528 -0
- package/dist-boot/ext-privateer-truncation.mjs +74 -0
- package/dist-boot/ext-privateer-update.mjs +210 -0
- package/dist-boot/ext-privateer-vision.mjs +148 -0
- package/dist-boot/ext-privateer-web.mjs +543 -0
- package/dist-boot/ext-rpiv-ask-user-question.mjs +710 -0
- package/dist-boot/manifest.json +9 -0
- package/extensions/privateer-brand.ts +3 -1
- package/extensions/privateer-context.ts +4 -2
- package/extensions/privateer-final-audit.ts +17 -0
- package/extensions/privateer-gate.ts +55 -9
- package/extensions/privateer-recovery.ts +175 -0
- package/extensions/privateer-web.ts +4 -4
- package/package.json +3 -1
- package/patches/@earendil-works+pi-coding-agent+1.3.0.patch +199 -2
- package/patches/pi-background-tasks+2.6.10.patch +91 -7
- package/src/cli/chat.ts +7 -1
- package/src/config/moatManifest.json +2 -0
- package/src/config/whatsNew.json +4 -4
- package/src/context.ts +33 -5
- package/src/engine/finalAudit.ts +38 -0
- package/src/engine/stallWatchdog.ts +8 -1
- package/src/providers/account.ts +34 -3
- package/src/remote/attentionPush.ts +40 -0
- package/src/remote/liveTaskSession.ts +7 -1
- package/src/remote/relayClient.ts +38 -4
- package/src/remote/remoteBridge.ts +17 -0
- package/src/remote/transcriptSnapshot.ts +104 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Session sealing — Pi's session transcripts, encrypted at rest.
|
|
2
|
+
//
|
|
3
|
+
// Pi writes every conversation to ~/.privateer/agent/sessions/<cwd>/<ts>_<id>.jsonl,
|
|
4
|
+
// one JSON entry per line, and until now in plaintext: every prompt, every reply,
|
|
5
|
+
// every file the agent read and every command's output, readable by anything that
|
|
6
|
+
// can read the home directory — backup and sync tools, indexers, a stray `grep -r`,
|
|
7
|
+
// any other program the user runs.
|
|
8
|
+
//
|
|
9
|
+
// Now each line except the header is sealed with AES-256-GCM:
|
|
10
|
+
// {"pvs":1,"iv":"<b64 12 bytes>","ct":"<b64 ciphertext‖tag>"}
|
|
11
|
+
// The HEADER (type "session": id, timestamp, cwd) stays readable on purpose. Pi
|
|
12
|
+
// finds and lists sessions by reading only the first line of each file, and the cwd
|
|
13
|
+
// it holds is already spelled out in the directory name, so sealing it would cost
|
|
14
|
+
// session discovery and hide nothing.
|
|
15
|
+
//
|
|
16
|
+
// The patch in patches/@earendil-works+pi-coding-agent+*.patch routes Pi's reads
|
|
17
|
+
// (parseSessionEntryLine / parseSessionEntries) and writes (every JSON.stringify of
|
|
18
|
+
// an entry) through the codec this module installs on globalThis. Without the codec
|
|
19
|
+
// — Pi run by hand, outside `privateer` — Pi writes plaintext as before, and sealed
|
|
20
|
+
// lines read as absent rather than as garbage. Readers accept both forms forever, so
|
|
21
|
+
// files written before this change, and files of mixed form, keep working.
|
|
22
|
+
//
|
|
23
|
+
// THE KEY, AND WHAT THIS DOES NOT DO. One random 32-byte key per machine, in
|
|
24
|
+
// $PRIVATEER_HOME/session-key.json (0600) — next to the data, like this tool's other
|
|
25
|
+
// secrets (terminal-key.json, credentials). So this protects transcripts from
|
|
26
|
+
// anything that reads the session FILES (indexers, a shared or synced sessions
|
|
27
|
+
// folder, an attachment pasted from the wrong directory, tools that scan for
|
|
28
|
+
// .jsonl), and it does NOT protect them from someone who can read the whole
|
|
29
|
+
// ~/.privateer as this user: that reader has the key too. Say exactly that, never
|
|
30
|
+
// more. An OS keychain would move the key out of the folder, at the price of
|
|
31
|
+
// sessions that can't open over ssh or with the keychain locked — a different
|
|
32
|
+
// trade, not a free upgrade.
|
|
33
|
+
//
|
|
34
|
+
// Never mint a key over one that exists: a second key orphans every session sealed
|
|
35
|
+
// under the first. A key file that is present but unreadable or malformed is an
|
|
36
|
+
// error (sealing is switched off and the user is told), never a reason to start over.
|
|
37
|
+
|
|
38
|
+
import crypto from "node:crypto";
|
|
39
|
+
import fs from "node:fs";
|
|
40
|
+
import path from "node:path";
|
|
41
|
+
|
|
42
|
+
export const CODEC_KEY = Symbol.for("privateer.sessionCodec");
|
|
43
|
+
const KEY_FILE = "session-key.json";
|
|
44
|
+
|
|
45
|
+
export function keyPath(home) {
|
|
46
|
+
return path.join(home, KEY_FILE);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseKeyFile(text) {
|
|
50
|
+
const parsed = JSON.parse(text);
|
|
51
|
+
const key = Buffer.from(String(parsed?.key ?? ""), "base64");
|
|
52
|
+
if (parsed?.v !== 1 || key.length !== 32) throw new Error("session key file is malformed");
|
|
53
|
+
return key;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load this machine's session key, minting it only when no key file exists at all.
|
|
58
|
+
* Two terminals starting at once race on `wx`: the loser re-reads the winner's key.
|
|
59
|
+
*/
|
|
60
|
+
export function loadOrCreateKey(home) {
|
|
61
|
+
const file = keyPath(home);
|
|
62
|
+
try {
|
|
63
|
+
return parseKeyFile(fs.readFileSync(file, "utf8"));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (e?.code !== "ENOENT") throw e;
|
|
66
|
+
}
|
|
67
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
68
|
+
const key = crypto.randomBytes(32);
|
|
69
|
+
try {
|
|
70
|
+
fs.writeFileSync(file, JSON.stringify({ v: 1, key: key.toString("base64"), created: new Date().toISOString() }) + "\n", { mode: 0o600, flag: "wx" });
|
|
71
|
+
return key;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e?.code === "EEXIST") return parseKeyFile(fs.readFileSync(file, "utf8"));
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isSealed(obj) {
|
|
79
|
+
return !!obj && typeof obj === "object" && obj.pvs === 1 && typeof obj.iv === "string" && typeof obj.ct === "string";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One entry → one line (no newline). The session header is never sealed. */
|
|
83
|
+
export function encodeEntry(entry, key) {
|
|
84
|
+
if (entry?.type === "session") return JSON.stringify(entry);
|
|
85
|
+
const iv = crypto.randomBytes(12);
|
|
86
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
87
|
+
const ct = Buffer.concat([cipher.update(JSON.stringify(entry), "utf8"), cipher.final(), cipher.getAuthTag()]);
|
|
88
|
+
return JSON.stringify({ pvs: 1, iv: iv.toString("base64"), ct: ct.toString("base64") });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A parsed line → the entry. Plaintext passes through; a line that fails to open → null. */
|
|
92
|
+
export function decodeEntry(obj, key) {
|
|
93
|
+
if (!isSealed(obj)) return obj;
|
|
94
|
+
try {
|
|
95
|
+
const iv = Buffer.from(obj.iv, "base64");
|
|
96
|
+
const raw = Buffer.from(obj.ct, "base64");
|
|
97
|
+
if (iv.length !== 12 || raw.length < 17) return null;
|
|
98
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
99
|
+
decipher.setAuthTag(raw.subarray(raw.length - 16));
|
|
100
|
+
const text = Buffer.concat([decipher.update(raw.subarray(0, raw.length - 16)), decipher.final()]).toString("utf8");
|
|
101
|
+
return JSON.parse(text);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Install the codec Pi's patched session manager calls. Returns { ok, error? }.
|
|
109
|
+
* `failures` counts lines that would not open (a different machine's key, a damaged
|
|
110
|
+
* file), so the agent can say so instead of showing a silently shorter session.
|
|
111
|
+
*/
|
|
112
|
+
export function installCodec(home) {
|
|
113
|
+
let key;
|
|
114
|
+
try {
|
|
115
|
+
key = loadOrCreateKey(home);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
delete globalThis[CODEC_KEY];
|
|
118
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
119
|
+
}
|
|
120
|
+
const codec = {
|
|
121
|
+
failures: 0,
|
|
122
|
+
encode: (entry) => encodeEntry(entry, key),
|
|
123
|
+
decode: (obj) => {
|
|
124
|
+
const out = decodeEntry(obj, key);
|
|
125
|
+
if (out === null) codec.failures++;
|
|
126
|
+
return out;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
globalThis[CODEC_KEY] = codec;
|
|
130
|
+
return { ok: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Seal one existing file in place. Lines that are already sealed, the header, and
|
|
135
|
+
* lines that don't parse are carried over byte-for-byte (a malformed line is Pi's to
|
|
136
|
+
* skip, not ours to drop). Written to a sibling temp file, VERIFIED by reading it
|
|
137
|
+
* back, then renamed over the original only if the original hasn't changed since it
|
|
138
|
+
* was read — a session another terminal is appending to is left for a later pass.
|
|
139
|
+
* The file keeps its original atime/mtime (Pi sorts sessions by mtime).
|
|
140
|
+
* Returns "sealed" | "unchanged" | "busy" | "failed".
|
|
141
|
+
*/
|
|
142
|
+
export function sealFile(file, key) {
|
|
143
|
+
let before, text;
|
|
144
|
+
try {
|
|
145
|
+
before = fs.statSync(file);
|
|
146
|
+
text = fs.readFileSync(file, "utf8");
|
|
147
|
+
} catch {
|
|
148
|
+
return "failed";
|
|
149
|
+
}
|
|
150
|
+
const lines = text.split("\n");
|
|
151
|
+
let changed = false;
|
|
152
|
+
const out = lines.map((line) => {
|
|
153
|
+
if (!line.trim()) return line;
|
|
154
|
+
let obj;
|
|
155
|
+
try {
|
|
156
|
+
obj = JSON.parse(line);
|
|
157
|
+
} catch {
|
|
158
|
+
return line;
|
|
159
|
+
}
|
|
160
|
+
if (isSealed(obj) || obj?.type === "session") return line;
|
|
161
|
+
changed = true;
|
|
162
|
+
return encodeEntry(obj, key);
|
|
163
|
+
});
|
|
164
|
+
if (!changed) return "unchanged";
|
|
165
|
+
|
|
166
|
+
// Verify: every original entry must come back identical from what we will write.
|
|
167
|
+
for (let i = 0; i < lines.length; i++) {
|
|
168
|
+
if (out[i] === lines[i]) continue;
|
|
169
|
+
const back = decodeEntry(JSON.parse(out[i]), key);
|
|
170
|
+
if (JSON.stringify(back) !== JSON.stringify(JSON.parse(lines[i]))) return "failed";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const tmp = `${file}.sealing-${process.pid}`;
|
|
174
|
+
try {
|
|
175
|
+
fs.writeFileSync(tmp, out.join("\n"), { mode: 0o600 });
|
|
176
|
+
const now = fs.statSync(file);
|
|
177
|
+
if (now.size !== before.size || now.mtimeMs !== before.mtimeMs) {
|
|
178
|
+
fs.rmSync(tmp, { force: true });
|
|
179
|
+
return "busy";
|
|
180
|
+
}
|
|
181
|
+
// Keep the original times: Pi orders sessions by mtime (`privateer -c` continues
|
|
182
|
+
// the most recent one), so a conversion must not make an old session look new.
|
|
183
|
+
fs.utimesSync(tmp, before.atime, before.mtime);
|
|
184
|
+
fs.renameSync(tmp, file);
|
|
185
|
+
return "sealed";
|
|
186
|
+
} catch {
|
|
187
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort */ }
|
|
188
|
+
return "failed";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Every session file under Pi's sessions dir. */
|
|
193
|
+
export function listSessionFiles(sessionsDir) {
|
|
194
|
+
const out = [];
|
|
195
|
+
let dirs = [];
|
|
196
|
+
try {
|
|
197
|
+
dirs = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
|
198
|
+
} catch {
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
for (const d of dirs) {
|
|
202
|
+
if (!d.isDirectory()) continue;
|
|
203
|
+
const dir = path.join(sessionsDir, d.name);
|
|
204
|
+
try {
|
|
205
|
+
for (const f of fs.readdirSync(dir)) if (f.endsWith(".jsonl")) out.push(path.join(dir, f));
|
|
206
|
+
} catch { /* unreadable folder: skip */ }
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Seal every plaintext session on this machine — once, then never again. Files
|
|
213
|
+
* touched in the last QUIET_MS are skipped (another terminal may be writing them);
|
|
214
|
+
* the done-marker is only written when nothing was skipped or failed, so a later
|
|
215
|
+
* launch finishes the job. Async-yielding so the launcher's event loop stays live.
|
|
216
|
+
*/
|
|
217
|
+
export const QUIET_MS = 10 * 60 * 1000;
|
|
218
|
+
export async function sweepSessions(home, sessionsDir, { now = Date.now(), quietMs = QUIET_MS } = {}) {
|
|
219
|
+
const done = path.join(home, "run", "sessions-sealed-v1");
|
|
220
|
+
if (fs.existsSync(done)) return { skipped: true };
|
|
221
|
+
let key;
|
|
222
|
+
try {
|
|
223
|
+
key = loadOrCreateKey(home);
|
|
224
|
+
} catch {
|
|
225
|
+
return { error: true };
|
|
226
|
+
}
|
|
227
|
+
const tally = { sealed: 0, unchanged: 0, busy: 0, failed: 0, recent: 0 };
|
|
228
|
+
for (const file of listSessionFiles(sessionsDir)) {
|
|
229
|
+
try {
|
|
230
|
+
if (now - fs.statSync(file).mtimeMs < quietMs) { tally.recent++; continue; }
|
|
231
|
+
} catch { continue; }
|
|
232
|
+
tally[sealFile(file, key)]++;
|
|
233
|
+
await new Promise((r) => setImmediate(r));
|
|
234
|
+
}
|
|
235
|
+
if (!tally.busy && !tally.failed && !tally.recent) {
|
|
236
|
+
try {
|
|
237
|
+
fs.mkdirSync(path.dirname(done), { recursive: true, mode: 0o700 });
|
|
238
|
+
fs.writeFileSync(done, new Date().toISOString() + "\n", { mode: 0o600 });
|
|
239
|
+
} catch { /* retried next launch */ }
|
|
240
|
+
}
|
|
241
|
+
return tally;
|
|
242
|
+
}
|