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,4578 @@
|
|
|
1
|
+
import{createRequire as __pvCR}from"node:module";import{fileURLToPath as __pvF}from"node:url";import{dirname as __pvD}from"node:path";const require=__pvCR(import.meta.url);const __pvUrl=(r)=>new URL(r,import.meta.url).href;const __pvFile=(r)=>__pvF(new URL(r,import.meta.url));
|
|
2
|
+
import {
|
|
3
|
+
SecureClient
|
|
4
|
+
} from "./c-FJ5T42DS.mjs";
|
|
5
|
+
import "./c-L26VOJX4.mjs";
|
|
6
|
+
import "./c-C67644MB.mjs";
|
|
7
|
+
import "./c-65UZIZC7.mjs";
|
|
8
|
+
import "./c-U4ITOXAN.mjs";
|
|
9
|
+
import {
|
|
10
|
+
require_undici
|
|
11
|
+
} from "./c-P63T3XL2.mjs";
|
|
12
|
+
import {
|
|
13
|
+
x25519
|
|
14
|
+
} from "./c-4WVR32AP.mjs";
|
|
15
|
+
import {
|
|
16
|
+
__name,
|
|
17
|
+
__toESM
|
|
18
|
+
} from "./c-CN77DCGF.mjs";
|
|
19
|
+
|
|
20
|
+
// src/auth/privateer.ts?ext=privateer-privacy
|
|
21
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4, chmodSync as chmodSync4, rmSync as rmSync3 } from "node:fs";
|
|
22
|
+
import { hostname, userInfo } from "node:os";
|
|
23
|
+
|
|
24
|
+
// src/config/paths.ts?ext=privateer-privacy
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
function globalDir() {
|
|
28
|
+
return process.env.PRIVATEER_HOME ?? join(homedir(), ".privateer");
|
|
29
|
+
}
|
|
30
|
+
__name(globalDir, "globalDir");
|
|
31
|
+
function agentDir() {
|
|
32
|
+
return join(globalDir(), "agent");
|
|
33
|
+
}
|
|
34
|
+
__name(agentDir, "agentDir");
|
|
35
|
+
function credentialsPath() {
|
|
36
|
+
return join(globalDir(), "credentials.json");
|
|
37
|
+
}
|
|
38
|
+
__name(credentialsPath, "credentialsPath");
|
|
39
|
+
function configPath() {
|
|
40
|
+
return join(globalDir(), "config.json");
|
|
41
|
+
}
|
|
42
|
+
__name(configPath, "configPath");
|
|
43
|
+
function accountSessionsDir() {
|
|
44
|
+
return join(globalDir(), "account-sessions");
|
|
45
|
+
}
|
|
46
|
+
__name(accountSessionsDir, "accountSessionsDir");
|
|
47
|
+
function accountSessionsPath() {
|
|
48
|
+
return join(globalDir(), "account-sessions.json");
|
|
49
|
+
}
|
|
50
|
+
__name(accountSessionsPath, "accountSessionsPath");
|
|
51
|
+
|
|
52
|
+
// src/auth/accountSessions.ts?ext=privateer-privacy
|
|
53
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, renameSync, writeFileSync, chmodSync } from "node:fs";
|
|
54
|
+
import { join as join2 } from "node:path";
|
|
55
|
+
var ENTRY = /^(\d+)\.json$/;
|
|
56
|
+
var CLAIM = /^(\d+)\.claim-(\d+)$/;
|
|
57
|
+
function entryPath(pid) {
|
|
58
|
+
return join2(accountSessionsDir(), `${pid}.json`);
|
|
59
|
+
}
|
|
60
|
+
__name(entryPath, "entryPath");
|
|
61
|
+
function claimPath(pid, claimant = process.pid) {
|
|
62
|
+
return join2(accountSessionsDir(), `${pid}.claim-${claimant}`);
|
|
63
|
+
}
|
|
64
|
+
__name(claimPath, "claimPath");
|
|
65
|
+
function tryChmod(path, mode) {
|
|
66
|
+
try {
|
|
67
|
+
chmodSync(path, mode);
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
__name(tryChmod, "tryChmod");
|
|
72
|
+
function tryRm(path) {
|
|
73
|
+
try {
|
|
74
|
+
rmSync(path, { force: true });
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
__name(tryRm, "tryRm");
|
|
79
|
+
function readEntry(path, pid) {
|
|
80
|
+
try {
|
|
81
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
82
|
+
if (typeof raw?.refresh !== "string" || !raw.refresh) return null;
|
|
83
|
+
return { pid, refresh: raw.refresh, expires: typeof raw.expires === "number" ? raw.expires : 0 };
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
__name(readEntry, "readEntry");
|
|
89
|
+
function isAlive(pid) {
|
|
90
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
91
|
+
try {
|
|
92
|
+
process.kill(pid, 0);
|
|
93
|
+
return true;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return e.code === "EPERM";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
__name(isAlive, "isAlive");
|
|
99
|
+
function recordOwnedSession(cred) {
|
|
100
|
+
try {
|
|
101
|
+
mkdirSync(accountSessionsDir(), { recursive: true });
|
|
102
|
+
tryChmod(globalDir(), 448);
|
|
103
|
+
tryChmod(accountSessionsDir(), 448);
|
|
104
|
+
const path = entryPath(process.pid);
|
|
105
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
106
|
+
writeFileSync(tmp, JSON.stringify({ refresh: cred.refresh, expires: cred.expires }) + "\n", {
|
|
107
|
+
encoding: "utf8",
|
|
108
|
+
mode: 384
|
|
109
|
+
});
|
|
110
|
+
tryChmod(tmp, 384);
|
|
111
|
+
renameSync(tmp, path);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
__name(recordOwnedSession, "recordOwnedSession");
|
|
116
|
+
function orphanedSessions(now = Date.now()) {
|
|
117
|
+
let names;
|
|
118
|
+
try {
|
|
119
|
+
names = readdirSync(accountSessionsDir());
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const orphans = [];
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
const path = join2(accountSessionsDir(), name);
|
|
126
|
+
const claim = CLAIM.exec(name);
|
|
127
|
+
if (claim) {
|
|
128
|
+
if (!isAlive(Number(claim[2]))) tryRm(path);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const m = ENTRY.exec(name);
|
|
132
|
+
if (!m) continue;
|
|
133
|
+
const pid = Number(m[1]);
|
|
134
|
+
if (pid === process.pid || isAlive(pid)) continue;
|
|
135
|
+
const entry = readEntry(path, pid);
|
|
136
|
+
if (!entry || entry.expires > 0 && entry.expires <= now) {
|
|
137
|
+
tryRm(path);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
orphans.push(entry);
|
|
141
|
+
}
|
|
142
|
+
return orphans;
|
|
143
|
+
}
|
|
144
|
+
__name(orphanedSessions, "orphanedSessions");
|
|
145
|
+
function claimOrphan(pid) {
|
|
146
|
+
try {
|
|
147
|
+
renameSync(entryPath(pid), claimPath(pid));
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
__name(claimOrphan, "claimOrphan");
|
|
154
|
+
function releaseOrphan(pid) {
|
|
155
|
+
try {
|
|
156
|
+
renameSync(claimPath(pid), entryPath(pid));
|
|
157
|
+
} catch {
|
|
158
|
+
tryRm(claimPath(pid));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
__name(releaseOrphan, "releaseOrphan");
|
|
162
|
+
function dropOwnedSession(pid) {
|
|
163
|
+
tryRm(claimPath(pid));
|
|
164
|
+
if (pid !== process.pid) tryRm(entryPath(pid));
|
|
165
|
+
}
|
|
166
|
+
__name(dropOwnedSession, "dropOwnedSession");
|
|
167
|
+
function clearOwnedSessions() {
|
|
168
|
+
tryRm(accountSessionsPath());
|
|
169
|
+
try {
|
|
170
|
+
if (existsSync(accountSessionsDir())) rmSync(accountSessionsDir(), { recursive: true, force: true });
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
__name(clearOwnedSessions, "clearOwnedSessions");
|
|
175
|
+
|
|
176
|
+
// src/engine/errors.ts?ext=privateer-privacy
|
|
177
|
+
var CAP_CODE = /CAP|QUOTA|LIMIT_REACHED|INSUFFICIENT|TOP_?UP/i;
|
|
178
|
+
function isAccountCapCode(code) {
|
|
179
|
+
return typeof code === "string" && CAP_CODE.test(code);
|
|
180
|
+
}
|
|
181
|
+
__name(isAccountCapCode, "isAccountCapCode");
|
|
182
|
+
|
|
183
|
+
// src/crypto/terminalKey.ts?ext=privateer-privacy
|
|
184
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, chmodSync as chmodSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
185
|
+
import { join as join3 } from "node:path";
|
|
186
|
+
function keyPath() {
|
|
187
|
+
return join3(globalDir(), "terminal-key.json");
|
|
188
|
+
}
|
|
189
|
+
__name(keyPath, "keyPath");
|
|
190
|
+
var cached;
|
|
191
|
+
function b64(bytes) {
|
|
192
|
+
return Buffer.from(bytes).toString("base64");
|
|
193
|
+
}
|
|
194
|
+
__name(b64, "b64");
|
|
195
|
+
function fromB64(s, label) {
|
|
196
|
+
const buf = Buffer.from(s, "base64");
|
|
197
|
+
if (buf.length !== 32) throw new Error(`${label} must be 32 bytes`);
|
|
198
|
+
return new Uint8Array(buf);
|
|
199
|
+
}
|
|
200
|
+
__name(fromB64, "fromB64");
|
|
201
|
+
function loadOrCreate() {
|
|
202
|
+
if (cached) return cached;
|
|
203
|
+
try {
|
|
204
|
+
const parsed = JSON.parse(readFileSync2(keyPath(), "utf8"));
|
|
205
|
+
if (parsed?.v === 1 && parsed.publicKey && parsed.secretKey) {
|
|
206
|
+
cached = {
|
|
207
|
+
publicKey: fromB64(parsed.publicKey, "terminal public key"),
|
|
208
|
+
secretKey: fromB64(parsed.secretKey, "terminal secret key")
|
|
209
|
+
};
|
|
210
|
+
return cached;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
const secretKey = x25519.utils.randomPrivateKey();
|
|
215
|
+
const publicKey = x25519.getPublicKey(secretKey);
|
|
216
|
+
const file = { v: 1, publicKey: b64(publicKey), secretKey: b64(secretKey) };
|
|
217
|
+
mkdirSync2(globalDir(), { recursive: true, mode: 448 });
|
|
218
|
+
writeFileSync2(keyPath(), JSON.stringify(file), { mode: 384 });
|
|
219
|
+
try {
|
|
220
|
+
chmodSync2(keyPath(), 384);
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
cached = { publicKey, secretKey };
|
|
224
|
+
return cached;
|
|
225
|
+
}
|
|
226
|
+
__name(loadOrCreate, "loadOrCreate");
|
|
227
|
+
function terminalPublicKeyBase64() {
|
|
228
|
+
return b64(loadOrCreate().publicKey);
|
|
229
|
+
}
|
|
230
|
+
__name(terminalPublicKeyBase64, "terminalPublicKeyBase64");
|
|
231
|
+
|
|
232
|
+
// src/crypto/accountTrust.ts?ext=privateer-privacy
|
|
233
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, chmodSync as chmodSync3, rmSync as rmSync2 } from "node:fs";
|
|
234
|
+
import { join as join4 } from "node:path";
|
|
235
|
+
function trustPath() {
|
|
236
|
+
return join4(globalDir(), "account-trust.json");
|
|
237
|
+
}
|
|
238
|
+
__name(trustPath, "trustPath");
|
|
239
|
+
function pinAccountSignKey(pub) {
|
|
240
|
+
const value = (pub ?? "").trim();
|
|
241
|
+
if (!value) return;
|
|
242
|
+
try {
|
|
243
|
+
const file = { v: 1, accountSignPub: value };
|
|
244
|
+
writeFileSync3(trustPath(), JSON.stringify(file), { mode: 384 });
|
|
245
|
+
try {
|
|
246
|
+
chmodSync3(trustPath(), 384);
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
__name(pinAccountSignKey, "pinAccountSignKey");
|
|
253
|
+
function clearAccountSignKey() {
|
|
254
|
+
try {
|
|
255
|
+
rmSync2(trustPath(), { force: true });
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
__name(clearAccountSignKey, "clearAccountSignKey");
|
|
260
|
+
|
|
261
|
+
// src/auth/privateer.ts?ext=privateer-privacy
|
|
262
|
+
var DEFAULT_SERVER_URL = "https://helix-server-m1ac.onrender.com";
|
|
263
|
+
function isSafeServerUrl(raw) {
|
|
264
|
+
let u;
|
|
265
|
+
try {
|
|
266
|
+
u = new URL(raw);
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
if (u.protocol === "https:") return true;
|
|
271
|
+
if (u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1")) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
__name(isSafeServerUrl, "isSafeServerUrl");
|
|
277
|
+
function serverBaseUrl() {
|
|
278
|
+
const env = process.env.PRIVATEER_SERVER_URL?.replace(/\/$/, "");
|
|
279
|
+
if (env) {
|
|
280
|
+
if (!isSafeServerUrl(env)) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Refusing PRIVATEER_SERVER_URL=${env}: must be https:// (http allowed only for localhost). This protects your account token from being sent over an insecure or attacker-controlled connection.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return env;
|
|
286
|
+
}
|
|
287
|
+
const base = (loadCredentials()?.serverBaseUrl || DEFAULT_SERVER_URL).replace(/\/$/, "");
|
|
288
|
+
if (!isSafeServerUrl(base)) {
|
|
289
|
+
throw new Error(`Stored Privateer server URL is not https (${base}). Run /logout then /login again.`);
|
|
290
|
+
}
|
|
291
|
+
return base;
|
|
292
|
+
}
|
|
293
|
+
__name(serverBaseUrl, "serverBaseUrl");
|
|
294
|
+
function defaultDeviceLabel() {
|
|
295
|
+
try {
|
|
296
|
+
return `${userInfo().username}@${hostname()}`;
|
|
297
|
+
} catch {
|
|
298
|
+
return "terminal";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
__name(defaultDeviceLabel, "defaultDeviceLabel");
|
|
302
|
+
var SHARED = /* @__PURE__ */ Symbol.for("privateer.auth.shared");
|
|
303
|
+
function shared() {
|
|
304
|
+
const g = globalThis;
|
|
305
|
+
return g[SHARED] ??= { cache: null, signedIn: /* @__PURE__ */ new Set(), expired: /* @__PURE__ */ new Set() };
|
|
306
|
+
}
|
|
307
|
+
__name(shared, "shared");
|
|
308
|
+
var _child = null;
|
|
309
|
+
var _spawnInFlight = null;
|
|
310
|
+
var _refreshInFlight = null;
|
|
311
|
+
var _account = null;
|
|
312
|
+
function loadCredentials() {
|
|
313
|
+
const state = shared();
|
|
314
|
+
if (state.cache) return state.cache;
|
|
315
|
+
const path = credentialsPath();
|
|
316
|
+
if (!existsSync2(path)) return null;
|
|
317
|
+
try {
|
|
318
|
+
state.cache = JSON.parse(readFileSync4(path, "utf8"));
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
return state.cache;
|
|
323
|
+
}
|
|
324
|
+
__name(loadCredentials, "loadCredentials");
|
|
325
|
+
function saveCredentials(creds) {
|
|
326
|
+
const dir = globalDir();
|
|
327
|
+
mkdirSync3(dir, { recursive: true });
|
|
328
|
+
tryChmod2(dir, 448);
|
|
329
|
+
const path = credentialsPath();
|
|
330
|
+
writeFileSync4(path, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
331
|
+
tryChmod2(path, 384);
|
|
332
|
+
shared().cache = creds;
|
|
333
|
+
}
|
|
334
|
+
__name(saveCredentials, "saveCredentials");
|
|
335
|
+
var ARMED_SLOT = /* @__PURE__ */ Symbol.for("privateer.accountCredential");
|
|
336
|
+
function forgetArmedAccountCredential() {
|
|
337
|
+
const slot = globalThis[ARMED_SLOT];
|
|
338
|
+
if (slot) slot.cred = void 0;
|
|
339
|
+
}
|
|
340
|
+
__name(forgetArmedAccountCredential, "forgetArmedAccountCredential");
|
|
341
|
+
function clearCredentials() {
|
|
342
|
+
try {
|
|
343
|
+
rmSync3(credentialsPath(), { force: true });
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
clearAccountSignKey();
|
|
347
|
+
shared().cache = null;
|
|
348
|
+
_child = null;
|
|
349
|
+
_account = null;
|
|
350
|
+
forgetArmedAccountCredential();
|
|
351
|
+
}
|
|
352
|
+
__name(clearCredentials, "clearCredentials");
|
|
353
|
+
function hasCredentials() {
|
|
354
|
+
return loadCredentials() !== null;
|
|
355
|
+
}
|
|
356
|
+
__name(hasCredentials, "hasCredentials");
|
|
357
|
+
function currentUser() {
|
|
358
|
+
return loadCredentials()?.user ?? null;
|
|
359
|
+
}
|
|
360
|
+
__name(currentUser, "currentUser");
|
|
361
|
+
function tryChmod2(path, mode) {
|
|
362
|
+
try {
|
|
363
|
+
chmodSync4(path, mode);
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
__name(tryChmod2, "tryChmod");
|
|
368
|
+
function notifySessionExpired() {
|
|
369
|
+
for (const listener of shared().expired) {
|
|
370
|
+
try {
|
|
371
|
+
listener();
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
__name(notifySessionExpired, "notifySessionExpired");
|
|
377
|
+
function notifySignedIn() {
|
|
378
|
+
for (const listener of shared().signedIn) {
|
|
379
|
+
try {
|
|
380
|
+
listener();
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
__name(notifySignedIn, "notifySignedIn");
|
|
386
|
+
async function postJson(base, path, body, init = {}) {
|
|
387
|
+
return fetch(`${base}${path}`, {
|
|
388
|
+
...init,
|
|
389
|
+
method: "POST",
|
|
390
|
+
headers: { "Content-Type": "application/json", ...init.headers || {} },
|
|
391
|
+
body: JSON.stringify(body)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
__name(postJson, "postJson");
|
|
395
|
+
async function requestDeviceCode(deviceLabel = defaultDeviceLabel()) {
|
|
396
|
+
const base = serverBaseUrl();
|
|
397
|
+
let terminalPub;
|
|
398
|
+
try {
|
|
399
|
+
terminalPub = terminalPublicKeyBase64();
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
const res = await postJson(base, "/auth/device/code", { deviceLabel, terminalPub });
|
|
403
|
+
if (!res.ok) {
|
|
404
|
+
throw new Error(`Couldn't start login (${res.status}). Check your connection or PRIVATEER_SERVER_URL.`);
|
|
405
|
+
}
|
|
406
|
+
return await res.json();
|
|
407
|
+
}
|
|
408
|
+
__name(requestDeviceCode, "requestDeviceCode");
|
|
409
|
+
async function pollForToken(code, opts = {}) {
|
|
410
|
+
const base = serverBaseUrl();
|
|
411
|
+
let interval = Math.max(1, code.interval || 5) * 1e3;
|
|
412
|
+
const deadline = Date.now() + (code.expires_in || 600) * 1e3;
|
|
413
|
+
while (Date.now() < deadline) {
|
|
414
|
+
if (opts.signal?.aborted) throw new Error("Login cancelled.");
|
|
415
|
+
await sleep(interval, opts.signal);
|
|
416
|
+
const res = await postJson(base, "/auth/device/token", { device_code: code.device_code });
|
|
417
|
+
if (res.ok) {
|
|
418
|
+
const data = await res.json();
|
|
419
|
+
const creds = { accessToken: data.accessToken, refreshToken: data.refreshToken, user: data.user, serverBaseUrl: base };
|
|
420
|
+
saveCredentials(creds);
|
|
421
|
+
pinAccountSignKey(data.accountSignPub);
|
|
422
|
+
notifySignedIn();
|
|
423
|
+
return creds;
|
|
424
|
+
}
|
|
425
|
+
let err = "";
|
|
426
|
+
try {
|
|
427
|
+
err = (await res.json()).error || "";
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
if (err === "authorization_pending") {
|
|
431
|
+
opts.onPoll?.("pending");
|
|
432
|
+
} else if (err === "slow_down") {
|
|
433
|
+
interval += 2e3;
|
|
434
|
+
opts.onPoll?.("slow_down");
|
|
435
|
+
} else if (err === "access_denied") {
|
|
436
|
+
throw new Error("Login was denied in the app.");
|
|
437
|
+
} else if (err === "expired_token") {
|
|
438
|
+
throw new Error("This login code expired. Run /login again.");
|
|
439
|
+
} else {
|
|
440
|
+
throw new Error(`Login failed (${res.status}${err ? `: ${err}` : ""}).`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
throw new Error("This login code expired. Run /login again.");
|
|
444
|
+
}
|
|
445
|
+
__name(pollForToken, "pollForToken");
|
|
446
|
+
async function runDeviceLogin(opts) {
|
|
447
|
+
const code = await requestDeviceCode(opts.deviceLabel);
|
|
448
|
+
opts.onCode(code);
|
|
449
|
+
const creds = await pollForToken(code, { onPoll: opts.onPoll, signal: opts.signal });
|
|
450
|
+
return creds.user;
|
|
451
|
+
}
|
|
452
|
+
__name(runDeviceLogin, "runDeviceLogin");
|
|
453
|
+
async function spawnFailure(res) {
|
|
454
|
+
if (res.status === 401) {
|
|
455
|
+
if (hasCredentials()) {
|
|
456
|
+
clearCredentials();
|
|
457
|
+
notifySessionExpired();
|
|
458
|
+
}
|
|
459
|
+
return new Error("Your Privateer session expired. Run /login to sign in again.");
|
|
460
|
+
}
|
|
461
|
+
let message;
|
|
462
|
+
try {
|
|
463
|
+
message = (await res.json()).message;
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
return new Error(message?.trim() || `Couldn't start a Privateer session (HTTP ${res.status}).`);
|
|
467
|
+
}
|
|
468
|
+
__name(spawnFailure, "spawnFailure");
|
|
469
|
+
async function spawnChildSession() {
|
|
470
|
+
const parent = loadCredentials();
|
|
471
|
+
if (!parent) throw new Error("Not logged in to Privateer. Run /login.");
|
|
472
|
+
const res = await postJson(serverBaseUrl(), "/auth/session/spawn", {
|
|
473
|
+
refreshToken: parent.refreshToken,
|
|
474
|
+
deviceLabel: defaultDeviceLabel()
|
|
475
|
+
}, {
|
|
476
|
+
headers: { Authorization: `Bearer ${parent.accessToken}` }
|
|
477
|
+
});
|
|
478
|
+
if (!res.ok) throw await spawnFailure(res);
|
|
479
|
+
const { accessToken, refreshToken } = await res.json();
|
|
480
|
+
_child = { accessToken, refreshToken };
|
|
481
|
+
return _child;
|
|
482
|
+
}
|
|
483
|
+
__name(spawnChildSession, "spawnChildSession");
|
|
484
|
+
function ensureChildSession() {
|
|
485
|
+
if (_child) return Promise.resolve(_child);
|
|
486
|
+
if (!_spawnInFlight) {
|
|
487
|
+
_spawnInFlight = spawnChildSession().finally(() => {
|
|
488
|
+
_spawnInFlight = null;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return _spawnInFlight;
|
|
492
|
+
}
|
|
493
|
+
__name(ensureChildSession, "ensureChildSession");
|
|
494
|
+
function refreshChildSession() {
|
|
495
|
+
if (_refreshInFlight) return _refreshInFlight;
|
|
496
|
+
_refreshInFlight = (async () => {
|
|
497
|
+
if (_child) {
|
|
498
|
+
const res = await postJson(serverBaseUrl(), "/auth/refresh", { refreshToken: _child.refreshToken });
|
|
499
|
+
if (res.ok) {
|
|
500
|
+
const { accessToken, refreshToken } = await res.json();
|
|
501
|
+
_child = { accessToken, refreshToken };
|
|
502
|
+
return _child;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
_child = null;
|
|
506
|
+
return spawnChildSession();
|
|
507
|
+
})().finally(() => {
|
|
508
|
+
_refreshInFlight = null;
|
|
509
|
+
});
|
|
510
|
+
return _refreshInFlight;
|
|
511
|
+
}
|
|
512
|
+
__name(refreshChildSession, "refreshChildSession");
|
|
513
|
+
async function authedFetch(input, init = {}) {
|
|
514
|
+
const bodyBuf = init.body;
|
|
515
|
+
const withAuth = /* @__PURE__ */ __name((token) => {
|
|
516
|
+
const headers = new Headers(init.headers);
|
|
517
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
518
|
+
return { ...init, headers };
|
|
519
|
+
}, "withAuth");
|
|
520
|
+
let child = await ensureChildSession();
|
|
521
|
+
let res = await fetch(input, withAuth(child.accessToken));
|
|
522
|
+
if (res.status === 401) {
|
|
523
|
+
child = await refreshChildSession();
|
|
524
|
+
res = await fetch(input, { ...withAuth(child.accessToken), body: bodyBuf });
|
|
525
|
+
}
|
|
526
|
+
return await defuseRetryableCap(res);
|
|
527
|
+
}
|
|
528
|
+
__name(authedFetch, "authedFetch");
|
|
529
|
+
async function defuseRetryableCap(res) {
|
|
530
|
+
if (res.status !== 429) return res;
|
|
531
|
+
const body = await res.text();
|
|
532
|
+
let code;
|
|
533
|
+
try {
|
|
534
|
+
const parsed = JSON.parse(body);
|
|
535
|
+
code = parsed.code ?? parsed.error?.code;
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
const status = isAccountCapCode(typeof code === "string" ? code : void 0) ? 402 : 429;
|
|
539
|
+
return new Response(body, { status, statusText: res.statusText, headers: res.headers });
|
|
540
|
+
}
|
|
541
|
+
__name(defuseRetryableCap, "defuseRetryableCap");
|
|
542
|
+
async function deleteSession(accessToken, timeoutMs) {
|
|
543
|
+
try {
|
|
544
|
+
await fetch(`${serverBaseUrl()}/auth/session/current`, {
|
|
545
|
+
method: "DELETE",
|
|
546
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
547
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
548
|
+
});
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
__name(deleteSession, "deleteSession");
|
|
553
|
+
async function logout() {
|
|
554
|
+
const parent = loadCredentials();
|
|
555
|
+
if (parent) {
|
|
556
|
+
try {
|
|
557
|
+
const fresh = await rotateSession(parent.refreshToken);
|
|
558
|
+
await deleteSession(fresh.access, 5e3);
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
_child = null;
|
|
563
|
+
_account = null;
|
|
564
|
+
clearOwnedSessions();
|
|
565
|
+
clearCredentials();
|
|
566
|
+
}
|
|
567
|
+
__name(logout, "logout");
|
|
568
|
+
function jwtExpMs(token, fallbackMs = 5 * 6e4) {
|
|
569
|
+
try {
|
|
570
|
+
const seg = token.split(".")[1];
|
|
571
|
+
const payload = JSON.parse(Buffer.from(seg, "base64").toString("utf8"));
|
|
572
|
+
if (typeof payload.exp === "number") return payload.exp * 1e3 - 3e4;
|
|
573
|
+
} catch {
|
|
574
|
+
}
|
|
575
|
+
return Date.now() + fallbackMs;
|
|
576
|
+
}
|
|
577
|
+
__name(jwtExpMs, "jwtExpMs");
|
|
578
|
+
async function spawnAccountCredentials() {
|
|
579
|
+
const parent = loadCredentials();
|
|
580
|
+
if (!parent) throw new Error("Not logged in to Privateer. Run /login.");
|
|
581
|
+
const res = await postJson(
|
|
582
|
+
serverBaseUrl(),
|
|
583
|
+
"/auth/session/spawn",
|
|
584
|
+
{ refreshToken: parent.refreshToken, deviceLabel: defaultDeviceLabel() },
|
|
585
|
+
{ headers: { Authorization: `Bearer ${parent.accessToken}` } }
|
|
586
|
+
);
|
|
587
|
+
if (!res.ok) throw await spawnFailure(res);
|
|
588
|
+
const { accessToken, refreshToken } = await res.json();
|
|
589
|
+
_account = { accessToken };
|
|
590
|
+
const cred = { access: accessToken, refresh: refreshToken, expires: jwtExpMs(accessToken) };
|
|
591
|
+
recordOwnedSession(cred);
|
|
592
|
+
return cred;
|
|
593
|
+
}
|
|
594
|
+
__name(spawnAccountCredentials, "spawnAccountCredentials");
|
|
595
|
+
function isRefreshRejection(e) {
|
|
596
|
+
return e instanceof Error && typeof e.status === "number";
|
|
597
|
+
}
|
|
598
|
+
__name(isRefreshRejection, "isRefreshRejection");
|
|
599
|
+
async function rotateSession(refresh) {
|
|
600
|
+
const res = await postJson(serverBaseUrl(), "/auth/refresh", { refreshToken: refresh });
|
|
601
|
+
if (!res.ok) {
|
|
602
|
+
const err = new Error(`account refresh failed (${res.status})`);
|
|
603
|
+
err.status = res.status;
|
|
604
|
+
throw err;
|
|
605
|
+
}
|
|
606
|
+
const { accessToken, refreshToken } = await res.json();
|
|
607
|
+
return { access: accessToken, refresh: refreshToken, expires: jwtExpMs(accessToken) };
|
|
608
|
+
}
|
|
609
|
+
__name(rotateSession, "rotateSession");
|
|
610
|
+
async function refreshAccountCredentials(refresh) {
|
|
611
|
+
const cred = await rotateSession(refresh);
|
|
612
|
+
_account = { accessToken: cred.access };
|
|
613
|
+
recordOwnedSession(cred);
|
|
614
|
+
return cred;
|
|
615
|
+
}
|
|
616
|
+
__name(refreshAccountCredentials, "refreshAccountCredentials");
|
|
617
|
+
async function acquireAccountCredential() {
|
|
618
|
+
const orphans = orphanedSessions();
|
|
619
|
+
let adopted = null;
|
|
620
|
+
let attempted = 0;
|
|
621
|
+
while (attempted < orphans.length && !adopted) {
|
|
622
|
+
const orphan = orphans[attempted++];
|
|
623
|
+
if (!claimOrphan(orphan.pid)) continue;
|
|
624
|
+
try {
|
|
625
|
+
adopted = await refreshAccountCredentials(orphan.refresh);
|
|
626
|
+
dropOwnedSession(orphan.pid);
|
|
627
|
+
} catch (e) {
|
|
628
|
+
if (isRefreshRejection(e)) dropOwnedSession(orphan.pid);
|
|
629
|
+
else releaseOrphan(orphan.pid);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const leftovers = orphans.slice(attempted);
|
|
633
|
+
if (leftovers.length) void revokeOrphanedSessions(leftovers);
|
|
634
|
+
return adopted ?? await spawnAccountCredentials();
|
|
635
|
+
}
|
|
636
|
+
__name(acquireAccountCredential, "acquireAccountCredential");
|
|
637
|
+
async function revokeOrphanedSessions(orphans, timeoutMs = 1500) {
|
|
638
|
+
for (const orphan of orphans) {
|
|
639
|
+
if (!claimOrphan(orphan.pid)) continue;
|
|
640
|
+
try {
|
|
641
|
+
const cred = await rotateSession(orphan.refresh);
|
|
642
|
+
await deleteSession(cred.access, timeoutMs);
|
|
643
|
+
dropOwnedSession(orphan.pid);
|
|
644
|
+
} catch (e) {
|
|
645
|
+
if (isRefreshRejection(e)) dropOwnedSession(orphan.pid);
|
|
646
|
+
else releaseOrphan(orphan.pid);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
__name(revokeOrphanedSessions, "revokeOrphanedSessions");
|
|
651
|
+
function sleep(ms, signal) {
|
|
652
|
+
return new Promise((resolve, reject) => {
|
|
653
|
+
const t = setTimeout(resolve, ms);
|
|
654
|
+
signal?.addEventListener(
|
|
655
|
+
"abort",
|
|
656
|
+
() => {
|
|
657
|
+
clearTimeout(t);
|
|
658
|
+
reject(new Error("Login cancelled."));
|
|
659
|
+
},
|
|
660
|
+
{ once: true }
|
|
661
|
+
);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
__name(sleep, "sleep");
|
|
665
|
+
|
|
666
|
+
// src/providers/account.ts?ext=privateer-privacy
|
|
667
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
|
|
668
|
+
import { join as join7 } from "node:path";
|
|
669
|
+
|
|
670
|
+
// src/util/openBrowser.ts?ext=privateer-privacy
|
|
671
|
+
import { spawn } from "node:child_process";
|
|
672
|
+
function canOpenBrowser(env = process.env, platform = process.platform) {
|
|
673
|
+
if (env.PRIVATEER_NO_BROWSER?.trim()) return false;
|
|
674
|
+
if (env.SSH_TTY || env.SSH_CONNECTION) return false;
|
|
675
|
+
if (platform !== "darwin" && platform !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) return false;
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
__name(canOpenBrowser, "canOpenBrowser");
|
|
679
|
+
function browsableUrl(raw) {
|
|
680
|
+
const s = (raw ?? "").trim();
|
|
681
|
+
if (!s) return void 0;
|
|
682
|
+
try {
|
|
683
|
+
const u = new URL(s);
|
|
684
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") return void 0;
|
|
685
|
+
return u.href;
|
|
686
|
+
} catch {
|
|
687
|
+
return void 0;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
__name(browsableUrl, "browsableUrl");
|
|
691
|
+
function openInBrowser(rawUrl) {
|
|
692
|
+
const url = browsableUrl(rawUrl);
|
|
693
|
+
if (!url) return Promise.resolve(false);
|
|
694
|
+
let cmd;
|
|
695
|
+
let args;
|
|
696
|
+
if (process.platform === "darwin") {
|
|
697
|
+
cmd = "open";
|
|
698
|
+
args = [url];
|
|
699
|
+
} else if (process.platform === "win32") {
|
|
700
|
+
cmd = "cmd";
|
|
701
|
+
args = ["/c", "start", "", url.replace(/&/g, "^&")];
|
|
702
|
+
} else {
|
|
703
|
+
cmd = "xdg-open";
|
|
704
|
+
args = [url];
|
|
705
|
+
}
|
|
706
|
+
return new Promise((resolve) => {
|
|
707
|
+
let settled = false;
|
|
708
|
+
const done = /* @__PURE__ */ __name((ok) => {
|
|
709
|
+
if (!settled) {
|
|
710
|
+
settled = true;
|
|
711
|
+
resolve(ok);
|
|
712
|
+
}
|
|
713
|
+
}, "done");
|
|
714
|
+
try {
|
|
715
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
716
|
+
child.once("error", () => done(false));
|
|
717
|
+
child.once("exit", (code) => done(code === 0));
|
|
718
|
+
child.unref();
|
|
719
|
+
setTimeout(() => done(true), 2e3).unref?.();
|
|
720
|
+
} catch {
|
|
721
|
+
done(false);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
__name(openInBrowser, "openInBrowser");
|
|
726
|
+
|
|
727
|
+
// node_modules/pi-privacy/src/posture/tiers.ts?ext=privateer-privacy
|
|
728
|
+
var TIERS = {
|
|
729
|
+
"tee-verified": {
|
|
730
|
+
tier: "tee-verified",
|
|
731
|
+
label: "Verified TEE",
|
|
732
|
+
verifiability: "cryptographic",
|
|
733
|
+
posture: "green",
|
|
734
|
+
blurb: "Confidential-enclave inference, cryptographically verified: remote attestation proved genuine TEE hardware and the live TLS key matched the attestation report."
|
|
735
|
+
},
|
|
736
|
+
"tee-unverified": {
|
|
737
|
+
tier: "tee-unverified",
|
|
738
|
+
label: "TEE (unconfirmed)",
|
|
739
|
+
verifiability: "none",
|
|
740
|
+
posture: "yellow",
|
|
741
|
+
blurb: "Provider claims a TEE, but attestation was incomplete or the live key could not be matched here \u2014 treat as unverified until it goes green."
|
|
742
|
+
},
|
|
743
|
+
local: {
|
|
744
|
+
tier: "local",
|
|
745
|
+
label: "On-device",
|
|
746
|
+
verifiability: "observable",
|
|
747
|
+
posture: "green",
|
|
748
|
+
blurb: "Runs against a loopback endpoint \u2014 inference is local; no prompt leaves the machine."
|
|
749
|
+
},
|
|
750
|
+
"zdr-enforced": {
|
|
751
|
+
tier: "zdr-enforced",
|
|
752
|
+
label: "ZDR (enforced)",
|
|
753
|
+
verifiability: "observable",
|
|
754
|
+
posture: "green",
|
|
755
|
+
blurb: "Routing is constrained to providers that contractually don't retain data \u2014 verified observable: OpenRouter refuses to serve when the policy can't be met. Enforcement of a policy, not hardware attestation."
|
|
756
|
+
},
|
|
757
|
+
"zdr-policy": {
|
|
758
|
+
tier: "zdr-policy",
|
|
759
|
+
label: "ZDR (by policy)",
|
|
760
|
+
verifiability: "policy",
|
|
761
|
+
posture: "yellow",
|
|
762
|
+
blurb: "The provider states it doesn't retain data, but this is a policy promise we can't verify \u2014 not hardware, not attested."
|
|
763
|
+
},
|
|
764
|
+
standard: {
|
|
765
|
+
tier: "standard",
|
|
766
|
+
label: "Standard",
|
|
767
|
+
verifiability: "none",
|
|
768
|
+
posture: "neutral",
|
|
769
|
+
blurb: "No special privacy guarantee."
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
var ORDER = [
|
|
773
|
+
"tee-verified",
|
|
774
|
+
"local",
|
|
775
|
+
"zdr-enforced",
|
|
776
|
+
"tee-unverified",
|
|
777
|
+
"zdr-policy",
|
|
778
|
+
"standard"
|
|
779
|
+
];
|
|
780
|
+
function tierRank(t) {
|
|
781
|
+
const i = ORDER.indexOf(t);
|
|
782
|
+
return i === -1 ? ORDER.length : i;
|
|
783
|
+
}
|
|
784
|
+
__name(tierRank, "tierRank");
|
|
785
|
+
function tierFromTeePosture(p) {
|
|
786
|
+
return p === "green" ? "tee-verified" : p === "yellow" ? "tee-unverified" : "standard";
|
|
787
|
+
}
|
|
788
|
+
__name(tierFromTeePosture, "tierFromTeePosture");
|
|
789
|
+
|
|
790
|
+
// node_modules/pi-privacy/src/providers/catalog.ts?ext=privateer-privacy
|
|
791
|
+
var PRIVACY_PROVIDERS = [
|
|
792
|
+
{
|
|
793
|
+
id: "privateer",
|
|
794
|
+
label: "Privateer",
|
|
795
|
+
// Posture-aware, exactly like OpenRouter — tee-verified is the CEILING, not a
|
|
796
|
+
// promise. Privateer's in-app ACCOUNT channel offers verified-TEE inference, but
|
|
797
|
+
// pi-privacy only exercises the PUBLIC developer-key channel (sk-priv-…), which is
|
|
798
|
+
// server-proxied: the proxy mediates attestation, so a pi client can't verify the
|
|
799
|
+
// enclave itself. The public key therefore floors to zdr-policy at runtime (see
|
|
800
|
+
// effectiveTier) — verified TEE is never claimed from it alone. NOT `attestable`
|
|
801
|
+
// here: pi-privacy does not attest Privateer itself. The account channel (its own
|
|
802
|
+
// OAuth session + account server + sealed relay) lives in the host (privateer-agent),
|
|
803
|
+
// which reuses this package's interpretReport/teePosture and injects the verified
|
|
804
|
+
// verdict through the extension's resolveTier hook; the picker's `verifiedTee` signal
|
|
805
|
+
// reflects that the host channel is active (see PiPrivacyOptions.privateerVerifiedTee).
|
|
806
|
+
tier: "tee-verified",
|
|
807
|
+
// ceiling; resolves to zdr-policy on the public developer key
|
|
808
|
+
postureAware: true,
|
|
809
|
+
baseUrl: "https://api.privateer.pro/v1",
|
|
810
|
+
api: "openai-completions",
|
|
811
|
+
keyEnv: "${PRIVATEER_API_KEY}",
|
|
812
|
+
note: "privateer.pro \u2192 API keys (sk-priv-\u2026). Public developer key is server-proxied, zero-retention by policy (NOT TEE-attested here); verified TEE is the in-app account channel."
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
id: "tinfoil",
|
|
816
|
+
label: "Tinfoil (private TEE inference)",
|
|
817
|
+
tier: "tee-verified",
|
|
818
|
+
attestable: true,
|
|
819
|
+
baseUrl: "https://inference.tinfoil.sh/v1",
|
|
820
|
+
api: "openai-completions",
|
|
821
|
+
keyEnv: "${TINFOIL_API_KEY}",
|
|
822
|
+
note: "tinfoil.sh \u2192 dashboard \u2192 API Keys. Confidential-enclave inference; verified by attestation + live-TLS-key match."
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
id: "nearai",
|
|
826
|
+
label: "NEAR AI (private TEE inference)",
|
|
827
|
+
tier: "tee-verified",
|
|
828
|
+
attestable: true,
|
|
829
|
+
baseUrl: "https://cloud-api.near.ai/v1",
|
|
830
|
+
api: "openai-completions",
|
|
831
|
+
keyEnv: "${NEARAI_API_KEY}",
|
|
832
|
+
note: "cloud.near.ai \u2192 API Keys. Confidential-compute TEE; attestation carried in the report body (verified over HTTPS)."
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
id: "venice",
|
|
836
|
+
label: "Venice (no-retention inference)",
|
|
837
|
+
tier: "zdr-policy",
|
|
838
|
+
baseUrl: "https://api.venice.ai/api/v1",
|
|
839
|
+
api: "openai-completions",
|
|
840
|
+
keyEnv: "${VENICE_API_KEY}",
|
|
841
|
+
// Honest copy: policy, not hardware. Venice injects a body param to disable its
|
|
842
|
+
// system prompt — handled by a before_provider_request hook, not config.
|
|
843
|
+
note: 'venice.ai \u2192 API Keys (no retention by policy, not TEE-attested; "anonymized" models proxy upstream).',
|
|
844
|
+
compat: { veniceDisableSystemPrompt: true }
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
id: "fireworks",
|
|
848
|
+
label: "Fireworks (no-retention inference)",
|
|
849
|
+
tier: "zdr-policy",
|
|
850
|
+
baseUrl: "https://api.fireworks.ai/inference/v1",
|
|
851
|
+
api: "openai-completions",
|
|
852
|
+
keyEnv: "${FIREWORKS_API_KEY}",
|
|
853
|
+
// Honest copy: ZDR is the default for OPEN models only; Fireworks's own f1 /
|
|
854
|
+
// FireFunction may log. Not TEE-attested.
|
|
855
|
+
note: "fireworks.ai \u2192 API Keys (open models: zero retention by default, not TEE-attested; Fireworks's own f1/FireFunction may log)."
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
id: "openrouter",
|
|
859
|
+
label: "OpenRouter",
|
|
860
|
+
tier: "zdr-enforced",
|
|
861
|
+
// ceiling; posture-aware — see postureAware
|
|
862
|
+
postureAware: true,
|
|
863
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
864
|
+
api: "openai-completions",
|
|
865
|
+
keyEnv: "${OPENROUTER_API_KEY}",
|
|
866
|
+
// Honest copy: ZDR is per-model/account. Yellow (zdr-policy) until enforcement
|
|
867
|
+
// pins requests to zero-retention endpoints, then green (zdr-enforced).
|
|
868
|
+
note: "openrouter.ai/keys. ZDR is per-model; not guaranteed until ZDR routing is enforced (then requests only hit zero-retention endpoints).",
|
|
869
|
+
compat: { openRouterRouting: { zdr: true } }
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
id: "ollama",
|
|
873
|
+
label: "Ollama (local)",
|
|
874
|
+
tier: "local",
|
|
875
|
+
local: true,
|
|
876
|
+
baseUrl: "http://localhost:11434/v1",
|
|
877
|
+
// OpenAI-compat surface, not native /api
|
|
878
|
+
api: "openai-completions",
|
|
879
|
+
note: "runs locally \u2014 no key needed. Inference on-device; nothing leaves the machine."
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
id: "custom",
|
|
883
|
+
label: "Custom (OpenAI-compatible)",
|
|
884
|
+
tier: "standard",
|
|
885
|
+
// resolved to `local` at runtime when the baseUrl is loopback
|
|
886
|
+
api: "openai-completions",
|
|
887
|
+
note: "any OpenAI-compatible endpoint \u2014 LM Studio, vLLM, llama.cpp, a proxy. Detected as on-device when the URL is loopback."
|
|
888
|
+
}
|
|
889
|
+
];
|
|
890
|
+
var PROVIDER_BY_ID = Object.fromEntries(
|
|
891
|
+
PRIVACY_PROVIDERS.map((p) => [p.id, p])
|
|
892
|
+
);
|
|
893
|
+
function isLocalEndpoint(baseUrl) {
|
|
894
|
+
if (!baseUrl) return false;
|
|
895
|
+
let host;
|
|
896
|
+
try {
|
|
897
|
+
host = new URL(baseUrl).hostname.toLowerCase();
|
|
898
|
+
} catch {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
|
|
902
|
+
const zone = host.indexOf("%");
|
|
903
|
+
if (zone !== -1) host = host.slice(0, zone);
|
|
904
|
+
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
905
|
+
if (host === "::1" || host === "::") return true;
|
|
906
|
+
let v4 = host;
|
|
907
|
+
if (host.startsWith("::ffff:")) {
|
|
908
|
+
const rest = host.slice(7);
|
|
909
|
+
if (rest.includes(".")) v4 = rest;
|
|
910
|
+
else {
|
|
911
|
+
const groups = rest.split(":");
|
|
912
|
+
if (groups.length !== 2) return false;
|
|
913
|
+
const hi = Number.parseInt(groups[0], 16);
|
|
914
|
+
const lo = Number.parseInt(groups[1], 16);
|
|
915
|
+
if (!Number.isInteger(hi) || !Number.isInteger(lo) || hi > 65535 || lo > 65535) return false;
|
|
916
|
+
v4 = `${hi >>> 8}.${hi & 255}.${lo >>> 8}.${lo & 255}`;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(v4);
|
|
920
|
+
if (!m) return false;
|
|
921
|
+
const octets = m.slice(1).map(Number);
|
|
922
|
+
if (octets.some((n) => n > 255)) return false;
|
|
923
|
+
return octets[0] === 127 || v4 === "0.0.0.0";
|
|
924
|
+
}
|
|
925
|
+
__name(isLocalEndpoint, "isLocalEndpoint");
|
|
926
|
+
|
|
927
|
+
// node_modules/pi-privacy/src/attest/attestation.ts?ext=privateer-privacy
|
|
928
|
+
import { randomBytes, createHash, X509Certificate } from "node:crypto";
|
|
929
|
+
import { request as httpsRequest } from "node:https";
|
|
930
|
+
import { gunzipSync } from "node:zlib";
|
|
931
|
+
var TIMEOUT_MS = 12e3;
|
|
932
|
+
var NEARAI_BASE_URL = "https://cloud-api.near.ai/v1";
|
|
933
|
+
var TINFOIL_BASE_URL = "https://inference.tinfoil.sh/v1";
|
|
934
|
+
function baseFor(cfg) {
|
|
935
|
+
return (cfg.baseURL ?? NEARAI_BASE_URL).replace(/\/+$/, "");
|
|
936
|
+
}
|
|
937
|
+
__name(baseFor, "baseFor");
|
|
938
|
+
function randomNonce() {
|
|
939
|
+
return randomBytes(32).toString("hex");
|
|
940
|
+
}
|
|
941
|
+
__name(randomNonce, "randomNonce");
|
|
942
|
+
function deepFindString(obj, keys) {
|
|
943
|
+
const want = new Set(keys.map((k) => k.toLowerCase()));
|
|
944
|
+
const stack = [obj];
|
|
945
|
+
while (stack.length) {
|
|
946
|
+
const cur = stack.pop();
|
|
947
|
+
if (Array.isArray(cur)) {
|
|
948
|
+
stack.push(...cur);
|
|
949
|
+
} else if (cur && typeof cur === "object") {
|
|
950
|
+
for (const [k, v] of Object.entries(cur)) {
|
|
951
|
+
if (typeof v === "string" && want.has(k.toLowerCase()) && v.trim()) return v.trim();
|
|
952
|
+
if (v && typeof v === "object") stack.push(v);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return void 0;
|
|
957
|
+
}
|
|
958
|
+
__name(deepFindString, "deepFindString");
|
|
959
|
+
async function fetchAttestation(cfg, modelId) {
|
|
960
|
+
if (!cfg.apiKey) throw new Error("no API key");
|
|
961
|
+
const nonce = randomNonce();
|
|
962
|
+
const url = `${baseFor(cfg)}/attestation/report?model=${encodeURIComponent(modelId)}&signing_algo=ecdsa&nonce=${nonce}`;
|
|
963
|
+
const ac = new AbortController();
|
|
964
|
+
const timer = setTimeout(() => ac.abort(), TIMEOUT_MS);
|
|
965
|
+
let raw;
|
|
966
|
+
try {
|
|
967
|
+
const res = await fetch(url, {
|
|
968
|
+
headers: { authorization: `Bearer ${cfg.apiKey}` },
|
|
969
|
+
signal: ac.signal
|
|
970
|
+
});
|
|
971
|
+
if (!res.ok) {
|
|
972
|
+
const body = await res.text().catch(() => "");
|
|
973
|
+
const hint = body.slice(0, 200).trim();
|
|
974
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}${hint ? ` \u2014 ${hint}` : ""}`);
|
|
975
|
+
}
|
|
976
|
+
raw = await res.json();
|
|
977
|
+
} catch (err) {
|
|
978
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
979
|
+
throw new Error(`timed out after ${TIMEOUT_MS / 1e3}s`);
|
|
980
|
+
}
|
|
981
|
+
throw err;
|
|
982
|
+
} finally {
|
|
983
|
+
clearTimeout(timer);
|
|
984
|
+
}
|
|
985
|
+
return interpretReport(modelId, nonce, raw);
|
|
986
|
+
}
|
|
987
|
+
__name(fetchAttestation, "fetchAttestation");
|
|
988
|
+
function interpretReport(modelId, nonce, raw) {
|
|
989
|
+
const signingAddress = deepFindString(raw, ["signing_address", "signingAddress", "address"]);
|
|
990
|
+
const blob = JSON.stringify(raw).toLowerCase();
|
|
991
|
+
const hardware = [];
|
|
992
|
+
if (/nvidia|gpu/.test(blob)) hardware.push("NVIDIA");
|
|
993
|
+
if (/intel|tdx/.test(blob)) hardware.push("Intel TDX");
|
|
994
|
+
const nonceEchoed = nonce.length >= 16 && blob.includes(nonce.toLowerCase());
|
|
995
|
+
return { model: modelId, nonce, signingAddress, nonceEchoed, hardware, raw };
|
|
996
|
+
}
|
|
997
|
+
__name(interpretReport, "interpretReport");
|
|
998
|
+
function teePosture(att) {
|
|
999
|
+
if (!att.signingAddress && att.hardware.length === 0) return "red";
|
|
1000
|
+
if (att.signingAddress && att.hardware.length > 0 && att.nonceEchoed) return "green";
|
|
1001
|
+
return "yellow";
|
|
1002
|
+
}
|
|
1003
|
+
__name(teePosture, "teePosture");
|
|
1004
|
+
var httpsTransport = /* @__PURE__ */ __name((host) => new Promise((resolve, reject) => {
|
|
1005
|
+
const url = new URL(`https://${host}/.well-known/tinfoil-attestation`);
|
|
1006
|
+
const req = httpsRequest(
|
|
1007
|
+
{
|
|
1008
|
+
hostname: url.hostname,
|
|
1009
|
+
port: url.port ? Number(url.port) : 443,
|
|
1010
|
+
path: url.pathname,
|
|
1011
|
+
method: "GET",
|
|
1012
|
+
timeout: TIMEOUT_MS,
|
|
1013
|
+
// Force a fresh connection + full TLS handshake every time. A pooled/
|
|
1014
|
+
// resumed TLS session skips re-sending the server cert, so
|
|
1015
|
+
// getPeerCertificate() returns {} on the abbreviated handshake and the
|
|
1016
|
+
// key binding silently fails (posture flips green→yellow on re-verify).
|
|
1017
|
+
// Attestation must observe a real handshake anyway, so no session reuse.
|
|
1018
|
+
agent: false
|
|
1019
|
+
},
|
|
1020
|
+
(res) => {
|
|
1021
|
+
let liveTlsKeyFp;
|
|
1022
|
+
try {
|
|
1023
|
+
const peer = res.socket.getPeerCertificate(true);
|
|
1024
|
+
if (peer?.raw) {
|
|
1025
|
+
const spki = new X509Certificate(peer.raw).publicKey.export({ type: "spki", format: "der" });
|
|
1026
|
+
liveTlsKeyFp = createHash("sha256").update(spki).digest("hex");
|
|
1027
|
+
}
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
let body = "";
|
|
1031
|
+
res.on("data", (chunk) => body += chunk);
|
|
1032
|
+
res.on("end", () => {
|
|
1033
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
1034
|
+
const hint = body.slice(0, 200).trim();
|
|
1035
|
+
reject(new Error(`HTTP ${res.statusCode} ${res.statusMessage ?? ""}${hint ? ` \u2014 ${hint}` : ""}`));
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
resolve({ doc: JSON.parse(body), liveTlsKeyFp });
|
|
1040
|
+
} catch {
|
|
1041
|
+
reject(new Error("attestation endpoint returned non-JSON"));
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
res.on("error", reject);
|
|
1045
|
+
}
|
|
1046
|
+
);
|
|
1047
|
+
req.on("timeout", () => req.destroy(new Error(`timed out after ${TIMEOUT_MS / 1e3}s`)));
|
|
1048
|
+
req.on("error", reject);
|
|
1049
|
+
req.end();
|
|
1050
|
+
}), "httpsTransport");
|
|
1051
|
+
async function fetchTinfoilAttestation(cfg, transport = httpsTransport) {
|
|
1052
|
+
const host = new URL(cfg.baseURL ?? TINFOIL_BASE_URL).host;
|
|
1053
|
+
const { doc, liveTlsKeyFp } = await transport(host);
|
|
1054
|
+
return interpretTinfoilDoc(host, doc, liveTlsKeyFp);
|
|
1055
|
+
}
|
|
1056
|
+
__name(fetchTinfoilAttestation, "fetchTinfoilAttestation");
|
|
1057
|
+
function interpretTinfoilDoc(host, doc, liveTlsKeyFp) {
|
|
1058
|
+
const d = doc ?? {};
|
|
1059
|
+
const format = typeof d.format === "string" ? d.format : "";
|
|
1060
|
+
const fmt = format.toLowerCase();
|
|
1061
|
+
const hardware = [];
|
|
1062
|
+
if (/sev|snp/.test(fmt)) hardware.push("AMD SEV-SNP");
|
|
1063
|
+
if (/tdx/.test(fmt)) hardware.push("Intel TDX");
|
|
1064
|
+
if (/nitro/.test(fmt)) hardware.push("AWS Nitro");
|
|
1065
|
+
let report;
|
|
1066
|
+
if (typeof d.body === "string" && d.body) {
|
|
1067
|
+
try {
|
|
1068
|
+
let bytes = Buffer.from(d.body, "base64");
|
|
1069
|
+
if (bytes[0] === 31 && bytes[1] === 139) bytes = gunzipSync(bytes);
|
|
1070
|
+
report = bytes;
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const attestedTlsKeyFp = /sev-snp-guest/.test(fmt) && report && report.length >= 144 ? report.subarray(80, 112).toString("hex") : void 0;
|
|
1075
|
+
const tlsKeyMatched = !!liveTlsKeyFp && !!report && (attestedTlsKeyFp === liveTlsKeyFp || report.includes(Buffer.from(liveTlsKeyFp, "hex")));
|
|
1076
|
+
return { host, format, hardware, attestedTlsKeyFp, liveTlsKeyFp, tlsKeyMatched, raw: doc };
|
|
1077
|
+
}
|
|
1078
|
+
__name(interpretTinfoilDoc, "interpretTinfoilDoc");
|
|
1079
|
+
function tinfoilTeePosture(att) {
|
|
1080
|
+
if (att.hardware.length === 0 && !att.attestedTlsKeyFp) return "red";
|
|
1081
|
+
if (att.hardware.length > 0 && att.tlsKeyMatched) return "green";
|
|
1082
|
+
return "yellow";
|
|
1083
|
+
}
|
|
1084
|
+
__name(tinfoilTeePosture, "tinfoilTeePosture");
|
|
1085
|
+
|
|
1086
|
+
// node_modules/pi-privacy/src/attest/dispatcher.ts?ext=privateer-privacy
|
|
1087
|
+
var import_undici = __toESM(require_undici());
|
|
1088
|
+
import crypto2 from "node:crypto";
|
|
1089
|
+
var captured = /* @__PURE__ */ new Map();
|
|
1090
|
+
var installed = false;
|
|
1091
|
+
function installAttestationDispatcher() {
|
|
1092
|
+
if (installed) return;
|
|
1093
|
+
installed = true;
|
|
1094
|
+
const baseConnect = (0, import_undici.buildConnector)({});
|
|
1095
|
+
const attestingConnector = /* @__PURE__ */ __name((opts, cb) => baseConnect(opts, (err, socket) => {
|
|
1096
|
+
const host = opts.hostname;
|
|
1097
|
+
if (!err && socket && host && !captured.has(host) && typeof socket.getPeerCertificate === "function") {
|
|
1098
|
+
try {
|
|
1099
|
+
const cert = socket.getPeerCertificate(true);
|
|
1100
|
+
if (cert && cert.raw) {
|
|
1101
|
+
const spkiDer = new crypto2.X509Certificate(cert.raw).publicKey.export({
|
|
1102
|
+
type: "spki",
|
|
1103
|
+
format: "der"
|
|
1104
|
+
});
|
|
1105
|
+
captured.set(host, {
|
|
1106
|
+
subject: cert.subject?.CN ?? JSON.stringify(cert.subject),
|
|
1107
|
+
issuer: cert.issuer?.O ?? cert.issuer?.CN ?? JSON.stringify(cert.issuer),
|
|
1108
|
+
spkiSha256: crypto2.createHash("sha256").update(spkiDer).digest("hex"),
|
|
1109
|
+
fingerprint256: cert.fingerprint256
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
captured.set(host, { error: String(e) });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
cb(err, socket);
|
|
1117
|
+
}), "attestingConnector");
|
|
1118
|
+
(0, import_undici.setGlobalDispatcher)(new import_undici.Agent({ connect: attestingConnector, connectTimeout: 8e3 }));
|
|
1119
|
+
}
|
|
1120
|
+
__name(installAttestationDispatcher, "installAttestationDispatcher");
|
|
1121
|
+
function getCapturedCert(host) {
|
|
1122
|
+
return captured.get(host);
|
|
1123
|
+
}
|
|
1124
|
+
__name(getCapturedCert, "getCapturedCert");
|
|
1125
|
+
var dispatcherTransport = /* @__PURE__ */ __name(async (host) => {
|
|
1126
|
+
const res = await fetch(`https://${host}/.well-known/tinfoil-attestation`);
|
|
1127
|
+
if (!res.ok) {
|
|
1128
|
+
const hint = (await res.text().catch(() => "")).slice(0, 200).trim();
|
|
1129
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}${hint ? ` \u2014 ${hint}` : ""}`);
|
|
1130
|
+
}
|
|
1131
|
+
const doc = await res.json();
|
|
1132
|
+
const cert = getCapturedCert(host);
|
|
1133
|
+
return { doc, liveTlsKeyFp: cert && !cert.error ? cert.spkiSha256 : void 0 };
|
|
1134
|
+
}, "dispatcherTransport");
|
|
1135
|
+
|
|
1136
|
+
// node_modules/pi-privacy/src/posture/effective.ts?ext=privateer-privacy
|
|
1137
|
+
function effectiveTier(providerId, opts = {}) {
|
|
1138
|
+
const p = PROVIDER_BY_ID[providerId];
|
|
1139
|
+
if (!p) return "standard";
|
|
1140
|
+
if (p.local || isLocalEndpoint(opts.baseUrl)) return "local";
|
|
1141
|
+
if (p.postureAware && p.id === "openrouter") {
|
|
1142
|
+
return opts.zdrEnforced ? "zdr-enforced" : "zdr-policy";
|
|
1143
|
+
}
|
|
1144
|
+
if (p.postureAware && p.id === "privateer") {
|
|
1145
|
+
return opts.verifiedTee ? "tee-verified" : "zdr-policy";
|
|
1146
|
+
}
|
|
1147
|
+
return p.tier;
|
|
1148
|
+
}
|
|
1149
|
+
__name(effectiveTier, "effectiveTier");
|
|
1150
|
+
|
|
1151
|
+
// node_modules/pi-privacy/src/posture/picker.ts?ext=privateer-privacy
|
|
1152
|
+
function capabilityTier(providerId, baseUrl, opts = {}) {
|
|
1153
|
+
if (isLocalEndpoint(baseUrl)) return "local";
|
|
1154
|
+
return effectiveTier(providerId ?? "", {
|
|
1155
|
+
baseUrl,
|
|
1156
|
+
zdrEnforced: opts.zdrEnforced,
|
|
1157
|
+
verifiedTee: opts.verifiedTee
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
__name(capabilityTier, "capabilityTier");
|
|
1161
|
+
function capabilityBadge(tier, attestable) {
|
|
1162
|
+
switch (tier) {
|
|
1163
|
+
case "tee-verified":
|
|
1164
|
+
return attestable ? { glyph: "\u25C6", label: "Verifiable TEE" } : { glyph: "\u{1F6E1}", label: "Verified TEE" };
|
|
1165
|
+
case "local":
|
|
1166
|
+
return { glyph: "\u{1F6E1}", label: "On-device" };
|
|
1167
|
+
case "zdr-enforced":
|
|
1168
|
+
return { glyph: "\u{1F6E1}", label: "ZDR (enforced)" };
|
|
1169
|
+
case "tee-unverified":
|
|
1170
|
+
return { glyph: "\u26A0", label: "TEE (unconfirmed)" };
|
|
1171
|
+
case "zdr-policy":
|
|
1172
|
+
return { glyph: "\u26A0", label: "ZDR (by policy)" };
|
|
1173
|
+
default:
|
|
1174
|
+
return { glyph: "\u2022", label: "Standard" };
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
__name(capabilityBadge, "capabilityBadge");
|
|
1178
|
+
function pickerEntry(model, opts = {}) {
|
|
1179
|
+
const provider = model.provider;
|
|
1180
|
+
const verifiedTee = typeof opts.verifiedTee === "function" ? !!opts.verifiedTee(model) : !!opts.verifiedTee;
|
|
1181
|
+
const attestable = provider === "privateer" ? verifiedTee : !!(provider && PROVIDER_BY_ID[provider]?.attestable);
|
|
1182
|
+
const tier = capabilityTier(provider, model.baseUrl, { zdrEnforced: opts.zdrEnforced, verifiedTee });
|
|
1183
|
+
const { glyph, label } = capabilityBadge(tier, attestable);
|
|
1184
|
+
return { model, capabilityTier: tier, rank: tierRank(tier), glyph, label, attestable };
|
|
1185
|
+
}
|
|
1186
|
+
__name(pickerEntry, "pickerEntry");
|
|
1187
|
+
function rankModels(models, opts = {}) {
|
|
1188
|
+
return models.map((m) => pickerEntry(m, opts)).sort(
|
|
1189
|
+
(a, b) => a.rank - b.rank || (a.model.provider ?? "").localeCompare(b.model.provider ?? "") || (a.model.id ?? "").localeCompare(b.model.id ?? "")
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
__name(rankModels, "rankModels");
|
|
1193
|
+
function pickerOptionLabel(e, current = false) {
|
|
1194
|
+
const who = `${e.model.provider ?? "?"}/${e.model.id ?? "?"}`;
|
|
1195
|
+
return `${e.glyph} ${e.label} \xB7 ${who}${current ? " (current)" : ""}`;
|
|
1196
|
+
}
|
|
1197
|
+
__name(pickerOptionLabel, "pickerOptionLabel");
|
|
1198
|
+
|
|
1199
|
+
// node_modules/pi-privacy/src/pii/detect.ts?ext=privateer-privacy
|
|
1200
|
+
var PLACEHOLDER = {
|
|
1201
|
+
email: "\xABemail\xBB",
|
|
1202
|
+
phone: "\xABphone\xBB",
|
|
1203
|
+
ssn: "\xABssn\xBB",
|
|
1204
|
+
"credit-card": "\xABcard\xBB",
|
|
1205
|
+
ip: "\xABip\xBB",
|
|
1206
|
+
iban: "\xABiban\xBB",
|
|
1207
|
+
mac: "\xABmac\xBB",
|
|
1208
|
+
"aws-key": "\xABaws-key\xBB",
|
|
1209
|
+
"gh-token": "\xABtoken\xBB",
|
|
1210
|
+
"api-key": "\xABapi-key\xBB",
|
|
1211
|
+
jwt: "\xABjwt\xBB",
|
|
1212
|
+
"private-key": "\xABprivate-key\xBB"
|
|
1213
|
+
};
|
|
1214
|
+
var SECRET_TYPES = /* @__PURE__ */ new Set([
|
|
1215
|
+
"aws-key",
|
|
1216
|
+
"gh-token",
|
|
1217
|
+
"api-key",
|
|
1218
|
+
"jwt",
|
|
1219
|
+
"private-key"
|
|
1220
|
+
]);
|
|
1221
|
+
var PATTERNS = [
|
|
1222
|
+
{ type: "email", re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, validate: mailboxShaped },
|
|
1223
|
+
{ type: "ssn", re: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
1224
|
+
// ── secrets (prefix-anchored, high precision) ──────────────────────────────
|
|
1225
|
+
// PEM private-key block — match the whole block so redaction removes the key body.
|
|
1226
|
+
{ type: "private-key", re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g },
|
|
1227
|
+
// AWS access key id (AKIA / ASIA + 16 upper-alnum).
|
|
1228
|
+
{ type: "aws-key", re: /\bA(?:KIA|SIA)[0-9A-Z]{16}\b/g },
|
|
1229
|
+
// GitHub tokens: ghp_/gho_/ghu_/ghs_/ghr_ + 36+ base62.
|
|
1230
|
+
{ type: "gh-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,255}\b/g },
|
|
1231
|
+
// JWT: three base64url segments. `eyJ` is base64 of `{"` — a strong header marker.
|
|
1232
|
+
{ type: "jwt", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
1233
|
+
// OpenAI/Anthropic/Privateer & generic `sk-` secret keys.
|
|
1234
|
+
{ type: "api-key", re: /\bsk-[A-Za-z0-9_-]{16,}\b/g },
|
|
1235
|
+
// Slack tokens (bot/user/app/refresh/legacy).
|
|
1236
|
+
{ type: "api-key", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
1237
|
+
// Google API key.
|
|
1238
|
+
{ type: "api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
1239
|
+
// Stripe live secret / restricted keys.
|
|
1240
|
+
{ type: "api-key", re: /\b[sr]k_live_[0-9A-Za-z]{20,}\b/g },
|
|
1241
|
+
// ── consumer PII ───────────────────────────────────────────────────────────
|
|
1242
|
+
// 13–19 digit runs (optionally space/dash grouped) that pass the Luhn check — this
|
|
1243
|
+
// sharply cuts false positives vs "any long number".
|
|
1244
|
+
{ type: "credit-card", re: /\b(?:\d[ -]?){13,19}\b/g, validate: cardShaped },
|
|
1245
|
+
// IPv4 with each octet 0–255. The lookarounds keep it out of longer dotted runs —
|
|
1246
|
+
// `1.2.3.4.5` (an OID), `v1.2.3.4` (a version) — see the precision note above.
|
|
1247
|
+
{ type: "ip", re: /(?<![\w.])(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)(?!\.?\d)/g },
|
|
1248
|
+
// IBAN: 2-letter country + 2 check digits + 11–30 alphanumerics, mod-97 validated
|
|
1249
|
+
// (cuts false positives on random alphanumeric runs sharply).
|
|
1250
|
+
{ type: "iban", re: /\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/g, validate: ibanValid },
|
|
1251
|
+
// MAC address (colon or dash separated). A MAC is exactly six octets, so a run that
|
|
1252
|
+
// CONTINUES on either side is a hex dump or a certificate fingerprint, not an address.
|
|
1253
|
+
{ type: "mac", re: /(?<![0-9A-Fa-f][:-])(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}(?![:-][0-9A-Fa-f]{2})\b/g },
|
|
1254
|
+
// North-American / international-ish phone. Deliberately last + conservative to
|
|
1255
|
+
// avoid eating IDs; requires a plausible separator or leading +.
|
|
1256
|
+
{ type: "phone", re: /(?<![\d.])(?:\+\d{1,3}[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b/g }
|
|
1257
|
+
];
|
|
1258
|
+
var NOT_A_TLD = /* @__PURE__ */ new Set([
|
|
1259
|
+
"png",
|
|
1260
|
+
"jpg",
|
|
1261
|
+
"jpeg",
|
|
1262
|
+
"gif",
|
|
1263
|
+
"webp",
|
|
1264
|
+
"svg",
|
|
1265
|
+
"ico",
|
|
1266
|
+
"bmp",
|
|
1267
|
+
"tif",
|
|
1268
|
+
"tiff",
|
|
1269
|
+
"avif",
|
|
1270
|
+
"mp3",
|
|
1271
|
+
"mp4",
|
|
1272
|
+
"wav",
|
|
1273
|
+
"avi",
|
|
1274
|
+
"mkv",
|
|
1275
|
+
"webm",
|
|
1276
|
+
"ogg",
|
|
1277
|
+
"flac",
|
|
1278
|
+
"pdf",
|
|
1279
|
+
"docx",
|
|
1280
|
+
"xlsx",
|
|
1281
|
+
"pptx",
|
|
1282
|
+
"csv",
|
|
1283
|
+
"json",
|
|
1284
|
+
"yaml",
|
|
1285
|
+
"yml",
|
|
1286
|
+
"toml",
|
|
1287
|
+
"xml",
|
|
1288
|
+
"html",
|
|
1289
|
+
"htm",
|
|
1290
|
+
"css",
|
|
1291
|
+
"scss",
|
|
1292
|
+
"sass",
|
|
1293
|
+
"less",
|
|
1294
|
+
"tsx",
|
|
1295
|
+
"jsx",
|
|
1296
|
+
"mjs",
|
|
1297
|
+
"cjs",
|
|
1298
|
+
"java",
|
|
1299
|
+
"php",
|
|
1300
|
+
"sql",
|
|
1301
|
+
"txt",
|
|
1302
|
+
"log",
|
|
1303
|
+
"lock",
|
|
1304
|
+
"exe",
|
|
1305
|
+
"dll",
|
|
1306
|
+
"dmg",
|
|
1307
|
+
"iso",
|
|
1308
|
+
"tar",
|
|
1309
|
+
"gz",
|
|
1310
|
+
"tgz",
|
|
1311
|
+
"bz2",
|
|
1312
|
+
"rar",
|
|
1313
|
+
"woff",
|
|
1314
|
+
"woff2",
|
|
1315
|
+
"ttf",
|
|
1316
|
+
"otf",
|
|
1317
|
+
"eot",
|
|
1318
|
+
"bak",
|
|
1319
|
+
"tmp",
|
|
1320
|
+
"swp",
|
|
1321
|
+
"map",
|
|
1322
|
+
"min",
|
|
1323
|
+
"snap",
|
|
1324
|
+
"patch",
|
|
1325
|
+
"diff"
|
|
1326
|
+
]);
|
|
1327
|
+
var VERSION_DOMAIN = /^\d+(?:\.\d+)+(?:-[A-Za-z0-9.]+)?$/;
|
|
1328
|
+
function mailboxShaped(value) {
|
|
1329
|
+
const domain = value.slice(value.lastIndexOf("@") + 1).toLowerCase();
|
|
1330
|
+
if (VERSION_DOMAIN.test(domain)) return false;
|
|
1331
|
+
const tld = domain.slice(domain.lastIndexOf(".") + 1);
|
|
1332
|
+
return !NOT_A_TLD.has(tld);
|
|
1333
|
+
}
|
|
1334
|
+
__name(mailboxShaped, "mailboxShaped");
|
|
1335
|
+
var CARD_LENGTHS = /* @__PURE__ */ new Set([13, 14, 15, 16, 19]);
|
|
1336
|
+
function cardShaped(s) {
|
|
1337
|
+
const digits = s.replace(/\D/g, "");
|
|
1338
|
+
if (!CARD_LENGTHS.has(digits.length)) return false;
|
|
1339
|
+
const iin = digits.charCodeAt(0) - 48;
|
|
1340
|
+
if (iin < 2 || iin > 6) return false;
|
|
1341
|
+
return luhn(s);
|
|
1342
|
+
}
|
|
1343
|
+
__name(cardShaped, "cardShaped");
|
|
1344
|
+
function luhn(s) {
|
|
1345
|
+
const digits = s.replace(/\D/g, "");
|
|
1346
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
1347
|
+
let sum = 0;
|
|
1348
|
+
let alt = false;
|
|
1349
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
1350
|
+
let d = digits.charCodeAt(i) - 48;
|
|
1351
|
+
if (alt) {
|
|
1352
|
+
d *= 2;
|
|
1353
|
+
if (d > 9) d -= 9;
|
|
1354
|
+
}
|
|
1355
|
+
sum += d;
|
|
1356
|
+
alt = !alt;
|
|
1357
|
+
}
|
|
1358
|
+
return sum % 10 === 0;
|
|
1359
|
+
}
|
|
1360
|
+
__name(luhn, "luhn");
|
|
1361
|
+
function ibanValid(s) {
|
|
1362
|
+
const iban = s.toUpperCase();
|
|
1363
|
+
if (iban.length < 15 || iban.length > 34) return false;
|
|
1364
|
+
const rearranged = iban.slice(4) + iban.slice(0, 4);
|
|
1365
|
+
let remainder = 0;
|
|
1366
|
+
for (const ch of rearranged) {
|
|
1367
|
+
const code = ch >= "A" && ch <= "Z" ? (ch.charCodeAt(0) - 55).toString() : ch;
|
|
1368
|
+
for (const d of code) remainder = (remainder * 10 + (d.charCodeAt(0) - 48)) % 97;
|
|
1369
|
+
}
|
|
1370
|
+
return remainder === 1;
|
|
1371
|
+
}
|
|
1372
|
+
__name(ibanValid, "ibanValid");
|
|
1373
|
+
function maskPii(type, value) {
|
|
1374
|
+
switch (type) {
|
|
1375
|
+
case "email": {
|
|
1376
|
+
const at = value.lastIndexOf("@");
|
|
1377
|
+
const local = value.slice(0, at);
|
|
1378
|
+
const domain = value.slice(at);
|
|
1379
|
+
const head = local.length > 2 ? `${local[0]}\u2026${local[local.length - 1]}` : "\u2026";
|
|
1380
|
+
return `${head}${domain}`;
|
|
1381
|
+
}
|
|
1382
|
+
case "ip":
|
|
1383
|
+
return value.replace(/\.\d{1,3}$/, ".\u2022");
|
|
1384
|
+
case "mac":
|
|
1385
|
+
return `${value.slice(0, 8)}:\u2022\u2022:\u2022\u2022:\u2022\u2022`;
|
|
1386
|
+
case "private-key":
|
|
1387
|
+
return "-----BEGIN PRIVATE KEY----- block";
|
|
1388
|
+
case "aws-key":
|
|
1389
|
+
case "gh-token":
|
|
1390
|
+
case "api-key":
|
|
1391
|
+
case "jwt":
|
|
1392
|
+
return `${value.slice(0, 6)}\u2026 (${value.length} chars)`;
|
|
1393
|
+
default:
|
|
1394
|
+
return void 0;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
__name(maskPii, "maskPii");
|
|
1398
|
+
function scanPii(text, opts = {}) {
|
|
1399
|
+
if (!text) return { hits: [], suppressed: [], samples: [] };
|
|
1400
|
+
const { allow, samples: sampleCap = 4 } = opts;
|
|
1401
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1402
|
+
const skipped = /* @__PURE__ */ new Map();
|
|
1403
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1404
|
+
for (const { type, re, validate } of PATTERNS) {
|
|
1405
|
+
for (const m of text.matchAll(re)) {
|
|
1406
|
+
if (validate && !validate(m[0])) continue;
|
|
1407
|
+
if (allow?.(type, m[0])) {
|
|
1408
|
+
skipped.set(type, (skipped.get(type) ?? 0) + 1);
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
1412
|
+
if (sampleCap > 0) {
|
|
1413
|
+
const masked = maskPii(type, m[0]);
|
|
1414
|
+
if (masked) {
|
|
1415
|
+
const set = seen.get(type) ?? /* @__PURE__ */ new Set();
|
|
1416
|
+
if (set.size < sampleCap) set.add(masked);
|
|
1417
|
+
seen.set(type, set);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const toHits = /* @__PURE__ */ __name((m) => [...m.entries()].map(([type, count]) => ({ type, count })), "toHits");
|
|
1423
|
+
return {
|
|
1424
|
+
hits: toHits(counts),
|
|
1425
|
+
suppressed: toHits(skipped),
|
|
1426
|
+
samples: [...seen.entries()].flatMap(([type, set]) => [...set].map((masked) => ({ type, masked })))
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
__name(scanPii, "scanPii");
|
|
1430
|
+
function detectPii(text, opts = {}) {
|
|
1431
|
+
return scanPii(text, { ...opts, samples: 0 }).hits;
|
|
1432
|
+
}
|
|
1433
|
+
__name(detectPii, "detectPii");
|
|
1434
|
+
function hasSecrets(hits) {
|
|
1435
|
+
return hits.some((h) => SECRET_TYPES.has(h.type));
|
|
1436
|
+
}
|
|
1437
|
+
__name(hasSecrets, "hasSecrets");
|
|
1438
|
+
function secretHits(hits) {
|
|
1439
|
+
return hits.filter((h) => SECRET_TYPES.has(h.type));
|
|
1440
|
+
}
|
|
1441
|
+
__name(secretHits, "secretHits");
|
|
1442
|
+
function redactPii(text, only, allow) {
|
|
1443
|
+
let out = text;
|
|
1444
|
+
for (const { type, re, validate } of PATTERNS) {
|
|
1445
|
+
if (only && !only.has(type)) continue;
|
|
1446
|
+
out = out.replace(re, (m) => {
|
|
1447
|
+
if (validate && !validate(m)) return m;
|
|
1448
|
+
if (allow?.(type, m)) return m;
|
|
1449
|
+
return PLACEHOLDER[type];
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
return out;
|
|
1453
|
+
}
|
|
1454
|
+
__name(redactPii, "redactPii");
|
|
1455
|
+
function summarizePii(hits) {
|
|
1456
|
+
const label = {
|
|
1457
|
+
email: ["email", "emails"],
|
|
1458
|
+
phone: ["phone number", "phone numbers"],
|
|
1459
|
+
ssn: ["SSN", "SSNs"],
|
|
1460
|
+
"credit-card": ["card number", "card numbers"],
|
|
1461
|
+
ip: ["IP address", "IP addresses"],
|
|
1462
|
+
iban: ["IBAN", "IBANs"],
|
|
1463
|
+
mac: ["MAC address", "MAC addresses"],
|
|
1464
|
+
"aws-key": ["AWS key", "AWS keys"],
|
|
1465
|
+
"gh-token": ["GitHub token", "GitHub tokens"],
|
|
1466
|
+
"api-key": ["API key", "API keys"],
|
|
1467
|
+
jwt: ["JWT", "JWTs"],
|
|
1468
|
+
"private-key": ["private key", "private keys"]
|
|
1469
|
+
};
|
|
1470
|
+
return hits.map((h) => `${h.count} ${label[h.type][h.count === 1 ? 0 : 1]}`).join(", ");
|
|
1471
|
+
}
|
|
1472
|
+
__name(summarizePii, "summarizePii");
|
|
1473
|
+
function piiDetail(scan) {
|
|
1474
|
+
const lines = [];
|
|
1475
|
+
for (const hit of scan.hits) {
|
|
1476
|
+
const shown = scan.samples.filter((s) => s.type === hit.type).map((s) => s.masked);
|
|
1477
|
+
const more = hit.count - shown.length;
|
|
1478
|
+
const tail = shown.length ? `: ${shown.join(", ")}${more > 0 ? `, +${more} more` : ""}` : " (masked \u2014 not shown)";
|
|
1479
|
+
lines.push(` \u2022 ${summarizePii([hit])}${tail}`);
|
|
1480
|
+
}
|
|
1481
|
+
if (scan.suppressed.length) {
|
|
1482
|
+
lines.push(` \u2022 allowlisted (not counted): ${summarizePii(scan.suppressed)}`);
|
|
1483
|
+
}
|
|
1484
|
+
lines.push(" Values are masked here; the full values are in what would be sent.");
|
|
1485
|
+
return lines.join("\n");
|
|
1486
|
+
}
|
|
1487
|
+
__name(piiDetail, "piiDetail");
|
|
1488
|
+
function piiInline(scan) {
|
|
1489
|
+
return scan.hits.map((h) => {
|
|
1490
|
+
const shown = scan.samples.filter((s) => s.type === h.type).map((s) => s.masked);
|
|
1491
|
+
const more = h.count - shown.length;
|
|
1492
|
+
const tail = shown.length ? ` (${shown.join(", ")}${more > 0 ? `, +${more} more` : ""})` : "";
|
|
1493
|
+
return `${summarizePii([h])}${tail}`;
|
|
1494
|
+
}).join(", ");
|
|
1495
|
+
}
|
|
1496
|
+
__name(piiInline, "piiInline");
|
|
1497
|
+
function newPii(hits, baseline) {
|
|
1498
|
+
const out = [];
|
|
1499
|
+
for (const h of hits) {
|
|
1500
|
+
const seen = baseline.get(h.type) ?? 0;
|
|
1501
|
+
if (h.count > seen) out.push({ type: h.type, count: h.count - seen });
|
|
1502
|
+
}
|
|
1503
|
+
return out;
|
|
1504
|
+
}
|
|
1505
|
+
__name(newPii, "newPii");
|
|
1506
|
+
function mergePiiBaseline(baseline, hits) {
|
|
1507
|
+
const out = new Map(baseline);
|
|
1508
|
+
for (const h of hits) out.set(h.type, Math.max(out.get(h.type) ?? 0, h.count));
|
|
1509
|
+
return out;
|
|
1510
|
+
}
|
|
1511
|
+
__name(mergePiiBaseline, "mergePiiBaseline");
|
|
1512
|
+
|
|
1513
|
+
// node_modules/pi-privacy/src/posture/downgrade.ts?ext=privateer-privacy
|
|
1514
|
+
var EXPOSURE = {
|
|
1515
|
+
"tee-verified": 0,
|
|
1516
|
+
// provider cannot read the payload
|
|
1517
|
+
local: 0,
|
|
1518
|
+
// payload never leaves the machine
|
|
1519
|
+
"zdr-enforced": 1,
|
|
1520
|
+
// provider reads it; retention observably refused
|
|
1521
|
+
"zdr-policy": 2,
|
|
1522
|
+
// provider reads it; retention refused by promise only
|
|
1523
|
+
"tee-unverified": 2,
|
|
1524
|
+
// enclave claimed, unproven — assume it reads it
|
|
1525
|
+
standard: 3
|
|
1526
|
+
// read and retained at the provider's discretion
|
|
1527
|
+
};
|
|
1528
|
+
function exposureLevel(tier) {
|
|
1529
|
+
return EXPOSURE[tier] ?? 3;
|
|
1530
|
+
}
|
|
1531
|
+
__name(exposureLevel, "exposureLevel");
|
|
1532
|
+
function assessDowngrade(from, to, hits = []) {
|
|
1533
|
+
const a = from ?? "standard";
|
|
1534
|
+
const b = to ?? "standard";
|
|
1535
|
+
return {
|
|
1536
|
+
downgrade: from !== void 0 && to !== void 0 && exposureLevel(b) > exposureLevel(a),
|
|
1537
|
+
from: a,
|
|
1538
|
+
to: b,
|
|
1539
|
+
severity: hits.length === 0 ? "none" : hasSecrets(hits) ? "secret" : "pii"
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
__name(assessDowngrade, "assessDowngrade");
|
|
1543
|
+
function downgradeWarning(a, hits, modelLabel) {
|
|
1544
|
+
const target = modelLabel ? ` to ${modelLabel}` : "";
|
|
1545
|
+
const carried = hits.length ? `carrying ${summarizePii(hits)}` : "already in context";
|
|
1546
|
+
return `\u26A0 Privacy downgrade: ${TIERS[a.from].label} \u2192 ${TIERS[a.to].label}. This session's history \u2014 ${carried} \u2014 will be re-sent${target} on the next turn. Detection is best-effort structured PII/secrets, so treat it as a floor, not a full inventory.`;
|
|
1547
|
+
}
|
|
1548
|
+
__name(downgradeWarning, "downgradeWarning");
|
|
1549
|
+
|
|
1550
|
+
// node_modules/pi-privacy/src/posture/verify.ts?ext=privateer-privacy
|
|
1551
|
+
async function verifyModelPosture(providerId, modelId, opts = {}) {
|
|
1552
|
+
const p = PROVIDER_BY_ID[providerId];
|
|
1553
|
+
if (p?.id === "tinfoil") {
|
|
1554
|
+
try {
|
|
1555
|
+
const att = await fetchTinfoilAttestation(
|
|
1556
|
+
{ baseURL: opts.baseUrl },
|
|
1557
|
+
opts.transport ?? httpsTransport
|
|
1558
|
+
);
|
|
1559
|
+
const tp = tinfoilTeePosture(att);
|
|
1560
|
+
return { providerId, modelId, tier: tierFromTeePosture(tp), teePosture: tp, attestation: att };
|
|
1561
|
+
} catch (e) {
|
|
1562
|
+
return { providerId, modelId, tier: "tee-unverified", error: e.message };
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (p?.id === "nearai") {
|
|
1566
|
+
try {
|
|
1567
|
+
const att = await fetchAttestation({ apiKey: opts.apiKey, baseURL: opts.baseUrl }, modelId);
|
|
1568
|
+
const tp = teePosture(att);
|
|
1569
|
+
return { providerId, modelId, tier: tierFromTeePosture(tp), teePosture: tp, attestation: att };
|
|
1570
|
+
} catch (e) {
|
|
1571
|
+
return { providerId, modelId, tier: "tee-unverified", error: e.message };
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return {
|
|
1575
|
+
providerId,
|
|
1576
|
+
modelId,
|
|
1577
|
+
tier: effectiveTier(providerId, { baseUrl: opts.baseUrl, zdrEnforced: opts.zdrEnforced })
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
__name(verifyModelPosture, "verifyModelPosture");
|
|
1581
|
+
|
|
1582
|
+
// node_modules/pi-privacy/src/ext/patches.ts?ext=privateer-privacy
|
|
1583
|
+
function asObject(payload) {
|
|
1584
|
+
return payload && typeof payload === "object" ? { ...payload } : {};
|
|
1585
|
+
}
|
|
1586
|
+
__name(asObject, "asObject");
|
|
1587
|
+
function veniceRequestPatch(payload) {
|
|
1588
|
+
const p = asObject(payload);
|
|
1589
|
+
const vp = p.venice_parameters && typeof p.venice_parameters === "object" ? p.venice_parameters : {};
|
|
1590
|
+
return { ...p, venice_parameters: { ...vp, include_venice_system_prompt: false } };
|
|
1591
|
+
}
|
|
1592
|
+
__name(veniceRequestPatch, "veniceRequestPatch");
|
|
1593
|
+
function openRouterZdrPatch(payload) {
|
|
1594
|
+
const p = asObject(payload);
|
|
1595
|
+
const provider = p.provider && typeof p.provider === "object" ? p.provider : {};
|
|
1596
|
+
return { ...p, provider: { ...provider, zdr: true, data_collection: "deny" } };
|
|
1597
|
+
}
|
|
1598
|
+
__name(openRouterZdrPatch, "openRouterZdrPatch");
|
|
1599
|
+
|
|
1600
|
+
// node_modules/pi-privacy/src/pii/allow.ts?ext=privateer-privacy
|
|
1601
|
+
var DEFAULT_ALLOW = [
|
|
1602
|
+
// RFC 2606 example domains + RFC 6761 special-use TLDs, and their subdomains.
|
|
1603
|
+
"@example.com",
|
|
1604
|
+
"@example.org",
|
|
1605
|
+
"@example.net",
|
|
1606
|
+
"@example",
|
|
1607
|
+
"@test",
|
|
1608
|
+
"@invalid",
|
|
1609
|
+
"@localhost",
|
|
1610
|
+
"@local",
|
|
1611
|
+
// Git/GitHub machine senders — the single biggest source of "N emails detected"
|
|
1612
|
+
// in a repository, and by construction not a mailbox anyone reads.
|
|
1613
|
+
"@users.noreply.github.com",
|
|
1614
|
+
"noreply@*",
|
|
1615
|
+
"no-reply@*",
|
|
1616
|
+
"donotreply@*",
|
|
1617
|
+
"do-not-reply@*",
|
|
1618
|
+
// Loopback, unspecified, broadcast, link-local. NOTE private LAN ranges
|
|
1619
|
+
// (10/8, 172.16/12, 192.168/16) are deliberately NOT here: they identify a real
|
|
1620
|
+
// host on a real network, which is exactly the thing the gate is for.
|
|
1621
|
+
"127.0.0.0/8",
|
|
1622
|
+
"0.0.0.0",
|
|
1623
|
+
"255.255.255.255",
|
|
1624
|
+
"169.254.0.0/16"
|
|
1625
|
+
];
|
|
1626
|
+
var IPV4 = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
1627
|
+
function ipToInt(ip) {
|
|
1628
|
+
const parts = ip.split(".").map(Number);
|
|
1629
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return void 0;
|
|
1630
|
+
return (parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
|
|
1631
|
+
}
|
|
1632
|
+
__name(ipToInt, "ipToInt");
|
|
1633
|
+
function globToRe(pattern) {
|
|
1634
|
+
const body = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
1635
|
+
return new RegExp(`^${body}$`, "i");
|
|
1636
|
+
}
|
|
1637
|
+
__name(globToRe, "globToRe");
|
|
1638
|
+
function parseRule(raw) {
|
|
1639
|
+
const entry = raw.trim();
|
|
1640
|
+
if (!entry) return void 0;
|
|
1641
|
+
if (/^\*+$/.test(entry)) return void 0;
|
|
1642
|
+
const cidr = /^(\d{1,3}(?:\.\d{1,3}){3})\/(\d{1,2})$/.exec(entry);
|
|
1643
|
+
if (cidr) {
|
|
1644
|
+
const net = ipToInt(cidr[1]);
|
|
1645
|
+
const len = Number(cidr[2]);
|
|
1646
|
+
if (net === void 0 || len > 32) return void 0;
|
|
1647
|
+
const mask = len === 0 ? 0 : 4294967295 << 32 - len >>> 0;
|
|
1648
|
+
return { kind: "cidr", net: (net & mask) >>> 0, mask };
|
|
1649
|
+
}
|
|
1650
|
+
if (entry.startsWith("@")) {
|
|
1651
|
+
const domain = entry.slice(1).toLowerCase();
|
|
1652
|
+
return domain ? { kind: "domain", domain } : void 0;
|
|
1653
|
+
}
|
|
1654
|
+
if (!entry.includes("@") && entry.includes(".") && !entry.includes("*") && !IPV4.test(entry)) {
|
|
1655
|
+
return { kind: "domain", domain: entry.toLowerCase() };
|
|
1656
|
+
}
|
|
1657
|
+
return { kind: "glob", re: globToRe(entry) };
|
|
1658
|
+
}
|
|
1659
|
+
__name(parseRule, "parseRule");
|
|
1660
|
+
function ruleMatches(rule, type, value) {
|
|
1661
|
+
switch (rule.kind) {
|
|
1662
|
+
case "domain": {
|
|
1663
|
+
if (type !== "email") return false;
|
|
1664
|
+
const at = value.lastIndexOf("@");
|
|
1665
|
+
if (at < 0) return false;
|
|
1666
|
+
const domain = value.slice(at + 1).toLowerCase();
|
|
1667
|
+
return domain === rule.domain || domain.endsWith(`.${rule.domain}`);
|
|
1668
|
+
}
|
|
1669
|
+
case "cidr": {
|
|
1670
|
+
if (type !== "ip") return false;
|
|
1671
|
+
const n = ipToInt(value);
|
|
1672
|
+
return n !== void 0 && (n & rule.mask) >>> 0 === rule.net;
|
|
1673
|
+
}
|
|
1674
|
+
case "glob":
|
|
1675
|
+
return rule.re.test(value);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
__name(ruleMatches, "ruleMatches");
|
|
1679
|
+
function compileAllow(entries = [], opts = {}) {
|
|
1680
|
+
const { defaults = true, warn } = opts;
|
|
1681
|
+
const rules = [];
|
|
1682
|
+
for (const raw of [...defaults ? DEFAULT_ALLOW : [], ...entries]) {
|
|
1683
|
+
const rule = parseRule(raw);
|
|
1684
|
+
if (rule) rules.push(rule);
|
|
1685
|
+
else warn?.(`piiAllow entry ${JSON.stringify(raw)} is not a usable pattern \u2014 ignoring it.`);
|
|
1686
|
+
}
|
|
1687
|
+
return (type, value) => rules.some((r) => ruleMatches(r, type, value));
|
|
1688
|
+
}
|
|
1689
|
+
__name(compileAllow, "compileAllow");
|
|
1690
|
+
|
|
1691
|
+
// node_modules/pi-privacy/src/pii/command.ts?ext=privateer-privacy
|
|
1692
|
+
var PII_COMMAND_WORDS = ["off", "warn", "redact", "on", "status"];
|
|
1693
|
+
var SYNONYMS = {
|
|
1694
|
+
off: { kind: "set", mode: "off" },
|
|
1695
|
+
bypass: { kind: "set", mode: "off" },
|
|
1696
|
+
disable: { kind: "set", mode: "off" },
|
|
1697
|
+
warn: { kind: "set", mode: "warn" },
|
|
1698
|
+
ask: { kind: "set", mode: "warn" },
|
|
1699
|
+
prompt: { kind: "set", mode: "warn" },
|
|
1700
|
+
redact: { kind: "set", mode: "redact" },
|
|
1701
|
+
mask: { kind: "set", mode: "redact" },
|
|
1702
|
+
on: { kind: "restore" },
|
|
1703
|
+
restore: { kind: "restore" },
|
|
1704
|
+
reset: { kind: "restore" },
|
|
1705
|
+
default: { kind: "restore" },
|
|
1706
|
+
status: { kind: "status" },
|
|
1707
|
+
show: { kind: "status" }
|
|
1708
|
+
};
|
|
1709
|
+
function parsePiiCommand(args) {
|
|
1710
|
+
const word = (typeof args === "string" ? args : "").trim().split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
1711
|
+
if (!word) return { kind: "status" };
|
|
1712
|
+
return SYNONYMS[word] ?? { kind: "unknown", word };
|
|
1713
|
+
}
|
|
1714
|
+
__name(parsePiiCommand, "parsePiiCommand");
|
|
1715
|
+
var MODE_BLURB = {
|
|
1716
|
+
warn: "you are asked before PII goes down an unverified channel",
|
|
1717
|
+
redact: "PII is masked before send, without asking",
|
|
1718
|
+
off: "outbound requests are not gated on PII at all"
|
|
1719
|
+
};
|
|
1720
|
+
function stillOn(s) {
|
|
1721
|
+
const on = [];
|
|
1722
|
+
if (s.toolExfilPolicy !== "off") on.push("credentials leaving via a tool call");
|
|
1723
|
+
if (s.toolResultPolicy !== "off") on.push("credentials arriving in a tool result");
|
|
1724
|
+
if (s.downgradePolicy !== "off") on.push("a model downgrade carrying credentials");
|
|
1725
|
+
return on;
|
|
1726
|
+
}
|
|
1727
|
+
__name(stillOn, "stillOn");
|
|
1728
|
+
function statusLine(s) {
|
|
1729
|
+
const effective = s.session ?? s.configured;
|
|
1730
|
+
const source = s.session ? `set by /${s.commandName} this session` : "configured";
|
|
1731
|
+
return `PII gate: ${effective} (${source}) \u2014 ${MODE_BLURB[effective]}. Configured: ${s.configured}. Tool exfil: ${s.toolExfilPolicy}. Tool result: ${s.toolResultPolicy}. Downgrade: ${s.downgradePolicy}.`;
|
|
1732
|
+
}
|
|
1733
|
+
__name(statusLine, "statusLine");
|
|
1734
|
+
var usage = /* @__PURE__ */ __name((name) => `Usage: /${name} ${PII_COMMAND_WORDS.join(" | ")}.`, "usage");
|
|
1735
|
+
function runPiiCommand(args, s) {
|
|
1736
|
+
const action = parsePiiCommand(args);
|
|
1737
|
+
const before = s.session ?? s.configured;
|
|
1738
|
+
if (action.kind === "status")
|
|
1739
|
+
return { session: s.session, changed: false, message: `${statusLine(s)} ${usage(s.commandName)}`, level: "info" };
|
|
1740
|
+
if (action.kind === "unknown")
|
|
1741
|
+
return {
|
|
1742
|
+
session: s.session,
|
|
1743
|
+
changed: false,
|
|
1744
|
+
// Say what is still in force, so a mistyped bypass can't be mistaken for a
|
|
1745
|
+
// successful one: the user who typed `/pii of` must not walk away believing
|
|
1746
|
+
// the gate is down.
|
|
1747
|
+
message: `"${action.word}" is not a /${s.commandName} setting \u2014 nothing changed. ${statusLine(s)} ${usage(s.commandName)}`,
|
|
1748
|
+
level: "warning"
|
|
1749
|
+
};
|
|
1750
|
+
if (action.kind === "restore") {
|
|
1751
|
+
if (s.configured === "off")
|
|
1752
|
+
return {
|
|
1753
|
+
session: void 0,
|
|
1754
|
+
changed: before !== "off",
|
|
1755
|
+
message: `Your configured PII policy is "off", so there is nothing to restore \u2014 the gate stays off. Use /${s.commandName} warn or /${s.commandName} redact to turn it on for this session.`,
|
|
1756
|
+
level: "warning"
|
|
1757
|
+
};
|
|
1758
|
+
return {
|
|
1759
|
+
session: void 0,
|
|
1760
|
+
changed: before !== s.configured,
|
|
1761
|
+
message: `PII gate restored to your configured policy: ${s.configured} \u2014 ${MODE_BLURB[s.configured]}.`,
|
|
1762
|
+
level: "info"
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
const changed = before !== action.mode;
|
|
1766
|
+
if (action.mode === "off") {
|
|
1767
|
+
const on = stillOn(s);
|
|
1768
|
+
const remains = on.length ? `Still on: ${on.join(", ")}.` : `Nothing else is gating this session \u2014 the tool, result and downgrade guards are all off too.`;
|
|
1769
|
+
return {
|
|
1770
|
+
session: "off",
|
|
1771
|
+
changed,
|
|
1772
|
+
message: `\u2691 PII gate OFF for this session. Outbound requests are no longer scanned-and-gated for PII, and a tool call carrying PII but no credential no longer prompts. ${remains} This lasts for this session only \u2014 /${s.commandName} on restores "${s.configured}", and so does a restart.`,
|
|
1773
|
+
// A bypass is not routine output. It stays at warning level so it reads as the
|
|
1774
|
+
// guard coming down, which is what it is.
|
|
1775
|
+
level: "warning"
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
return {
|
|
1779
|
+
session: action.mode,
|
|
1780
|
+
changed,
|
|
1781
|
+
message: `PII gate: ${action.mode} for this session \u2014 ${MODE_BLURB[action.mode]}.` + (action.mode === s.configured ? "" : ` (Configured: ${s.configured}.)`) + (changed ? " Earlier answers this session are cleared, so the gate re-asks about PII already in context." : ""),
|
|
1782
|
+
level: "info"
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
__name(runPiiCommand, "runPiiCommand");
|
|
1786
|
+
|
|
1787
|
+
// node_modules/pi-privacy/src/ext/toolgate.ts?ext=privateer-privacy
|
|
1788
|
+
var LOCAL_TOOLS = /* @__PURE__ */ new Set(["read", "grep", "find", "ls", "edit", "write"]);
|
|
1789
|
+
var EGRESS_CMD = /(?:^|[\s|&;(`$])(?:curl|wget|nc|ncat|netcat|scp|sftp|rsync|telnet|ftp|xh|httpie|kubectl|aws|gcloud|az)\b|>\s*\/dev\/tcp\/|\bgit\s+push\b|\bssh\s/i;
|
|
1790
|
+
var URL_RE = /\bhttps?:\/\/[^\s"'`)<>]+/gi;
|
|
1791
|
+
var TOK_START = `(?:^|[\\s"'\`=@<(|])`;
|
|
1792
|
+
var TOK_END = `(?=$|[\\s"'\`)>;,|&])`;
|
|
1793
|
+
var DIR = `(?:[\\w.~/-]*/)?`;
|
|
1794
|
+
var SENSITIVE_FILES = [
|
|
1795
|
+
{ label: ".env file", re: new RegExp(`${TOK_START}${DIR}\\.env(?:\\.[\\w-]+)?${TOK_END}`, "i") },
|
|
1796
|
+
{ label: "SSH key material", re: new RegExp(`${TOK_START}${DIR}\\.ssh(?:/[\\w.-]+)?${TOK_END}`, "i") },
|
|
1797
|
+
// id_rsa but NOT id_rsa.pub — the trailing "." fails TOK_END, so the public half
|
|
1798
|
+
// (which is not a secret) never fires the gate.
|
|
1799
|
+
{ label: "SSH private key", re: new RegExp(`${TOK_START}${DIR}id_(?:rsa|dsa|ecdsa|ed25519)${TOK_END}`, "i") },
|
|
1800
|
+
{ label: "AWS credentials", re: new RegExp(`${DIR}\\.aws/(?:credentials|config)${TOK_END}`, "i") },
|
|
1801
|
+
{ label: "kubeconfig", re: new RegExp(`${DIR}\\.kube/config${TOK_END}`, "i") },
|
|
1802
|
+
{
|
|
1803
|
+
label: "stored credentials",
|
|
1804
|
+
re: new RegExp(`${TOK_START}${DIR}\\.(?:npmrc|netrc|pypirc|git-credentials|docker/config\\.json)${TOK_END}`, "i")
|
|
1805
|
+
},
|
|
1806
|
+
// Private-key / keystore files. Requires a literal dot before the extension, so
|
|
1807
|
+
// header names like `x-api-key` don't match.
|
|
1808
|
+
{
|
|
1809
|
+
label: "key or certificate file",
|
|
1810
|
+
re: new RegExp(`${TOK_START}${DIR}[\\w.-]+\\.(?:pem|key|p12|pfx|jks|keystore|kdbx)${TOK_END}`, "i")
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
label: "secrets file",
|
|
1814
|
+
re: new RegExp(`${TOK_START}${DIR}(?:secrets?|credentials?)\\.(?:json|ya?ml|env|txt|toml|csv)${TOK_END}`, "i")
|
|
1815
|
+
}
|
|
1816
|
+
];
|
|
1817
|
+
function sensitiveFileRefs(text) {
|
|
1818
|
+
const out = [];
|
|
1819
|
+
for (const { label, re } of SENSITIVE_FILES) {
|
|
1820
|
+
if (re.test(text) && !out.includes(label)) out.push(label);
|
|
1821
|
+
}
|
|
1822
|
+
return out;
|
|
1823
|
+
}
|
|
1824
|
+
__name(sensitiveFileRefs, "sensitiveFileRefs");
|
|
1825
|
+
function safeStringify(input) {
|
|
1826
|
+
if (typeof input === "string") return input;
|
|
1827
|
+
try {
|
|
1828
|
+
return JSON.stringify(input ?? "");
|
|
1829
|
+
} catch {
|
|
1830
|
+
return String(input ?? "");
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
__name(safeStringify, "safeStringify");
|
|
1834
|
+
function firstRemoteUrl(text) {
|
|
1835
|
+
const urls = text.match(URL_RE);
|
|
1836
|
+
if (!urls) return void 0;
|
|
1837
|
+
return urls.find((u) => !isLocalEndpoint(u));
|
|
1838
|
+
}
|
|
1839
|
+
__name(firstRemoteUrl, "firstRemoteUrl");
|
|
1840
|
+
function splitCommands(cmd) {
|
|
1841
|
+
return cmd.split(/\|\||&&|[;\n|]/).map((s) => s.trim()).filter(Boolean);
|
|
1842
|
+
}
|
|
1843
|
+
__name(splitCommands, "splitCommands");
|
|
1844
|
+
function assessToolCall(toolName, input) {
|
|
1845
|
+
const name = toolName ?? "";
|
|
1846
|
+
if (LOCAL_TOOLS.has(name)) return { egress: false };
|
|
1847
|
+
const text = safeStringify(input);
|
|
1848
|
+
const remote = firstRemoteUrl(text);
|
|
1849
|
+
if (name === "bash") {
|
|
1850
|
+
const cmd = typeof input?.command === "string" ? input.command : text;
|
|
1851
|
+
let target;
|
|
1852
|
+
let egress = false;
|
|
1853
|
+
const files2 = [];
|
|
1854
|
+
for (const seg of splitCommands(cmd)) {
|
|
1855
|
+
const remote2 = firstRemoteUrl(seg);
|
|
1856
|
+
const hasUrl = URL_RE.test(seg);
|
|
1857
|
+
URL_RE.lastIndex = 0;
|
|
1858
|
+
if (remote2 || !hasUrl && EGRESS_CMD.test(seg)) {
|
|
1859
|
+
egress = true;
|
|
1860
|
+
target ??= remote2;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (egress) files2.push(...sensitiveFileRefs(cmd));
|
|
1864
|
+
return { egress, target, sensitiveFiles: files2.length ? files2 : void 0 };
|
|
1865
|
+
}
|
|
1866
|
+
if (!remote) return { egress: false };
|
|
1867
|
+
const files = sensitiveFileRefs(text);
|
|
1868
|
+
return { egress: true, target: remote, sensitiveFiles: files.length ? files : void 0 };
|
|
1869
|
+
}
|
|
1870
|
+
__name(assessToolCall, "assessToolCall");
|
|
1871
|
+
|
|
1872
|
+
// node_modules/pi-privacy/src/surface/tools.ts?ext=privateer-privacy
|
|
1873
|
+
var BUILTIN_TOOLS = /* @__PURE__ */ new Set([
|
|
1874
|
+
"bash",
|
|
1875
|
+
"read",
|
|
1876
|
+
"write",
|
|
1877
|
+
"edit",
|
|
1878
|
+
"grep",
|
|
1879
|
+
"find",
|
|
1880
|
+
"ls"
|
|
1881
|
+
]);
|
|
1882
|
+
var SHELL_TOOLS = /* @__PURE__ */ new Set(["bash"]);
|
|
1883
|
+
var URL_PARAM = /^(?:url|uri|endpoint|host|hostname|origin|webhook|server|base_?url|address)$/i;
|
|
1884
|
+
var URL_IN_TEXT = /\bhttps?:\/\//i;
|
|
1885
|
+
function schemaNamesNetwork(schema, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
1886
|
+
if (depth > 6 || schema === null || typeof schema !== "object") return false;
|
|
1887
|
+
if (seen.has(schema)) return false;
|
|
1888
|
+
seen.add(schema);
|
|
1889
|
+
if (Array.isArray(schema)) return schema.some((s) => schemaNamesNetwork(s, depth + 1, seen));
|
|
1890
|
+
const obj = schema;
|
|
1891
|
+
const props = obj.properties;
|
|
1892
|
+
if (props && typeof props === "object" && !Array.isArray(props)) {
|
|
1893
|
+
for (const [key, val] of Object.entries(props)) {
|
|
1894
|
+
if (URL_PARAM.test(key)) return true;
|
|
1895
|
+
if (schemaNamesNetwork(val, depth + 1, seen)) return true;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
for (const key of ["items", "additionalProperties", "oneOf", "anyOf", "allOf", "$defs"]) {
|
|
1899
|
+
if (key in obj && schemaNamesNetwork(obj[key], depth + 1, seen)) return true;
|
|
1900
|
+
}
|
|
1901
|
+
return false;
|
|
1902
|
+
}
|
|
1903
|
+
__name(schemaNamesNetwork, "schemaNamesNetwork");
|
|
1904
|
+
function toolProvenance(info) {
|
|
1905
|
+
const si = info.sourceInfo;
|
|
1906
|
+
if (si?.scope === "project") return "project";
|
|
1907
|
+
if (si?.scope === "temporary") return "temporary";
|
|
1908
|
+
if (info.name && BUILTIN_TOOLS.has(info.name)) return "builtin";
|
|
1909
|
+
if (si?.origin === "package") return "package";
|
|
1910
|
+
if (si?.scope === "user") return "user";
|
|
1911
|
+
return "unknown";
|
|
1912
|
+
}
|
|
1913
|
+
__name(toolProvenance, "toolProvenance");
|
|
1914
|
+
function toolReach(info, provenance) {
|
|
1915
|
+
const name = info.name ?? "";
|
|
1916
|
+
if (SHELL_TOOLS.has(name) && provenance === "builtin") return "shell";
|
|
1917
|
+
if (LOCAL_TOOLS.has(name) && provenance === "builtin") return "local";
|
|
1918
|
+
if (SHELL_TOOLS.has(name)) return "shell";
|
|
1919
|
+
if (schemaNamesNetwork(info.parameters)) return "network";
|
|
1920
|
+
if (URL_IN_TEXT.test(info.description ?? "") || URL_IN_TEXT.test(info.promptGuidelines ?? "")) return "network";
|
|
1921
|
+
return "unknown";
|
|
1922
|
+
}
|
|
1923
|
+
__name(toolReach, "toolReach");
|
|
1924
|
+
var PROVENANCE_LABEL = {
|
|
1925
|
+
builtin: "builtin",
|
|
1926
|
+
user: "user",
|
|
1927
|
+
package: "package",
|
|
1928
|
+
project: "project",
|
|
1929
|
+
temporary: "temporary",
|
|
1930
|
+
unknown: "unattributed"
|
|
1931
|
+
};
|
|
1932
|
+
var REACH_LABEL = {
|
|
1933
|
+
local: "local files only",
|
|
1934
|
+
shell: "shell (unbounded)",
|
|
1935
|
+
network: "network (declared)",
|
|
1936
|
+
unknown: "unassessed"
|
|
1937
|
+
};
|
|
1938
|
+
function isRepoSupplied(p) {
|
|
1939
|
+
return p === "project" || p === "temporary";
|
|
1940
|
+
}
|
|
1941
|
+
__name(isRepoSupplied, "isRepoSupplied");
|
|
1942
|
+
function classifyTool(info) {
|
|
1943
|
+
const provenance = toolProvenance(info);
|
|
1944
|
+
const reach = toolReach(info, provenance);
|
|
1945
|
+
const name = info.name ?? "(unnamed)";
|
|
1946
|
+
const flagged = isRepoSupplied(provenance) || provenance === "unknown";
|
|
1947
|
+
return {
|
|
1948
|
+
name,
|
|
1949
|
+
provenance,
|
|
1950
|
+
reach,
|
|
1951
|
+
sourcePath: info.sourceInfo?.path,
|
|
1952
|
+
glyph: flagged ? "\u26A0" : "\u2022",
|
|
1953
|
+
provenanceLabel: PROVENANCE_LABEL[provenance],
|
|
1954
|
+
reachLabel: REACH_LABEL[reach],
|
|
1955
|
+
concern: concernFor(provenance, info.sourceInfo?.path)
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
__name(classifyTool, "classifyTool");
|
|
1959
|
+
function concernFor(p, path) {
|
|
1960
|
+
const where = path ? ` (${path})` : "";
|
|
1961
|
+
switch (p) {
|
|
1962
|
+
case "project":
|
|
1963
|
+
return `supplied by this project${where}, not by you \u2014 it arrived with the repository`;
|
|
1964
|
+
case "temporary":
|
|
1965
|
+
return `supplied by a CLI override for this run${where}, not by your configuration`;
|
|
1966
|
+
case "unknown":
|
|
1967
|
+
return "no source metadata \u2014 pi-privacy can't say where this came from";
|
|
1968
|
+
default:
|
|
1969
|
+
return void 0;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
__name(concernFor, "concernFor");
|
|
1973
|
+
var PROVENANCE_ORDER = ["project", "temporary", "unknown", "package", "user", "builtin"];
|
|
1974
|
+
function rankSurface(tools) {
|
|
1975
|
+
return tools.map(classifyTool).sort(
|
|
1976
|
+
(a, b) => PROVENANCE_ORDER.indexOf(a.provenance) - PROVENANCE_ORDER.indexOf(b.provenance) || a.name.localeCompare(b.name)
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
__name(rankSurface, "rankSurface");
|
|
1980
|
+
function summarizeSurface(entries) {
|
|
1981
|
+
const byProvenance = {
|
|
1982
|
+
builtin: 0,
|
|
1983
|
+
user: 0,
|
|
1984
|
+
package: 0,
|
|
1985
|
+
project: 0,
|
|
1986
|
+
temporary: 0,
|
|
1987
|
+
unknown: 0
|
|
1988
|
+
};
|
|
1989
|
+
for (const e of entries) byProvenance[e.provenance]++;
|
|
1990
|
+
return {
|
|
1991
|
+
total: entries.length,
|
|
1992
|
+
notYours: byProvenance.project + byProvenance.temporary + byProvenance.unknown,
|
|
1993
|
+
byProvenance
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
__name(summarizeSurface, "summarizeSurface");
|
|
1997
|
+
function surfaceLine(e) {
|
|
1998
|
+
const where = e.sourcePath ? ` ${e.sourcePath}` : "";
|
|
1999
|
+
return `${e.glyph} ${e.provenanceLabel.padEnd(9)} ${e.name.padEnd(14)}${where} \u2014 ${e.reachLabel}`;
|
|
2000
|
+
}
|
|
2001
|
+
__name(surfaceLine, "surfaceLine");
|
|
2002
|
+
function surfaceReport(entries, collapseBuiltins = true) {
|
|
2003
|
+
const s = summarizeSurface(entries);
|
|
2004
|
+
const head = `${s.total} tool${s.total === 1 ? "" : "s"} available \xB7 ` + (s.notYours === 0 ? "all supplied by you or pi" : `${s.notYours} not supplied by you`);
|
|
2005
|
+
const shown = collapseBuiltins ? entries.filter((e) => e.provenance !== "builtin") : entries;
|
|
2006
|
+
const lines = shown.map(surfaceLine);
|
|
2007
|
+
if (collapseBuiltins && s.byProvenance.builtin > 0) lines.push(` \u2026 ${s.byProvenance.builtin} builtin`);
|
|
2008
|
+
return [head, ...lines];
|
|
2009
|
+
}
|
|
2010
|
+
__name(surfaceReport, "surfaceReport");
|
|
2011
|
+
|
|
2012
|
+
// node_modules/pi-privacy/src/extension.ts?ext=privateer-privacy
|
|
2013
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2014
|
+
|
|
2015
|
+
// node_modules/pi-privacy/src/surface/ledger.ts?ext=privateer-privacy
|
|
2016
|
+
var UNNAMED_HOST = "(unnamed host)";
|
|
2017
|
+
function createLedger() {
|
|
2018
|
+
return { entries: /* @__PURE__ */ new Map() };
|
|
2019
|
+
}
|
|
2020
|
+
__name(createLedger, "createLedger");
|
|
2021
|
+
function hostOf(assessment) {
|
|
2022
|
+
const target = assessment.target;
|
|
2023
|
+
if (!target) return UNNAMED_HOST;
|
|
2024
|
+
try {
|
|
2025
|
+
const h = new URL(target).hostname;
|
|
2026
|
+
return h || UNNAMED_HOST;
|
|
2027
|
+
} catch {
|
|
2028
|
+
return UNNAMED_HOST;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
__name(hostOf, "hostOf");
|
|
2032
|
+
function recordEgress(led, tool, assessment, opts = {}) {
|
|
2033
|
+
if (!assessment.egress) return;
|
|
2034
|
+
const host = hostOf(assessment);
|
|
2035
|
+
const key = `${tool}\0${host}`;
|
|
2036
|
+
const cur = led.entries.get(key) ?? { tool, host, calls: 0, withPii: 0, blocked: 0 };
|
|
2037
|
+
cur.calls++;
|
|
2038
|
+
if (opts.pii) cur.withPii++;
|
|
2039
|
+
if (opts.blocked) cur.blocked++;
|
|
2040
|
+
led.entries.set(key, cur);
|
|
2041
|
+
}
|
|
2042
|
+
__name(recordEgress, "recordEgress");
|
|
2043
|
+
function ledgerHosts(led) {
|
|
2044
|
+
return [...led.entries.values()].sort(
|
|
2045
|
+
(a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool) || a.host.localeCompare(b.host)
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
__name(ledgerHosts, "ledgerHosts");
|
|
2049
|
+
function observationLine(o) {
|
|
2050
|
+
const notes = [];
|
|
2051
|
+
if (o.withPii) notes.push(`${o.withPii} carried PII/credentials`);
|
|
2052
|
+
if (o.blocked) notes.push(`${o.blocked} blocked`);
|
|
2053
|
+
const suffix = notes.length ? ` (${notes.join(", ")})` : "";
|
|
2054
|
+
return `${o.tool} \u2192 ${o.host} ${o.calls} call${o.calls === 1 ? "" : "s"}${suffix}`;
|
|
2055
|
+
}
|
|
2056
|
+
__name(observationLine, "observationLine");
|
|
2057
|
+
function ledgerReport(led) {
|
|
2058
|
+
return ledgerHosts(led).map(observationLine);
|
|
2059
|
+
}
|
|
2060
|
+
__name(ledgerReport, "ledgerReport");
|
|
2061
|
+
|
|
2062
|
+
// node_modules/pi-privacy/src/ext/results.ts?ext=privateer-privacy
|
|
2063
|
+
function toolResultText(content) {
|
|
2064
|
+
if (content === void 0 || content === null) return "";
|
|
2065
|
+
if (typeof content === "string") return content;
|
|
2066
|
+
if (Array.isArray(content)) {
|
|
2067
|
+
return content.map((p) => typeof p === "string" ? p : typeof p?.text === "string" ? p.text : "").join("\n");
|
|
2068
|
+
}
|
|
2069
|
+
try {
|
|
2070
|
+
return JSON.stringify(content);
|
|
2071
|
+
} catch {
|
|
2072
|
+
return String(content);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
__name(toolResultText, "toolResultText");
|
|
2076
|
+
function redactToolResultContent(content, only = SECRET_TYPES, allow) {
|
|
2077
|
+
if (typeof content === "string") {
|
|
2078
|
+
const out = redactPii(content, only, allow);
|
|
2079
|
+
return out === content ? void 0 : out;
|
|
2080
|
+
}
|
|
2081
|
+
if (Array.isArray(content)) {
|
|
2082
|
+
let changed = false;
|
|
2083
|
+
const out = content.map((p) => {
|
|
2084
|
+
if (typeof p === "string") {
|
|
2085
|
+
const r = redactPii(p, only, allow);
|
|
2086
|
+
if (r !== p) changed = true;
|
|
2087
|
+
return r;
|
|
2088
|
+
}
|
|
2089
|
+
const text = p?.text;
|
|
2090
|
+
if (typeof text === "string") {
|
|
2091
|
+
const r = redactPii(text, only, allow);
|
|
2092
|
+
if (r !== text) changed = true;
|
|
2093
|
+
return { ...p, text: r };
|
|
2094
|
+
}
|
|
2095
|
+
return p;
|
|
2096
|
+
});
|
|
2097
|
+
return changed ? out : void 0;
|
|
2098
|
+
}
|
|
2099
|
+
return void 0;
|
|
2100
|
+
}
|
|
2101
|
+
__name(redactToolResultContent, "redactToolResultContent");
|
|
2102
|
+
|
|
2103
|
+
// node_modules/pi-privacy/src/ext/badge.ts?ext=privateer-privacy
|
|
2104
|
+
var DEFAULT_BADGE_SINKS = ["status", "widget", "title"];
|
|
2105
|
+
function postureBadge(tier) {
|
|
2106
|
+
if (!tier) return "\u22EF checking privacy";
|
|
2107
|
+
const info = TIERS[tier];
|
|
2108
|
+
const glyph = info.posture === "green" ? "\u{1F6E1}" : info.posture === "yellow" ? "\u26A0" : info.posture === "red" ? "\u26D4" : "\u2022";
|
|
2109
|
+
return `${glyph} ${info.label}`;
|
|
2110
|
+
}
|
|
2111
|
+
__name(postureBadge, "postureBadge");
|
|
2112
|
+
function renderBadgeTo(ui, sink, key, badge, tier) {
|
|
2113
|
+
switch (sink) {
|
|
2114
|
+
case "status":
|
|
2115
|
+
if (typeof ui.setStatus === "function") return ui.setStatus(key, badge), true;
|
|
2116
|
+
return false;
|
|
2117
|
+
case "widget":
|
|
2118
|
+
if (typeof ui.setWidget === "function") return ui.setWidget(key, [badge]), true;
|
|
2119
|
+
return false;
|
|
2120
|
+
case "title":
|
|
2121
|
+
if (typeof ui.setTitle === "function") return ui.setTitle(badge), true;
|
|
2122
|
+
return false;
|
|
2123
|
+
case "notify":
|
|
2124
|
+
if (typeof ui.notify === "function")
|
|
2125
|
+
return ui.notify(badge, TIERS[tier ?? "standard"].posture === "green" ? "info" : "warning"), true;
|
|
2126
|
+
return false;
|
|
2127
|
+
default:
|
|
2128
|
+
return false;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
__name(renderBadgeTo, "renderBadgeTo");
|
|
2132
|
+
|
|
2133
|
+
// node_modules/pi-privacy/src/ext/register.ts?ext=privateer-privacy
|
|
2134
|
+
var BUILTIN = /* @__PURE__ */ new Set(["openrouter", "fireworks"]);
|
|
2135
|
+
var SEED_MODELS = {
|
|
2136
|
+
tinfoil: "deepseek-v4-pro",
|
|
2137
|
+
nearai: "zai-org/GLM-5.1-FP8",
|
|
2138
|
+
venice: "qwen3-coder-480b-a35b-instruct-turbo",
|
|
2139
|
+
ollama: "llama3.1",
|
|
2140
|
+
privateer: "near/zai-org/GLM-5.1-FP8"
|
|
2141
|
+
};
|
|
2142
|
+
function registerable(p) {
|
|
2143
|
+
return !!p.baseUrl && !BUILTIN.has(p.id) && p.id !== "custom";
|
|
2144
|
+
}
|
|
2145
|
+
__name(registerable, "registerable");
|
|
2146
|
+
function providerConfig(p) {
|
|
2147
|
+
const seed = SEED_MODELS[p.id];
|
|
2148
|
+
const models = seed ? [
|
|
2149
|
+
{
|
|
2150
|
+
id: seed,
|
|
2151
|
+
name: seed,
|
|
2152
|
+
reasoning: false,
|
|
2153
|
+
input: ["text"],
|
|
2154
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2155
|
+
contextWindow: 128e3,
|
|
2156
|
+
maxTokens: 4096
|
|
2157
|
+
}
|
|
2158
|
+
] : [];
|
|
2159
|
+
const cfg = { name: p.label, baseUrl: p.baseUrl, api: p.api, models };
|
|
2160
|
+
if (p.keyEnv) {
|
|
2161
|
+
cfg.apiKey = p.keyEnv;
|
|
2162
|
+
cfg.authHeader = true;
|
|
2163
|
+
} else if (p.local && models.length) {
|
|
2164
|
+
cfg.apiKey = "local";
|
|
2165
|
+
}
|
|
2166
|
+
return cfg;
|
|
2167
|
+
}
|
|
2168
|
+
__name(providerConfig, "providerConfig");
|
|
2169
|
+
function nearApiKey(env = process.env) {
|
|
2170
|
+
return env.NEARAI_API_KEY ?? env.NEAR_AI_API_KEY;
|
|
2171
|
+
}
|
|
2172
|
+
__name(nearApiKey, "nearApiKey");
|
|
2173
|
+
|
|
2174
|
+
// node_modules/pi-privacy/src/ext/payload.ts?ext=privateer-privacy
|
|
2175
|
+
function payloadText(payload) {
|
|
2176
|
+
try {
|
|
2177
|
+
return JSON.stringify(payload?.messages ?? payload ?? "");
|
|
2178
|
+
} catch {
|
|
2179
|
+
return "";
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
__name(payloadText, "payloadText");
|
|
2183
|
+
function redactPayloadPii(payload, allow) {
|
|
2184
|
+
if (!payload || typeof payload !== "object" || !Array.isArray(payload.messages)) return payload;
|
|
2185
|
+
const mask = /* @__PURE__ */ __name((s) => redactPii(s, void 0, allow), "mask");
|
|
2186
|
+
const messages = payload.messages.map((m) => {
|
|
2187
|
+
if (typeof m?.content === "string") return { ...m, content: mask(m.content) };
|
|
2188
|
+
if (Array.isArray(m?.content)) {
|
|
2189
|
+
return {
|
|
2190
|
+
...m,
|
|
2191
|
+
content: m.content.map((p) => typeof p?.text === "string" ? { ...p, text: mask(p.text) } : p)
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
return m;
|
|
2195
|
+
});
|
|
2196
|
+
return { ...payload, messages };
|
|
2197
|
+
}
|
|
2198
|
+
__name(redactPayloadPii, "redactPayloadPii");
|
|
2199
|
+
|
|
2200
|
+
// node_modules/pi-privacy/src/extension.ts?ext=privateer-privacy
|
|
2201
|
+
function isVerifiedPrivate(tier) {
|
|
2202
|
+
return tier === "tee-verified" || tier === "local";
|
|
2203
|
+
}
|
|
2204
|
+
__name(isVerifiedPrivate, "isVerifiedPrivate");
|
|
2205
|
+
var PII_WORD_HELP = {
|
|
2206
|
+
off: "bypass the PII gate for this session (credentials still gated)",
|
|
2207
|
+
warn: "ask before PII goes down an unverified channel",
|
|
2208
|
+
redact: "mask PII before send, without asking",
|
|
2209
|
+
on: "restore the configured policy",
|
|
2210
|
+
status: "what is in force right now"
|
|
2211
|
+
};
|
|
2212
|
+
function makePiPrivacyExtension(opts = {}) {
|
|
2213
|
+
const {
|
|
2214
|
+
installDispatcher = true,
|
|
2215
|
+
registerProviders = true,
|
|
2216
|
+
enforceOpenRouterZdr = false,
|
|
2217
|
+
onPosture,
|
|
2218
|
+
useDispatcherTransport = true,
|
|
2219
|
+
piiPolicy = "warn",
|
|
2220
|
+
piiUnattended = false,
|
|
2221
|
+
renderPiiAutoRedact: renderPiiAutoRedact2,
|
|
2222
|
+
piiAllow = [],
|
|
2223
|
+
piiAllowDefaults = true,
|
|
2224
|
+
showBadge = true,
|
|
2225
|
+
badgeSinks = DEFAULT_BADGE_SINKS,
|
|
2226
|
+
badgeKey = "pi-privacy",
|
|
2227
|
+
renderBadge,
|
|
2228
|
+
toolExfilPolicy = "warn",
|
|
2229
|
+
toolResultPolicy = "warn",
|
|
2230
|
+
downgradePolicy = "warn",
|
|
2231
|
+
modelPicker = true,
|
|
2232
|
+
modelPickerCommand = "models",
|
|
2233
|
+
toolSurfacePolicy = "warn",
|
|
2234
|
+
toolSurfaceCommand = "surface",
|
|
2235
|
+
piiCommand = true,
|
|
2236
|
+
piiCommandName = "pii",
|
|
2237
|
+
resolveTier,
|
|
2238
|
+
privateerVerifiedTee = false
|
|
2239
|
+
} = opts;
|
|
2240
|
+
return /* @__PURE__ */ __name(function piPrivacy(pi) {
|
|
2241
|
+
if (installDispatcher) installAttestationDispatcher();
|
|
2242
|
+
if (registerProviders && typeof pi.registerProvider === "function") {
|
|
2243
|
+
for (const p of PRIVACY_PROVIDERS) {
|
|
2244
|
+
if (registerable(p)) pi.registerProvider(p.id, providerConfig(p));
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
let currentProviderId;
|
|
2248
|
+
let currentModelId;
|
|
2249
|
+
let currentTier;
|
|
2250
|
+
let piiChoice = "ask";
|
|
2251
|
+
let piiSession;
|
|
2252
|
+
const piiMode = /* @__PURE__ */ __name(() => piiSession ?? piiPolicy, "piiMode");
|
|
2253
|
+
const piiBypassed = /* @__PURE__ */ __name(() => piiSession === "off", "piiBypassed");
|
|
2254
|
+
const piiUnattendedNow = /* @__PURE__ */ __name(() => typeof piiUnattended === "function" ? !!piiUnattended() : !!piiUnattended, "piiUnattendedNow");
|
|
2255
|
+
let piiSeen = /* @__PURE__ */ new Map();
|
|
2256
|
+
let piiLastAction;
|
|
2257
|
+
const piiAllowEntries2 = /* @__PURE__ */ __name(() => (typeof piiAllow === "function" ? piiAllow() : piiAllow) ?? [], "piiAllowEntries");
|
|
2258
|
+
let allowKey;
|
|
2259
|
+
let allowMatcher;
|
|
2260
|
+
const piiAllowed = /* @__PURE__ */ __name((type, value) => {
|
|
2261
|
+
const entries = piiAllowEntries2();
|
|
2262
|
+
const key = entries.join("\0");
|
|
2263
|
+
if (key !== allowKey || !allowMatcher) {
|
|
2264
|
+
allowKey = key;
|
|
2265
|
+
allowMatcher = compileAllow(entries, {
|
|
2266
|
+
defaults: piiAllowDefaults,
|
|
2267
|
+
warn: /* @__PURE__ */ __name((m) => console.warn(`[pi-privacy] ${m}`), "warn")
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
return allowMatcher(type, value);
|
|
2271
|
+
}, "piiAllowed");
|
|
2272
|
+
const scanOpts = { allow: piiAllowed };
|
|
2273
|
+
let toolAllow = false;
|
|
2274
|
+
let resultChoice = "ask";
|
|
2275
|
+
let toolSnapshot = [];
|
|
2276
|
+
let surfaceByName = /* @__PURE__ */ new Map();
|
|
2277
|
+
const ledger = createLedger();
|
|
2278
|
+
const surfaceOn = toolSurfacePolicy !== "off";
|
|
2279
|
+
const captureTools = /* @__PURE__ */ __name((ctx) => {
|
|
2280
|
+
if (!surfaceOn || typeof ctx?.getAllTools !== "function") return;
|
|
2281
|
+
try {
|
|
2282
|
+
const tools = ctx.getAllTools();
|
|
2283
|
+
if (!Array.isArray(tools)) return;
|
|
2284
|
+
toolSnapshot = tools;
|
|
2285
|
+
surfaceByName = new Map(rankSurface(tools).map((e) => [e.name, e]));
|
|
2286
|
+
} catch {
|
|
2287
|
+
}
|
|
2288
|
+
}, "captureTools");
|
|
2289
|
+
const provenanceSeen = /* @__PURE__ */ new Set();
|
|
2290
|
+
let projectToolsAllowed = false;
|
|
2291
|
+
const previewSource = /* @__PURE__ */ __name((entry) => {
|
|
2292
|
+
const path = entry.sourcePath;
|
|
2293
|
+
if (!path) return "No source path recorded for this tool \u2014 nothing to show.";
|
|
2294
|
+
try {
|
|
2295
|
+
const text = readFileSync5(path, "utf8");
|
|
2296
|
+
const clipped = text.length > 4e3 ? `${text.slice(0, 4e3)}
|
|
2297
|
+
\u2026 (truncated)` : text;
|
|
2298
|
+
return `${path}:
|
|
2299
|
+
${clipped}`;
|
|
2300
|
+
} catch (e) {
|
|
2301
|
+
return `Could not read ${path}: ${e.message}`;
|
|
2302
|
+
}
|
|
2303
|
+
}, "previewSource");
|
|
2304
|
+
const guardProvenance = /* @__PURE__ */ __name(async (toolName, ctx) => {
|
|
2305
|
+
if (toolSurfacePolicy !== "warn" || projectToolsAllowed) return "allow";
|
|
2306
|
+
const entry = surfaceByName.get(toolName);
|
|
2307
|
+
if (!entry || !isRepoSupplied(entry.provenance)) return "allow";
|
|
2308
|
+
if (provenanceSeen.has(toolName)) return "allow";
|
|
2309
|
+
const warning = `\u26A0 \`${toolName}\` was ${entry.concern}. It is about to run for the first time this session. This says where it came FROM, not that it is unsafe.`;
|
|
2310
|
+
if (!canPrompt(ctx)) {
|
|
2311
|
+
provenanceSeen.add(toolName);
|
|
2312
|
+
notify(ctx, warning, "warning");
|
|
2313
|
+
return "allow";
|
|
2314
|
+
}
|
|
2315
|
+
for (let i = 0; i < 3; i++) {
|
|
2316
|
+
const choice = await ask(ctx, warning, [
|
|
2317
|
+
"Run it",
|
|
2318
|
+
"Show me the file",
|
|
2319
|
+
"Allow project tools for this session",
|
|
2320
|
+
"Block"
|
|
2321
|
+
]);
|
|
2322
|
+
if (choice === "Show me the file") {
|
|
2323
|
+
notify(ctx, previewSource(entry), "info");
|
|
2324
|
+
continue;
|
|
2325
|
+
}
|
|
2326
|
+
if (choice === "Allow project tools for this session") {
|
|
2327
|
+
projectToolsAllowed = true;
|
|
2328
|
+
provenanceSeen.add(toolName);
|
|
2329
|
+
return "allow";
|
|
2330
|
+
}
|
|
2331
|
+
if (choice === "Run it") {
|
|
2332
|
+
provenanceSeen.add(toolName);
|
|
2333
|
+
return "allow";
|
|
2334
|
+
}
|
|
2335
|
+
return "block";
|
|
2336
|
+
}
|
|
2337
|
+
return "block";
|
|
2338
|
+
}, "guardProvenance");
|
|
2339
|
+
let previousTier;
|
|
2340
|
+
let previousModel;
|
|
2341
|
+
let downgradeHandled = true;
|
|
2342
|
+
let contextHits = [];
|
|
2343
|
+
let lastUi;
|
|
2344
|
+
let lastCtx;
|
|
2345
|
+
let lastBadge;
|
|
2346
|
+
let lastHasUI = false;
|
|
2347
|
+
const captureUi = /* @__PURE__ */ __name((ctx) => {
|
|
2348
|
+
if (ctx?.ui) lastUi = ctx.ui, lastCtx = ctx, lastHasUI = !!ctx.hasUI;
|
|
2349
|
+
}, "captureUi");
|
|
2350
|
+
const uiOf = /* @__PURE__ */ __name((ctx) => ctx?.ui ?? lastUi, "uiOf");
|
|
2351
|
+
const canPrompt = /* @__PURE__ */ __name((ctx) => (ctx?.hasUI ?? lastHasUI) && typeof uiOf(ctx)?.select === "function", "canPrompt");
|
|
2352
|
+
const ask = /* @__PURE__ */ __name((ctx, title, options) => canPrompt(ctx) ? uiOf(ctx).select(title, options) : Promise.resolve(void 0), "ask");
|
|
2353
|
+
const notify = /* @__PURE__ */ __name((ctx, message, level) => uiOf(ctx)?.notify?.(message, level), "notify");
|
|
2354
|
+
const postureOpts = /* @__PURE__ */ __name(() => ({
|
|
2355
|
+
apiKey: currentProviderId === "nearai" ? nearApiKey() : void 0,
|
|
2356
|
+
zdrEnforced: currentProviderId === "openrouter" && enforceOpenRouterZdr,
|
|
2357
|
+
transport: useDispatcherTransport && installDispatcher ? dispatcherTransport : void 0
|
|
2358
|
+
}), "postureOpts");
|
|
2359
|
+
const paintBadge = /* @__PURE__ */ __name(() => {
|
|
2360
|
+
if (!showBadge || !lastUi) return;
|
|
2361
|
+
const badge = postureBadge(currentTier);
|
|
2362
|
+
if (badge === lastBadge) return;
|
|
2363
|
+
let rendered = false;
|
|
2364
|
+
if (renderBadge) renderBadge(badge, currentTier, lastCtx), rendered = true;
|
|
2365
|
+
else {
|
|
2366
|
+
for (const sink of badgeSinks) {
|
|
2367
|
+
if (renderBadgeTo(lastUi, sink, badgeKey, badge, currentTier)) {
|
|
2368
|
+
rendered = true;
|
|
2369
|
+
break;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
if (rendered) lastBadge = badge;
|
|
2374
|
+
}, "paintBadge");
|
|
2375
|
+
const refreshPosture = /* @__PURE__ */ __name(async () => {
|
|
2376
|
+
if (!currentProviderId || !currentModelId) return;
|
|
2377
|
+
if (resolveTier) {
|
|
2378
|
+
const t = await resolveTier(currentProviderId, currentModelId);
|
|
2379
|
+
if (t !== void 0) {
|
|
2380
|
+
currentTier = t;
|
|
2381
|
+
onPosture?.({ providerId: currentProviderId, modelId: currentModelId, tier: t });
|
|
2382
|
+
paintBadge();
|
|
2383
|
+
void guardDowngrade(t);
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
const result = await verifyModelPosture(currentProviderId, currentModelId, postureOpts());
|
|
2388
|
+
currentTier = result.tier;
|
|
2389
|
+
onPosture?.(result);
|
|
2390
|
+
paintBadge();
|
|
2391
|
+
void guardDowngrade(result.tier);
|
|
2392
|
+
}, "refreshPosture");
|
|
2393
|
+
const guardDowngrade = /* @__PURE__ */ __name(async (toTier, ctx) => {
|
|
2394
|
+
if (downgradePolicy === "off" || downgradeHandled) return;
|
|
2395
|
+
const a = assessDowngrade(previousTier, toTier, contextHits);
|
|
2396
|
+
if (!a.downgrade || a.severity === "none") return;
|
|
2397
|
+
if (piiBypassed() && a.severity !== "secret") return;
|
|
2398
|
+
downgradeHandled = true;
|
|
2399
|
+
const label = currentProviderId ? `${currentProviderId}${currentModelId ? `/${currentModelId}` : ""}` : void 0;
|
|
2400
|
+
const warning = downgradeWarning(a, contextHits, label);
|
|
2401
|
+
const revert = /* @__PURE__ */ __name(async () => {
|
|
2402
|
+
if (previousModel === void 0 || typeof pi.setModel !== "function") {
|
|
2403
|
+
notify(ctx, `${warning} Could not revert the switch automatically.`, "warning");
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
await pi.setModel(previousModel);
|
|
2407
|
+
notify(ctx, `Reverted to ${TIERS[a.from].label} \u2014 session context stays put.`, "info");
|
|
2408
|
+
}, "revert");
|
|
2409
|
+
if (downgradePolicy === "block") return revert();
|
|
2410
|
+
if (canPrompt(ctx)) {
|
|
2411
|
+
const choice = await ask(ctx, warning, [
|
|
2412
|
+
"Stay on the previous model",
|
|
2413
|
+
"Switch anyway",
|
|
2414
|
+
"Switch, redacting PII from now on"
|
|
2415
|
+
]);
|
|
2416
|
+
if (choice === "Switch anyway") return;
|
|
2417
|
+
if (choice === "Switch, redacting PII from now on") {
|
|
2418
|
+
piiChoice = "redact";
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
return revert();
|
|
2422
|
+
}
|
|
2423
|
+
if (a.severity === "secret") return revert();
|
|
2424
|
+
notify(ctx, warning, "warning");
|
|
2425
|
+
}, "guardDowngrade");
|
|
2426
|
+
pi.on("session_start", (_event, ctx) => {
|
|
2427
|
+
captureUi(ctx);
|
|
2428
|
+
captureTools(ctx);
|
|
2429
|
+
});
|
|
2430
|
+
pi.on("model_select", (event, ctx) => {
|
|
2431
|
+
const model = event?.model;
|
|
2432
|
+
previousTier = currentTier;
|
|
2433
|
+
previousModel = event?.previousModel;
|
|
2434
|
+
downgradeHandled = false;
|
|
2435
|
+
if (model?.provider !== currentProviderId) {
|
|
2436
|
+
piiSeen = /* @__PURE__ */ new Map();
|
|
2437
|
+
piiLastAction = void 0;
|
|
2438
|
+
}
|
|
2439
|
+
currentProviderId = model?.provider;
|
|
2440
|
+
currentModelId = model?.id;
|
|
2441
|
+
captureUi(ctx);
|
|
2442
|
+
currentTier = void 0;
|
|
2443
|
+
paintBadge();
|
|
2444
|
+
void guardDowngrade(
|
|
2445
|
+
effectiveTier(currentProviderId ?? "", {
|
|
2446
|
+
zdrEnforced: currentProviderId === "openrouter" && enforceOpenRouterZdr
|
|
2447
|
+
}),
|
|
2448
|
+
ctx
|
|
2449
|
+
);
|
|
2450
|
+
void refreshPosture();
|
|
2451
|
+
});
|
|
2452
|
+
pi.on("before_provider_request", async (event, ctx) => {
|
|
2453
|
+
captureUi(ctx);
|
|
2454
|
+
paintBadge();
|
|
2455
|
+
let payload = event?.payload;
|
|
2456
|
+
if (currentProviderId === "venice") payload = veniceRequestPatch(payload);
|
|
2457
|
+
else if (currentProviderId === "openrouter" && enforceOpenRouterZdr) payload = openRouterZdrPatch(payload);
|
|
2458
|
+
const scan = scanPii(payloadText(payload), scanOpts);
|
|
2459
|
+
const hits = scan.hits;
|
|
2460
|
+
contextHits = hits;
|
|
2461
|
+
if (!piiBypassed() && !isVerifiedPrivate(currentTier)) {
|
|
2462
|
+
if (hits.length > 0) {
|
|
2463
|
+
let action = piiChoice !== "ask" ? piiChoice : piiMode() === "redact" ? "redact" : "send";
|
|
2464
|
+
const fresh = newPii(hits, piiSeen);
|
|
2465
|
+
const decided = piiChoice !== "ask" ? piiChoice : piiLastAction;
|
|
2466
|
+
if (fresh.length === 0 && decided) action = decided;
|
|
2467
|
+
if (fresh.length > 0 && piiChoice === "ask" && piiMode() === "warn" && piiUnattendedNow()) {
|
|
2468
|
+
action = "redact";
|
|
2469
|
+
const notice = `\u2691 unattended \u2014 PII auto-redacted before send: ${piiInline(scan)} \u2192 ${TIERS[currentTier ?? "standard"].label} channel. Best-effort structured detection only, not a guarantee.`;
|
|
2470
|
+
const rendered = renderPiiAutoRedact2?.(notice);
|
|
2471
|
+
notify(ctx, rendered ?? notice, rendered ? "info" : "warning");
|
|
2472
|
+
} else if (fresh.length > 0 && piiChoice === "ask" && piiMode() === "warn" && canPrompt(ctx)) {
|
|
2473
|
+
const tierLabel = TIERS[currentTier ?? "standard"].label;
|
|
2474
|
+
const kind = hasSecrets(hits) ? "secrets/PII" : "structured PII";
|
|
2475
|
+
const again = piiLastAction ? ` (${summarizePii(fresh)} new since your last answer)` : "";
|
|
2476
|
+
const head = `\u26A0 ${summarizePii(hits)} detected${again} \u2014 sending to an unverified channel (${tierLabel}). Best-effort ${kind} detection only, not a guarantee.`;
|
|
2477
|
+
let title = head;
|
|
2478
|
+
for (let round = 0; round < 2; round++) {
|
|
2479
|
+
const options = [
|
|
2480
|
+
"Send as-is",
|
|
2481
|
+
"Redact PII",
|
|
2482
|
+
...round === 0 ? ["Show what was detected"] : [],
|
|
2483
|
+
"Redact + remember for session",
|
|
2484
|
+
"Send + remember for session"
|
|
2485
|
+
];
|
|
2486
|
+
const choice = await ask(ctx, title, options);
|
|
2487
|
+
if (choice === "Show what was detected") {
|
|
2488
|
+
title = `${head}
|
|
2489
|
+
|
|
2490
|
+
${piiDetail(scan)}`;
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
if (choice === "Redact PII") action = "redact";
|
|
2494
|
+
else if (choice === "Redact + remember for session") action = "redact", piiChoice = "redact";
|
|
2495
|
+
else if (choice === "Send + remember for session") action = "send", piiChoice = "send";
|
|
2496
|
+
else action = "send";
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
piiSeen = mergePiiBaseline(piiSeen, hits);
|
|
2501
|
+
piiLastAction = action;
|
|
2502
|
+
if (action === "redact") payload = redactPayloadPii(payload, piiAllowed);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return payload === event?.payload ? void 0 : payload;
|
|
2506
|
+
});
|
|
2507
|
+
const egressVerdict = /* @__PURE__ */ __name((assessment, label, input) => {
|
|
2508
|
+
if (!assessment.egress) return void 0;
|
|
2509
|
+
const hits = detectPii(payloadText(input), scanOpts);
|
|
2510
|
+
const files = assessment.sensitiveFiles ?? [];
|
|
2511
|
+
if (hits.length === 0 && files.length === 0) return void 0;
|
|
2512
|
+
const secret = hasSecrets(hits) || files.length > 0;
|
|
2513
|
+
const what = [files.length ? `${files.join(", ")} contents` : "", hits.length ? summarizePii(hits) : ""].filter(Boolean).join(" + ");
|
|
2514
|
+
const dest = assessment.target ? ` \u2192 ${assessment.target}` : "";
|
|
2515
|
+
const reason = `pi-privacy blocked ${secret ? "credential" : "PII"} exfiltration via ${label}`;
|
|
2516
|
+
const warning = `\u26A0 ${label} is about to send ${what} off this machine${dest}. A private (TEE/ZDR) model does NOT protect a tool call. Best-effort detection, not a guarantee.`;
|
|
2517
|
+
return { warning, reason, secret };
|
|
2518
|
+
}, "egressVerdict");
|
|
2519
|
+
const egressGated = /* @__PURE__ */ __name((v) => v.secret || !piiBypassed(), "egressGated");
|
|
2520
|
+
const decideEgress = /* @__PURE__ */ __name(async (v, ctx) => {
|
|
2521
|
+
if (toolAllow) {
|
|
2522
|
+
notify(ctx, v.warning, "warning");
|
|
2523
|
+
return "allow";
|
|
2524
|
+
}
|
|
2525
|
+
if (toolExfilPolicy === "block") return "block";
|
|
2526
|
+
if (canPrompt(ctx)) {
|
|
2527
|
+
const choice = await ask(ctx, v.warning, ["Block", "Allow once", "Allow for session"]);
|
|
2528
|
+
if (choice === "Allow for session") {
|
|
2529
|
+
toolAllow = true;
|
|
2530
|
+
return "allow";
|
|
2531
|
+
}
|
|
2532
|
+
if (choice === "Allow once") return "allow";
|
|
2533
|
+
return "block";
|
|
2534
|
+
}
|
|
2535
|
+
if (v.secret) return "block";
|
|
2536
|
+
notify(ctx, v.warning, "warning");
|
|
2537
|
+
return "allow";
|
|
2538
|
+
}, "decideEgress");
|
|
2539
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
2540
|
+
if (toolExfilPolicy === "off" && !surfaceOn) return;
|
|
2541
|
+
captureUi(ctx);
|
|
2542
|
+
const name = event?.toolName ?? "a tool";
|
|
2543
|
+
if (await guardProvenance(name, ctx) === "block")
|
|
2544
|
+
return {
|
|
2545
|
+
block: true,
|
|
2546
|
+
reason: `pi-privacy blocked ${name}: supplied by this project, not by you`
|
|
2547
|
+
};
|
|
2548
|
+
const assessment = assessToolCall(event?.toolName, event?.input);
|
|
2549
|
+
const v = egressVerdict(assessment, name, event?.input);
|
|
2550
|
+
let blocked = false;
|
|
2551
|
+
if (toolExfilPolicy !== "off" && v && egressGated(v)) blocked = await decideEgress(v, ctx) === "block";
|
|
2552
|
+
if (surfaceOn) recordEgress(ledger, name, assessment, { pii: !!v, blocked });
|
|
2553
|
+
if (blocked) return { block: true, reason: v.reason };
|
|
2554
|
+
return;
|
|
2555
|
+
});
|
|
2556
|
+
pi.on("user_bash", async (event, ctx) => {
|
|
2557
|
+
if (toolExfilPolicy === "off" && !surfaceOn) return;
|
|
2558
|
+
captureUi(ctx);
|
|
2559
|
+
const command = typeof event?.command === "string" ? event.command : "";
|
|
2560
|
+
if (!command) return;
|
|
2561
|
+
const assessment = assessToolCall("bash", { command });
|
|
2562
|
+
const v = egressVerdict(assessment, "this ! command", { command });
|
|
2563
|
+
let blocked = false;
|
|
2564
|
+
if (toolExfilPolicy !== "off" && v && egressGated(v)) blocked = await decideEgress(v, ctx) === "block";
|
|
2565
|
+
if (surfaceOn) recordEgress(ledger, "! command", assessment, { pii: !!v, blocked });
|
|
2566
|
+
if (blocked) {
|
|
2567
|
+
return {
|
|
2568
|
+
result: {
|
|
2569
|
+
output: `${v.reason}. Set PI_PRIVACY_TOOL_EXFIL_POLICY=off (or allow it when prompted) if this is intended.`,
|
|
2570
|
+
exitCode: 1,
|
|
2571
|
+
cancelled: false,
|
|
2572
|
+
truncated: false
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
return;
|
|
2577
|
+
});
|
|
2578
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
2579
|
+
if (toolResultPolicy === "off") return;
|
|
2580
|
+
captureUi(ctx);
|
|
2581
|
+
const content = event?.content;
|
|
2582
|
+
const hits = secretHits(detectPii(toolResultText(content), scanOpts));
|
|
2583
|
+
if (hits.length === 0) return;
|
|
2584
|
+
const summary = summarizePii(hits);
|
|
2585
|
+
const warning = `\u26A0 ${event?.toolName ?? "a tool"} returned ${summary}. Keeping ${hits.length === 1 && hits[0].count === 1 ? "it" : "them"} in context means re-sending to the provider on every later turn, and writing to the session file on disk in plaintext. Best-effort secret detection, not a guarantee.`;
|
|
2586
|
+
let action = resultChoice !== "ask" ? resultChoice : toolResultPolicy === "redact" ? "redact" : "keep";
|
|
2587
|
+
if (resultChoice === "ask" && toolResultPolicy === "warn") {
|
|
2588
|
+
if (canPrompt(ctx)) {
|
|
2589
|
+
const choice = await ask(ctx, warning, [
|
|
2590
|
+
"Redact the credentials",
|
|
2591
|
+
"Keep them in context",
|
|
2592
|
+
"Redact for the rest of the session",
|
|
2593
|
+
"Keep for the rest of the session"
|
|
2594
|
+
]);
|
|
2595
|
+
if (choice === "Redact the credentials") action = "redact";
|
|
2596
|
+
else if (choice === "Redact for the rest of the session") action = "redact", resultChoice = "redact";
|
|
2597
|
+
else if (choice === "Keep for the rest of the session") action = "keep", resultChoice = "keep";
|
|
2598
|
+
else action = "keep";
|
|
2599
|
+
} else {
|
|
2600
|
+
action = "redact";
|
|
2601
|
+
notify(ctx, `${warning} Redacted (no UI to ask).`, "warning");
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
if (action !== "redact") return;
|
|
2605
|
+
const redacted = redactToolResultContent(content, void 0, piiAllowed);
|
|
2606
|
+
if (redacted === void 0) {
|
|
2607
|
+
notify(
|
|
2608
|
+
ctx,
|
|
2609
|
+
`Could not redact ${summary} from ${event?.toolName ?? "the tool"} result \u2014 unrecognized result shape. It stays in context.`,
|
|
2610
|
+
"warning"
|
|
2611
|
+
);
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
return { content: redacted };
|
|
2615
|
+
});
|
|
2616
|
+
const surfaceSection = /* @__PURE__ */ __name((ctx, full) => {
|
|
2617
|
+
captureTools(ctx);
|
|
2618
|
+
const lines = [];
|
|
2619
|
+
const entries = rankSurface(toolSnapshot);
|
|
2620
|
+
if (entries.length === 0) {
|
|
2621
|
+
if (full) lines.push("Tools inventory unavailable \u2014 this host did not expose the tool list.");
|
|
2622
|
+
} else {
|
|
2623
|
+
const report = surfaceReport(entries, !full);
|
|
2624
|
+
lines.push(`Tools ${report[0]}`);
|
|
2625
|
+
for (const l of report.slice(1)) lines.push(` ${l}`);
|
|
2626
|
+
if (summarizeSurface(entries).notYours > 0)
|
|
2627
|
+
lines.push(
|
|
2628
|
+
" \u26A0 marks a tool that came with this working directory or a CLI flag, not from your configuration. That is where it came FROM \u2014 pi-privacy does not judge what it does."
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
const obs = ledgerReport(ledger);
|
|
2632
|
+
if (obs.length) {
|
|
2633
|
+
lines.push(`Observed ${obs[0]}`);
|
|
2634
|
+
for (const l of obs.slice(1)) lines.push(` ${l}`);
|
|
2635
|
+
lines.push(
|
|
2636
|
+
" (observed via tool calls only \u2014 an extension's own fetch() never appears here, so this is a floor)"
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
return lines.join("\n");
|
|
2640
|
+
}, "surfaceSection");
|
|
2641
|
+
if (typeof pi.registerCommand === "function") {
|
|
2642
|
+
pi.registerCommand("verify", {
|
|
2643
|
+
description: "Verify the current model's privacy posture (TEE attestation)",
|
|
2644
|
+
handler: /* @__PURE__ */ __name(async (_args, ctx) => {
|
|
2645
|
+
if (!currentProviderId || !currentModelId) {
|
|
2646
|
+
ctx.ui?.notify?.("No model selected.", "warning");
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
const res = await verifyModelPosture(currentProviderId, currentModelId, postureOpts());
|
|
2650
|
+
const info = TIERS[res.tier];
|
|
2651
|
+
const detail = res.teePosture ? ` [${res.teePosture}]` : "";
|
|
2652
|
+
const err = res.error ? ` \u2014 ${res.error}` : "";
|
|
2653
|
+
ctx.ui?.notify?.(`${info.label}${detail}: ${info.blurb}${err}`, "info");
|
|
2654
|
+
if (res.attestation !== void 0) {
|
|
2655
|
+
let report;
|
|
2656
|
+
try {
|
|
2657
|
+
report = JSON.stringify(res.attestation, null, 2);
|
|
2658
|
+
} catch {
|
|
2659
|
+
report = String(res.attestation);
|
|
2660
|
+
}
|
|
2661
|
+
ctx.ui?.notify?.(`attestation report (verify independently):
|
|
2662
|
+
${report}`, "info");
|
|
2663
|
+
}
|
|
2664
|
+
if (surfaceOn) {
|
|
2665
|
+
const section = surfaceSection(ctx, false);
|
|
2666
|
+
if (section) ctx.ui?.notify?.(section, "info");
|
|
2667
|
+
}
|
|
2668
|
+
}, "handler")
|
|
2669
|
+
});
|
|
2670
|
+
if (piiCommand) {
|
|
2671
|
+
pi.registerCommand(piiCommandName, {
|
|
2672
|
+
description: "Set or bypass the PII gate for this session (off | warn | redact | on | status)",
|
|
2673
|
+
handler: /* @__PURE__ */ __name((args, ctx) => {
|
|
2674
|
+
const out = runPiiCommand(args, {
|
|
2675
|
+
configured: piiPolicy,
|
|
2676
|
+
session: piiSession,
|
|
2677
|
+
toolExfilPolicy,
|
|
2678
|
+
toolResultPolicy,
|
|
2679
|
+
downgradePolicy,
|
|
2680
|
+
commandName: piiCommandName
|
|
2681
|
+
});
|
|
2682
|
+
piiSession = out.session;
|
|
2683
|
+
if (out.changed) {
|
|
2684
|
+
piiChoice = "ask";
|
|
2685
|
+
piiSeen = /* @__PURE__ */ new Map();
|
|
2686
|
+
piiLastAction = void 0;
|
|
2687
|
+
}
|
|
2688
|
+
ctx.ui?.notify?.(out.message, out.level);
|
|
2689
|
+
}, "handler"),
|
|
2690
|
+
// Hint the words rather than making the user find them in a README.
|
|
2691
|
+
getArgumentCompletions: /* @__PURE__ */ __name((prefix) => {
|
|
2692
|
+
const p = prefix.trim().toLowerCase();
|
|
2693
|
+
return PII_COMMAND_WORDS.filter((w) => w.startsWith(p)).map((w) => ({
|
|
2694
|
+
value: w,
|
|
2695
|
+
label: w,
|
|
2696
|
+
description: PII_WORD_HELP[w]
|
|
2697
|
+
}));
|
|
2698
|
+
}, "getArgumentCompletions")
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
if (surfaceOn) {
|
|
2702
|
+
pi.registerCommand(toolSurfaceCommand, {
|
|
2703
|
+
description: "List the session's tools by who supplied them, plus observed egress",
|
|
2704
|
+
handler: /* @__PURE__ */ __name((_args, ctx) => {
|
|
2705
|
+
ctx.ui?.notify?.(surfaceSection(ctx, true), "info");
|
|
2706
|
+
}, "handler")
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
if (modelPicker) {
|
|
2710
|
+
pi.registerCommand(modelPickerCommand, {
|
|
2711
|
+
description: "Pick a model ranked by privacy (verified TEE / on-device / ZDR first)",
|
|
2712
|
+
handler: /* @__PURE__ */ __name(async (_args, ctx) => {
|
|
2713
|
+
const reg = ctx.modelRegistry;
|
|
2714
|
+
const models = reg?.getAvailable?.() ?? reg?.getAll?.() ?? [];
|
|
2715
|
+
if (models.length === 0) {
|
|
2716
|
+
ctx.ui?.notify?.(
|
|
2717
|
+
"No models to rank (none with configured auth were found).",
|
|
2718
|
+
"warning"
|
|
2719
|
+
);
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
const ranked = rankModels(models, {
|
|
2723
|
+
zdrEnforced: enforceOpenRouterZdr,
|
|
2724
|
+
verifiedTee: privateerVerifiedTee
|
|
2725
|
+
});
|
|
2726
|
+
const cur = ctx.getModel?.() ?? { provider: currentProviderId, id: currentModelId };
|
|
2727
|
+
const isCurrent = /* @__PURE__ */ __name((m) => m.provider === cur.provider && m.id === cur.id, "isCurrent");
|
|
2728
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
2729
|
+
const options = ranked.map((e) => {
|
|
2730
|
+
const label = pickerOptionLabel(e, isCurrent(e.model));
|
|
2731
|
+
byLabel.set(label, e.model);
|
|
2732
|
+
return label;
|
|
2733
|
+
});
|
|
2734
|
+
if (!ctx.hasUI || typeof ctx.ui?.select !== "function") {
|
|
2735
|
+
ctx.ui?.notify?.(
|
|
2736
|
+
`Models by privacy (strongest first):
|
|
2737
|
+
${options.join("\n")}
|
|
2738
|
+
\u25C6 = TEE that verifies when selected. Switch with /model.`,
|
|
2739
|
+
"info"
|
|
2740
|
+
);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
const choice = await ctx.ui.select(
|
|
2744
|
+
"Pick a model (strongest privacy first \u2014 \u25C6 verifies on select):",
|
|
2745
|
+
options
|
|
2746
|
+
);
|
|
2747
|
+
if (!choice) return;
|
|
2748
|
+
const model = byLabel.get(choice);
|
|
2749
|
+
if (!model) return;
|
|
2750
|
+
if (isCurrent(model)) {
|
|
2751
|
+
ctx.ui?.notify?.("Already on that model.", "info");
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
if (typeof pi.setModel !== "function") {
|
|
2755
|
+
ctx.ui?.notify?.("This host can't switch models programmatically.", "warning");
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const ok = await pi.setModel(model);
|
|
2759
|
+
if (!ok)
|
|
2760
|
+
ctx.ui?.notify?.(
|
|
2761
|
+
`Could not switch to ${model.provider}/${model.id} \u2014 no API key configured for it.`,
|
|
2762
|
+
"warning"
|
|
2763
|
+
);
|
|
2764
|
+
}, "handler")
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
}, "piPrivacy");
|
|
2769
|
+
}
|
|
2770
|
+
__name(makePiPrivacyExtension, "makePiPrivacyExtension");
|
|
2771
|
+
var extension_default = makePiPrivacyExtension();
|
|
2772
|
+
|
|
2773
|
+
// node_modules/pi-privacy/src/options.ts?ext=privateer-privacy
|
|
2774
|
+
var CODE_ONLY_OPTIONS = [
|
|
2775
|
+
"onPosture",
|
|
2776
|
+
"resolveTier",
|
|
2777
|
+
"renderBadge",
|
|
2778
|
+
"privateerVerifiedTee",
|
|
2779
|
+
"piiUnattended",
|
|
2780
|
+
"renderPiiAutoRedact"
|
|
2781
|
+
];
|
|
2782
|
+
|
|
2783
|
+
// node_modules/pi-privacy/src/config.ts?ext=privateer-privacy
|
|
2784
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2785
|
+
var POLICY3 = ["warn", "redact", "off"];
|
|
2786
|
+
var TOOL_POLICY = ["warn", "block", "off"];
|
|
2787
|
+
var DOWNGRADE_POLICY = ["warn", "block", "off"];
|
|
2788
|
+
var SURFACE_POLICY = ["warn", "report", "off"];
|
|
2789
|
+
var SINKS = ["status", "widget", "title", "notify"];
|
|
2790
|
+
function parseBool(name, raw, warn) {
|
|
2791
|
+
const v = raw.trim().toLowerCase();
|
|
2792
|
+
if (["1", "true", "yes", "on"].includes(v)) return true;
|
|
2793
|
+
if (["0", "false", "no", "off"].includes(v)) return false;
|
|
2794
|
+
warn(`${name}="${raw}" is not a boolean (true/false) \u2014 ignoring, using the default.`);
|
|
2795
|
+
return void 0;
|
|
2796
|
+
}
|
|
2797
|
+
__name(parseBool, "parseBool");
|
|
2798
|
+
function parseEnum(name, raw, allowed, warn) {
|
|
2799
|
+
const v = raw.trim().toLowerCase();
|
|
2800
|
+
if (allowed.includes(v)) return v;
|
|
2801
|
+
warn(`${name}="${raw}" is not one of ${allowed.join("|")} \u2014 ignoring, using the default.`);
|
|
2802
|
+
return void 0;
|
|
2803
|
+
}
|
|
2804
|
+
__name(parseEnum, "parseEnum");
|
|
2805
|
+
function parseSinks(name, raw, warn) {
|
|
2806
|
+
const parts = raw.split(/[,\s]+/).map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
2807
|
+
const out = [];
|
|
2808
|
+
for (const p of parts) {
|
|
2809
|
+
if (SINKS.includes(p)) out.push(p);
|
|
2810
|
+
else warn(`${name}: "${p}" is not a badge sink (${SINKS.join("|")}) \u2014 dropping it.`);
|
|
2811
|
+
}
|
|
2812
|
+
return out.length ? out : (warn(`${name}="${raw}" has no valid sinks \u2014 using the default chain.`), void 0);
|
|
2813
|
+
}
|
|
2814
|
+
__name(parseSinks, "parseSinks");
|
|
2815
|
+
function boolVar(opts, env, name, key, warn) {
|
|
2816
|
+
const raw = env[name];
|
|
2817
|
+
if (raw === void 0 || raw === "") return;
|
|
2818
|
+
const b = parseBool(name, raw, warn);
|
|
2819
|
+
if (b !== void 0) opts[key] = b;
|
|
2820
|
+
}
|
|
2821
|
+
__name(boolVar, "boolVar");
|
|
2822
|
+
function optionsFromEnv(env, warn) {
|
|
2823
|
+
const opts = {};
|
|
2824
|
+
const pii = env.PI_PRIVACY_PII_POLICY;
|
|
2825
|
+
if (pii) {
|
|
2826
|
+
const v = parseEnum("PI_PRIVACY_PII_POLICY", pii, POLICY3, warn);
|
|
2827
|
+
if (v) opts.piiPolicy = v;
|
|
2828
|
+
}
|
|
2829
|
+
const tool = env.PI_PRIVACY_TOOL_EXFIL_POLICY;
|
|
2830
|
+
if (tool) {
|
|
2831
|
+
const v = parseEnum("PI_PRIVACY_TOOL_EXFIL_POLICY", tool, TOOL_POLICY, warn);
|
|
2832
|
+
if (v) opts.toolExfilPolicy = v;
|
|
2833
|
+
}
|
|
2834
|
+
const result = env.PI_PRIVACY_TOOL_RESULT_POLICY;
|
|
2835
|
+
if (result) {
|
|
2836
|
+
const v = parseEnum("PI_PRIVACY_TOOL_RESULT_POLICY", result, POLICY3, warn);
|
|
2837
|
+
if (v) opts.toolResultPolicy = v;
|
|
2838
|
+
}
|
|
2839
|
+
const down = env.PI_PRIVACY_DOWNGRADE_POLICY;
|
|
2840
|
+
if (down) {
|
|
2841
|
+
const v = parseEnum("PI_PRIVACY_DOWNGRADE_POLICY", down, DOWNGRADE_POLICY, warn);
|
|
2842
|
+
if (v) opts.downgradePolicy = v;
|
|
2843
|
+
}
|
|
2844
|
+
const surface = env.PI_PRIVACY_TOOL_SURFACE_POLICY;
|
|
2845
|
+
if (surface) {
|
|
2846
|
+
const v = parseEnum("PI_PRIVACY_TOOL_SURFACE_POLICY", surface, SURFACE_POLICY, warn);
|
|
2847
|
+
if (v) opts.toolSurfacePolicy = v;
|
|
2848
|
+
}
|
|
2849
|
+
boolVar(opts, env, "PI_PRIVACY_ENFORCE_OPENROUTER_ZDR", "enforceOpenRouterZdr", warn);
|
|
2850
|
+
boolVar(opts, env, "PI_PRIVACY_SHOW_BADGE", "showBadge", warn);
|
|
2851
|
+
boolVar(opts, env, "PI_PRIVACY_INSTALL_DISPATCHER", "installDispatcher", warn);
|
|
2852
|
+
boolVar(opts, env, "PI_PRIVACY_REGISTER_PROVIDERS", "registerProviders", warn);
|
|
2853
|
+
boolVar(opts, env, "PI_PRIVACY_USE_DISPATCHER_TRANSPORT", "useDispatcherTransport", warn);
|
|
2854
|
+
boolVar(opts, env, "PI_PRIVACY_MODEL_PICKER", "modelPicker", warn);
|
|
2855
|
+
boolVar(opts, env, "PI_PRIVACY_PII_COMMAND", "piiCommand", warn);
|
|
2856
|
+
const sinks = env.PI_PRIVACY_BADGE_SINKS;
|
|
2857
|
+
if (sinks) {
|
|
2858
|
+
const v = parseSinks("PI_PRIVACY_BADGE_SINKS", sinks, warn);
|
|
2859
|
+
if (v) opts.badgeSinks = v;
|
|
2860
|
+
}
|
|
2861
|
+
const allow = env.PI_PRIVACY_PII_ALLOW;
|
|
2862
|
+
if (allow) {
|
|
2863
|
+
const parts = allow.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
2864
|
+
if (parts.length) opts.piiAllow = parts;
|
|
2865
|
+
else warn(`PI_PRIVACY_PII_ALLOW="${allow}" has no entries \u2014 ignoring it.`);
|
|
2866
|
+
}
|
|
2867
|
+
boolVar(opts, env, "PI_PRIVACY_PII_ALLOW_DEFAULTS", "piiAllowDefaults", warn);
|
|
2868
|
+
const key = env.PI_PRIVACY_BADGE_KEY;
|
|
2869
|
+
if (key && key.trim()) opts.badgeKey = key.trim();
|
|
2870
|
+
const cmd = env.PI_PRIVACY_MODEL_PICKER_COMMAND;
|
|
2871
|
+
if (cmd && cmd.trim()) opts.modelPickerCommand = cmd.trim();
|
|
2872
|
+
const scmd = env.PI_PRIVACY_TOOL_SURFACE_COMMAND;
|
|
2873
|
+
if (scmd && scmd.trim()) opts.toolSurfaceCommand = scmd.trim();
|
|
2874
|
+
const pcmd = env.PI_PRIVACY_PII_COMMAND_NAME;
|
|
2875
|
+
if (pcmd && pcmd.trim()) opts.piiCommandName = pcmd.trim();
|
|
2876
|
+
return opts;
|
|
2877
|
+
}
|
|
2878
|
+
__name(optionsFromEnv, "optionsFromEnv");
|
|
2879
|
+
function sanitizeConfig(raw, warn) {
|
|
2880
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2881
|
+
warn(`config file is not a JSON object \u2014 ignoring it.`);
|
|
2882
|
+
return {};
|
|
2883
|
+
}
|
|
2884
|
+
const src = raw;
|
|
2885
|
+
const opts = {};
|
|
2886
|
+
const enumKey = /* @__PURE__ */ __name((k, allowed, set) => {
|
|
2887
|
+
if (!(k in src)) return;
|
|
2888
|
+
const val = src[k];
|
|
2889
|
+
if (typeof val === "string" && allowed.includes(val.toLowerCase()))
|
|
2890
|
+
set(val.toLowerCase());
|
|
2891
|
+
else warn(`config.${k}=${JSON.stringify(val)} is not one of ${allowed.join("|")} \u2014 ignoring.`);
|
|
2892
|
+
}, "enumKey");
|
|
2893
|
+
const boolKey = /* @__PURE__ */ __name((k) => {
|
|
2894
|
+
if (!(k in src)) return;
|
|
2895
|
+
const val = src[k];
|
|
2896
|
+
if (typeof val === "boolean") opts[k] = val;
|
|
2897
|
+
else warn(`config.${k}=${JSON.stringify(val)} is not a boolean \u2014 ignoring.`);
|
|
2898
|
+
}, "boolKey");
|
|
2899
|
+
enumKey("piiPolicy", POLICY3, (v) => opts.piiPolicy = v);
|
|
2900
|
+
enumKey("toolExfilPolicy", TOOL_POLICY, (v) => opts.toolExfilPolicy = v);
|
|
2901
|
+
enumKey("toolResultPolicy", POLICY3, (v) => opts.toolResultPolicy = v);
|
|
2902
|
+
enumKey("downgradePolicy", DOWNGRADE_POLICY, (v) => opts.downgradePolicy = v);
|
|
2903
|
+
enumKey("toolSurfacePolicy", SURFACE_POLICY, (v) => opts.toolSurfacePolicy = v);
|
|
2904
|
+
boolKey("enforceOpenRouterZdr");
|
|
2905
|
+
boolKey("showBadge");
|
|
2906
|
+
boolKey("installDispatcher");
|
|
2907
|
+
boolKey("registerProviders");
|
|
2908
|
+
boolKey("useDispatcherTransport");
|
|
2909
|
+
boolKey("modelPicker");
|
|
2910
|
+
boolKey("piiAllowDefaults");
|
|
2911
|
+
boolKey("piiCommand");
|
|
2912
|
+
if ("piiAllow" in src) {
|
|
2913
|
+
const val = src.piiAllow;
|
|
2914
|
+
if (Array.isArray(val)) {
|
|
2915
|
+
const out = val.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => s.trim());
|
|
2916
|
+
if (out.length !== val.length) warn(`config.piiAllow has non-string/empty entries \u2014 dropping them.`);
|
|
2917
|
+
if (out.length) opts.piiAllow = out;
|
|
2918
|
+
} else warn(`config.piiAllow is not an array of strings \u2014 ignoring.`);
|
|
2919
|
+
}
|
|
2920
|
+
if ("badgeSinks" in src) {
|
|
2921
|
+
const val = src.badgeSinks;
|
|
2922
|
+
if (Array.isArray(val)) {
|
|
2923
|
+
const out = val.filter(
|
|
2924
|
+
(s) => typeof s === "string" && SINKS.includes(s)
|
|
2925
|
+
);
|
|
2926
|
+
if (out.length) opts.badgeSinks = out;
|
|
2927
|
+
else warn(`config.badgeSinks has no valid sinks (${SINKS.join("|")}) \u2014 using the default chain.`);
|
|
2928
|
+
} else warn(`config.badgeSinks is not an array \u2014 ignoring.`);
|
|
2929
|
+
}
|
|
2930
|
+
if ("badgeKey" in src) {
|
|
2931
|
+
const val = src.badgeKey;
|
|
2932
|
+
if (typeof val === "string" && val.trim()) opts.badgeKey = val.trim();
|
|
2933
|
+
else warn(`config.badgeKey=${JSON.stringify(val)} is not a non-empty string \u2014 ignoring.`);
|
|
2934
|
+
}
|
|
2935
|
+
if ("modelPickerCommand" in src) {
|
|
2936
|
+
const val = src.modelPickerCommand;
|
|
2937
|
+
if (typeof val === "string" && val.trim()) opts.modelPickerCommand = val.trim();
|
|
2938
|
+
else warn(`config.modelPickerCommand=${JSON.stringify(val)} is not a non-empty string \u2014 ignoring.`);
|
|
2939
|
+
}
|
|
2940
|
+
if ("toolSurfaceCommand" in src) {
|
|
2941
|
+
const val = src.toolSurfaceCommand;
|
|
2942
|
+
if (typeof val === "string" && val.trim()) opts.toolSurfaceCommand = val.trim();
|
|
2943
|
+
else warn(`config.toolSurfaceCommand=${JSON.stringify(val)} is not a non-empty string \u2014 ignoring.`);
|
|
2944
|
+
}
|
|
2945
|
+
if ("piiCommandName" in src) {
|
|
2946
|
+
const val = src.piiCommandName;
|
|
2947
|
+
if (typeof val === "string" && val.trim()) opts.piiCommandName = val.trim();
|
|
2948
|
+
else warn(`config.piiCommandName=${JSON.stringify(val)} is not a non-empty string \u2014 ignoring.`);
|
|
2949
|
+
}
|
|
2950
|
+
for (const k of Object.keys(src)) {
|
|
2951
|
+
if (CODE_ONLY_OPTIONS.includes(k))
|
|
2952
|
+
warn(`config.${k} is a code-only option and can't be set from JSON \u2014 import makePiPrivacyExtension() to use it.`);
|
|
2953
|
+
}
|
|
2954
|
+
return opts;
|
|
2955
|
+
}
|
|
2956
|
+
__name(sanitizeConfig, "sanitizeConfig");
|
|
2957
|
+
var PROTECTIVENESS = {
|
|
2958
|
+
piiPolicy: { off: 0, warn: 1, redact: 2 },
|
|
2959
|
+
toolExfilPolicy: { off: 0, warn: 1, block: 2 },
|
|
2960
|
+
toolResultPolicy: { off: 0, warn: 1, redact: 2 },
|
|
2961
|
+
downgradePolicy: { off: 0, warn: 1, block: 2 },
|
|
2962
|
+
// "report" keeps the inventory but stops warning; "off" removes the axis entirely.
|
|
2963
|
+
// Both are weaker than the default, and this is the sharpest case the floor exists
|
|
2964
|
+
// for: the tool-surface axis is what reports tools THE PROJECT SUPPLIED, so a
|
|
2965
|
+
// project-local config turning it down is not a hypothetical attack — it is the
|
|
2966
|
+
// whole attack, and it would leave no trace by construction.
|
|
2967
|
+
toolSurfacePolicy: { off: 0, report: 1, warn: 2 }
|
|
2968
|
+
};
|
|
2969
|
+
var DEFAULT_POLICY = {
|
|
2970
|
+
piiPolicy: "warn",
|
|
2971
|
+
toolExfilPolicy: "warn",
|
|
2972
|
+
toolResultPolicy: "warn",
|
|
2973
|
+
downgradePolicy: "warn",
|
|
2974
|
+
toolSurfacePolicy: "warn"
|
|
2975
|
+
};
|
|
2976
|
+
var PROTECTIVE_WHEN_TRUE = [
|
|
2977
|
+
"showBadge",
|
|
2978
|
+
// hides the live posture badge
|
|
2979
|
+
"installDispatcher",
|
|
2980
|
+
// no dispatcher → no TLS-key binding for Tinfoil attestation
|
|
2981
|
+
"useDispatcherTransport",
|
|
2982
|
+
// attestation stops binding to the real inference connection
|
|
2983
|
+
"registerProviders",
|
|
2984
|
+
// removes the private providers from the model list
|
|
2985
|
+
"modelPicker"
|
|
2986
|
+
// removes the privacy-ranked picker
|
|
2987
|
+
];
|
|
2988
|
+
var PROJECT_MAY_NOT_SET = {
|
|
2989
|
+
// The sharpest of the lot: `{"piiAllow": ["*@*", "@acme.com"]}` in a cloned repo is
|
|
2990
|
+
// piiPolicy:"off" for exactly the data that repo cares about, and it would read as a
|
|
2991
|
+
// gate that simply found nothing. An allowlist only ever REMOVES detection, so it
|
|
2992
|
+
// can only come from you.
|
|
2993
|
+
piiAllow: "an allowlist entry can hide PII from the gate entirely",
|
|
2994
|
+
badgeSinks: "a sink list can hide the posture badge",
|
|
2995
|
+
toolSurfaceCommand: "renaming the command can hide the tool-surface listing",
|
|
2996
|
+
modelPickerCommand: "renaming the command can hide the privacy-ranked model picker",
|
|
2997
|
+
// Not because hiding /pii would weaken anything — it wouldn't — but because a name
|
|
2998
|
+
// is a way to SHADOW: `{"piiCommandName": "surface"}` registers the PII lever over
|
|
2999
|
+
// the tool-surface listing, and the listing is exactly what would have named the
|
|
3000
|
+
// repo's own tools.
|
|
3001
|
+
piiCommandName: "renaming the command can shadow another command's name"
|
|
3002
|
+
};
|
|
3003
|
+
function clampProjectConfig(opts, warn) {
|
|
3004
|
+
const src = opts;
|
|
3005
|
+
const out = {};
|
|
3006
|
+
const note = /* @__PURE__ */ __name((key, val, floor) => warn(
|
|
3007
|
+
`project-local pi-privacy.config.json sets ${key}=${JSON.stringify(val)}, which is WEAKER than the built-in default (${floor}) \u2014 ignoring it. A project you open must not be able to turn off your privacy guards. If you meant it, set the PI_PRIVACY_* env var, or point PI_PRIVACY_CONFIG at this file.`
|
|
3008
|
+
), "note");
|
|
3009
|
+
for (const [key, val] of Object.entries(src)) {
|
|
3010
|
+
const ranks = PROTECTIVENESS[key];
|
|
3011
|
+
if (ranks) {
|
|
3012
|
+
const floor = DEFAULT_POLICY[key];
|
|
3013
|
+
if ((ranks[String(val)] ?? 0) < ranks[floor]) {
|
|
3014
|
+
note(key, val, `"${floor}"`);
|
|
3015
|
+
continue;
|
|
3016
|
+
}
|
|
3017
|
+
} else if (PROTECTIVE_WHEN_TRUE.includes(key) && val === false) {
|
|
3018
|
+
note(key, val, "true");
|
|
3019
|
+
continue;
|
|
3020
|
+
} else if (key in PROJECT_MAY_NOT_SET) {
|
|
3021
|
+
warn(
|
|
3022
|
+
`project-local pi-privacy.config.json sets ${key} \u2014 ignoring it, since ${PROJECT_MAY_NOT_SET[key]}. Set the PI_PRIVACY_* env var, or point PI_PRIVACY_CONFIG at this file, if you meant it.`
|
|
3023
|
+
);
|
|
3024
|
+
continue;
|
|
3025
|
+
}
|
|
3026
|
+
out[key] = val;
|
|
3027
|
+
}
|
|
3028
|
+
return out;
|
|
3029
|
+
}
|
|
3030
|
+
__name(clampProjectConfig, "clampProjectConfig");
|
|
3031
|
+
function loadConfig(deps = {}) {
|
|
3032
|
+
const env = deps.env ?? process.env;
|
|
3033
|
+
const warn = deps.warn ?? ((m) => console.warn(`[pi-privacy] ${m}`));
|
|
3034
|
+
const fromFile = loadFileConfig(env, deps, warn);
|
|
3035
|
+
const fromEnv = optionsFromEnv(env, warn);
|
|
3036
|
+
return { ...fromFile, ...fromEnv };
|
|
3037
|
+
}
|
|
3038
|
+
__name(loadConfig, "loadConfig");
|
|
3039
|
+
function loadFileConfig(env, deps, warn) {
|
|
3040
|
+
const explicit = env.PI_PRIVACY_CONFIG?.trim();
|
|
3041
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
3042
|
+
const path = explicit && explicit.length ? explicit : `${cwd}/pi-privacy.config.json`;
|
|
3043
|
+
let text;
|
|
3044
|
+
try {
|
|
3045
|
+
const read = deps.readFile ?? ((p) => readFileSync6(p, "utf8"));
|
|
3046
|
+
text = read(path);
|
|
3047
|
+
} catch (e) {
|
|
3048
|
+
if (explicit) warn(`config file "${path}" could not be read: ${e.message}`);
|
|
3049
|
+
return {};
|
|
3050
|
+
}
|
|
3051
|
+
let parsed;
|
|
3052
|
+
try {
|
|
3053
|
+
parsed = JSON.parse(text);
|
|
3054
|
+
} catch (e) {
|
|
3055
|
+
warn(`config file "${path}" is not valid JSON: ${e.message} \u2014 ignoring it.`);
|
|
3056
|
+
return {};
|
|
3057
|
+
}
|
|
3058
|
+
const opts = sanitizeConfig(parsed, warn);
|
|
3059
|
+
if (explicit || deps.projectTrusted) return opts;
|
|
3060
|
+
return clampProjectConfig(opts, warn);
|
|
3061
|
+
}
|
|
3062
|
+
__name(loadFileConfig, "loadFileConfig");
|
|
3063
|
+
|
|
3064
|
+
// src/providers/defaultModel.ts?ext=privateer-privacy
|
|
3065
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3066
|
+
import { join as join5 } from "node:path";
|
|
3067
|
+
var TINFOIL_MODEL_ID = "tinfoil/gemma4-31b";
|
|
3068
|
+
var TINFOIL_DEFAULT_SPEC = TINFOIL_MODEL_ID;
|
|
3069
|
+
var ACCOUNT_DEFAULT_MODEL_ID = TINFOIL_MODEL_ID;
|
|
3070
|
+
var ACCOUNT_DEFAULT_SPEC = `privateer/${ACCOUNT_DEFAULT_MODEL_ID}`;
|
|
3071
|
+
var ACCOUNT_NEAR_MODEL_ID = "near/zai-org/GLM-5.1-FP8";
|
|
3072
|
+
var LEGACY_BYO_FALLBACK = "openrouter/openai/gpt-4o-mini";
|
|
3073
|
+
var BYO_BY_KEY = [
|
|
3074
|
+
{ env: "ANTHROPIC_API_KEY", spec: "anthropic/claude-opus-4-8" },
|
|
3075
|
+
{ env: "OPENAI_API_KEY", spec: "openai/gpt-5.5" },
|
|
3076
|
+
{ env: "OPENROUTER_API_KEY", spec: LEGACY_BYO_FALLBACK }
|
|
3077
|
+
];
|
|
3078
|
+
function savedPiDefaultSpec() {
|
|
3079
|
+
try {
|
|
3080
|
+
const raw = readFileSync7(join5(agentDir(), "settings.json"), "utf8").trim();
|
|
3081
|
+
if (!raw) return null;
|
|
3082
|
+
const s = JSON.parse(raw);
|
|
3083
|
+
const provider = typeof s.defaultProvider === "string" ? s.defaultProvider.trim() : "";
|
|
3084
|
+
const modelId = typeof s.defaultModel === "string" ? s.defaultModel.trim() : "";
|
|
3085
|
+
return provider && modelId ? `${provider}/${modelId}` : null;
|
|
3086
|
+
} catch {
|
|
3087
|
+
return null;
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
__name(savedPiDefaultSpec, "savedPiDefaultSpec");
|
|
3091
|
+
function resolveDefaultModel(opts = {}) {
|
|
3092
|
+
const env = opts.env ?? process.env;
|
|
3093
|
+
const explicit = opts.explicit?.trim();
|
|
3094
|
+
if (explicit) return explicit;
|
|
3095
|
+
const fromEnv = env.PRIVATEER_MODEL?.trim();
|
|
3096
|
+
if (fromEnv) return fromEnv;
|
|
3097
|
+
const saved = opts.saved === void 0 ? savedPiDefaultSpec() : opts.saved;
|
|
3098
|
+
if (saved) return saved;
|
|
3099
|
+
if (env.TINFOIL_API_KEY?.trim()) return TINFOIL_DEFAULT_SPEC;
|
|
3100
|
+
const signedIn = opts.signedIn ?? hasCredentials();
|
|
3101
|
+
if (signedIn) return ACCOUNT_DEFAULT_SPEC;
|
|
3102
|
+
for (const { env: keyName, spec } of BYO_BY_KEY) {
|
|
3103
|
+
if (env[keyName]?.trim()) return spec;
|
|
3104
|
+
}
|
|
3105
|
+
return ACCOUNT_DEFAULT_SPEC;
|
|
3106
|
+
}
|
|
3107
|
+
__name(resolveDefaultModel, "resolveDefaultModel");
|
|
3108
|
+
function resolveSignedInModel(env = process.env) {
|
|
3109
|
+
return resolveDefaultModel({ env, signedIn: true, saved: null });
|
|
3110
|
+
}
|
|
3111
|
+
__name(resolveSignedInModel, "resolveSignedInModel");
|
|
3112
|
+
function splitSpec(spec) {
|
|
3113
|
+
const slash = spec.indexOf("/");
|
|
3114
|
+
if (slash <= 0 || slash === spec.length - 1) return null;
|
|
3115
|
+
return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
|
|
3116
|
+
}
|
|
3117
|
+
__name(splitSpec, "splitSpec");
|
|
3118
|
+
function ensurePiDefaultModel(spec = resolveSignedInModel()) {
|
|
3119
|
+
return writeSettingsDefaultModel(spec, true);
|
|
3120
|
+
}
|
|
3121
|
+
__name(ensurePiDefaultModel, "ensurePiDefaultModel");
|
|
3122
|
+
function writePiDefaultModel(spec) {
|
|
3123
|
+
return writeSettingsDefaultModel(spec, false);
|
|
3124
|
+
}
|
|
3125
|
+
__name(writePiDefaultModel, "writePiDefaultModel");
|
|
3126
|
+
function writeSettingsDefaultModel(spec, onlyIfUnset) {
|
|
3127
|
+
const parts = splitSpec(spec);
|
|
3128
|
+
if (!parts) return null;
|
|
3129
|
+
const dir = agentDir();
|
|
3130
|
+
const settingsPath = join5(dir, "settings.json");
|
|
3131
|
+
try {
|
|
3132
|
+
mkdirSync4(dir, { recursive: true });
|
|
3133
|
+
let settings = {};
|
|
3134
|
+
if (existsSync3(settingsPath)) {
|
|
3135
|
+
const raw = readFileSync7(settingsPath, "utf8").trim();
|
|
3136
|
+
if (raw) settings = JSON.parse(raw);
|
|
3137
|
+
}
|
|
3138
|
+
if (onlyIfUnset && typeof settings.defaultModel === "string" && settings.defaultModel.trim()) {
|
|
3139
|
+
return null;
|
|
3140
|
+
}
|
|
3141
|
+
settings.defaultProvider = parts.provider;
|
|
3142
|
+
settings.defaultModel = parts.modelId;
|
|
3143
|
+
writeFileSync5(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
3144
|
+
`);
|
|
3145
|
+
return spec;
|
|
3146
|
+
} catch {
|
|
3147
|
+
return null;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
__name(writeSettingsDefaultModel, "writeSettingsDefaultModel");
|
|
3151
|
+
|
|
3152
|
+
// src/providers/vision.ts?ext=privateer-privacy
|
|
3153
|
+
var VISION_PATTERNS = [
|
|
3154
|
+
// ── Named in the id ────────────────────────────────────────────────────────
|
|
3155
|
+
// Qwen's vision line (qwen3-vl-*, qwen2.5-vl-72b, near/Qwen/Qwen3-VL-30B-A3B) and
|
|
3156
|
+
// Baidu's (ernie-4.5-vl-424b). Qwen ships VL as a SEPARATE line, which is also why
|
|
3157
|
+
// the plain qwen3.x ids below are deliberately absent.
|
|
3158
|
+
/-vl[-_.]/i,
|
|
3159
|
+
// Z.ai marks vision with a trailing v on the version: glm-4.5v, glm-4.6v,
|
|
3160
|
+
// glm-5v-turbo. Same reasoning — the unsuffixed glm-5.x are their text siblings.
|
|
3161
|
+
/\bglm-\d+(?:\.\d+)?v\b/i,
|
|
3162
|
+
// ByteDance's GUI agent reads screenshots; that is the whole point of it.
|
|
3163
|
+
/\bui-tars\b/i,
|
|
3164
|
+
// ── Families that are multimodal throughout ────────────────────────────────
|
|
3165
|
+
// Every Claude from 3 onwards takes images, and 3 is the oldest in the catalog.
|
|
3166
|
+
/(^|\/)anthropic\/claude-/i,
|
|
3167
|
+
// Gemini has been multimodal since 1.0.
|
|
3168
|
+
/(^|\/)google\/gemini-/i,
|
|
3169
|
+
// Gemma from 3 on is multimodal; gemma-2 is not, so the version is part of the
|
|
3170
|
+
// match. Covers google/gemma-3-27b-it, google/gemma-4-31b-it, tinfoil/gemma4-31b,
|
|
3171
|
+
// near/google/gemma-4-31B-it, phala/google/gemma-4-31b-it and the Phala
|
|
3172
|
+
// gemma-4 derivatives.
|
|
3173
|
+
/gemma-?[34]/i,
|
|
3174
|
+
// gpt-4o, gpt-4.1 and the whole gpt-5 line. Deliberately NOT `openai/gpt-4`
|
|
3175
|
+
// (the original is text-only), not gpt-3.5, and not gpt-oss — the open-weights
|
|
3176
|
+
// models are text-only, which is what our own default was until this change.
|
|
3177
|
+
/(^|\/)openai\/gpt-(?:4o|4\.1|5)/i,
|
|
3178
|
+
// Llama 4 (maverick, scout) is natively multimodal; 3.x is not.
|
|
3179
|
+
/(^|\/)meta-llama\/llama-4-/i,
|
|
3180
|
+
// Nova lite/pro/premier take images; nova-micro is text-only.
|
|
3181
|
+
/(^|\/)amazon\/nova-(?:2-)?(?:lite|pro|premier)/i,
|
|
3182
|
+
// Grok 4 and up.
|
|
3183
|
+
/(^|\/)x-ai\/grok-4/i,
|
|
3184
|
+
// Mistral's multimodal tiers: small 3.2 and the medium 3 line.
|
|
3185
|
+
/(^|\/)mistralai\/mistral-(?:small-3\.2|medium-3)/i
|
|
3186
|
+
];
|
|
3187
|
+
function acceptsImages(modelId) {
|
|
3188
|
+
return VISION_PATTERNS.some((re) => re.test(modelId));
|
|
3189
|
+
}
|
|
3190
|
+
__name(acceptsImages, "acceptsImages");
|
|
3191
|
+
function visionInput(modelId) {
|
|
3192
|
+
return acceptsImages(modelId) ? ["text", "image"] : ["text"];
|
|
3193
|
+
}
|
|
3194
|
+
__name(visionInput, "visionInput");
|
|
3195
|
+
|
|
3196
|
+
// src/providers/phala/aci-verifier/errors.ts?ext=privateer-privacy
|
|
3197
|
+
var AciError = class extends Error {
|
|
3198
|
+
static {
|
|
3199
|
+
__name(this, "AciError");
|
|
3200
|
+
}
|
|
3201
|
+
constructor(message) {
|
|
3202
|
+
super(message);
|
|
3203
|
+
this.name = "AciError";
|
|
3204
|
+
}
|
|
3205
|
+
};
|
|
3206
|
+
var AciFormatError = class extends AciError {
|
|
3207
|
+
static {
|
|
3208
|
+
__name(this, "AciFormatError");
|
|
3209
|
+
}
|
|
3210
|
+
constructor(message) {
|
|
3211
|
+
super(message);
|
|
3212
|
+
this.name = "AciFormatError";
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
3215
|
+
|
|
3216
|
+
// src/providers/phala/aci-verifier/jcs.ts?ext=privateer-privacy
|
|
3217
|
+
function canonicalize(value) {
|
|
3218
|
+
if (value === null) return "null";
|
|
3219
|
+
switch (typeof value) {
|
|
3220
|
+
case "boolean":
|
|
3221
|
+
return value ? "true" : "false";
|
|
3222
|
+
case "number":
|
|
3223
|
+
if (!Number.isInteger(value)) {
|
|
3224
|
+
throw new AciFormatError(
|
|
3225
|
+
`JCS: ACI restricts numbers to integers, got ${value} (\xA73)`
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
return Object.is(value, -0) ? "0" : String(value);
|
|
3229
|
+
case "string":
|
|
3230
|
+
return JSON.stringify(value);
|
|
3231
|
+
case "object":
|
|
3232
|
+
if (Array.isArray(value)) {
|
|
3233
|
+
return "[" + value.map(canonicalize).join(",") + "]";
|
|
3234
|
+
}
|
|
3235
|
+
return serializeObject(value);
|
|
3236
|
+
default:
|
|
3237
|
+
throw new AciFormatError(`JCS: unsupported type ${typeof value}`);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
__name(canonicalize, "canonicalize");
|
|
3241
|
+
function serializeObject(obj) {
|
|
3242
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
3243
|
+
let out = "{";
|
|
3244
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3245
|
+
const k = keys[i];
|
|
3246
|
+
if (i > 0) out += ",";
|
|
3247
|
+
out += JSON.stringify(k) + ":" + canonicalize(obj[k]);
|
|
3248
|
+
}
|
|
3249
|
+
return out + "}";
|
|
3250
|
+
}
|
|
3251
|
+
__name(serializeObject, "serializeObject");
|
|
3252
|
+
function jcsBytes(value) {
|
|
3253
|
+
return new TextEncoder().encode(canonicalize(value));
|
|
3254
|
+
}
|
|
3255
|
+
__name(jcsBytes, "jcsBytes");
|
|
3256
|
+
|
|
3257
|
+
// src/providers/phala/aci-verifier/crypto.ts?ext=privateer-privacy
|
|
3258
|
+
var subtle = globalThis.crypto.subtle;
|
|
3259
|
+
function toHex(bytes) {
|
|
3260
|
+
let out = "";
|
|
3261
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
3262
|
+
return out;
|
|
3263
|
+
}
|
|
3264
|
+
__name(toHex, "toHex");
|
|
3265
|
+
function fromHex(hex) {
|
|
3266
|
+
const h = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
|
|
3267
|
+
if (h.length % 2 !== 0) {
|
|
3268
|
+
throw new AciFormatError(`hex string has odd length: ${hex.length} chars`);
|
|
3269
|
+
}
|
|
3270
|
+
const out = new Uint8Array(h.length / 2);
|
|
3271
|
+
for (let i = 0; i < out.length; i++) {
|
|
3272
|
+
const byte = Number.parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
3273
|
+
if (Number.isNaN(byte)) {
|
|
3274
|
+
throw new AciFormatError(`invalid hex at offset ${i * 2}: "${h.slice(i * 2, i * 2 + 2)}"`);
|
|
3275
|
+
}
|
|
3276
|
+
out[i] = byte;
|
|
3277
|
+
}
|
|
3278
|
+
return out;
|
|
3279
|
+
}
|
|
3280
|
+
__name(fromHex, "fromHex");
|
|
3281
|
+
async function sha256(bytes) {
|
|
3282
|
+
return new Uint8Array(await subtle.digest("SHA-256", bytes));
|
|
3283
|
+
}
|
|
3284
|
+
__name(sha256, "sha256");
|
|
3285
|
+
async function sha256Hex(bytes) {
|
|
3286
|
+
return toHex(await sha256(bytes));
|
|
3287
|
+
}
|
|
3288
|
+
__name(sha256Hex, "sha256Hex");
|
|
3289
|
+
async function sha256Prefixed(bytes) {
|
|
3290
|
+
return "sha256:" + await sha256Hex(bytes);
|
|
3291
|
+
}
|
|
3292
|
+
__name(sha256Prefixed, "sha256Prefixed");
|
|
3293
|
+
|
|
3294
|
+
// src/providers/phala/aci-verifier/digest.ts?ext=privateer-privacy
|
|
3295
|
+
var DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
|
|
3296
|
+
var NONCE_RE = /^[0-9a-f]{64}$/;
|
|
3297
|
+
async function computeKeysetDigest(keyset) {
|
|
3298
|
+
return sha256Prefixed(jcsBytes(keyset));
|
|
3299
|
+
}
|
|
3300
|
+
__name(computeKeysetDigest, "computeKeysetDigest");
|
|
3301
|
+
function attestationStatement(keysetDigest, nonce) {
|
|
3302
|
+
if (!DIGEST_RE.test(keysetDigest)) {
|
|
3303
|
+
throw new AciFormatError(`keyset digest is not sha256:<64-hex>: "${keysetDigest}"`);
|
|
3304
|
+
}
|
|
3305
|
+
if (nonce != null && !NONCE_RE.test(nonce)) {
|
|
3306
|
+
throw new AciFormatError("nonce must be exactly 64 lowercase hex characters (\xA73.2)");
|
|
3307
|
+
}
|
|
3308
|
+
const noncePart = nonce == null ? "null" : `"${nonce}"`;
|
|
3309
|
+
return new TextEncoder().encode(
|
|
3310
|
+
`{"keyset_digest":"${keysetDigest}","nonce":${noncePart},"purpose":"aci.report_data.v1"}`
|
|
3311
|
+
);
|
|
3312
|
+
}
|
|
3313
|
+
__name(attestationStatement, "attestationStatement");
|
|
3314
|
+
async function computeReportData(keysetDigest, nonce) {
|
|
3315
|
+
return sha256Hex(attestationStatement(keysetDigest, nonce));
|
|
3316
|
+
}
|
|
3317
|
+
__name(computeReportData, "computeReportData");
|
|
3318
|
+
|
|
3319
|
+
// src/providers/phala/aci-verifier/e2ee-channel.ts?ext=privateer-privacy
|
|
3320
|
+
var subtle2 = globalThis.crypto.subtle;
|
|
3321
|
+
var enc = new TextEncoder();
|
|
3322
|
+
var dec = new TextDecoder();
|
|
3323
|
+
var HKDF_INFO = enc.encode("aci.e2ee.v2.x25519");
|
|
3324
|
+
|
|
3325
|
+
// src/providers/phala/aci-verifier/report.ts?ext=privateer-privacy
|
|
3326
|
+
async function verifyReportBinding(report, nonce, options = {}) {
|
|
3327
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
3328
|
+
const checks = [];
|
|
3329
|
+
const versionOk = report.api_version === "aci/1";
|
|
3330
|
+
checks.push({
|
|
3331
|
+
name: "api_version",
|
|
3332
|
+
ok: versionOk,
|
|
3333
|
+
...versionOk ? {} : { detail: `api_version "${report.api_version}" is not "aci/1"` }
|
|
3334
|
+
});
|
|
3335
|
+
const keysetValue = report.attestation.workload_keyset;
|
|
3336
|
+
if (keysetValue === null || typeof keysetValue !== "object" || Array.isArray(keysetValue)) {
|
|
3337
|
+
const detail = "workload_keyset is not a JSON object";
|
|
3338
|
+
for (const name of ["workload_keyset_digest", "report_data", "not_after"]) {
|
|
3339
|
+
checks.push({ name, ok: false, detail });
|
|
3340
|
+
}
|
|
3341
|
+
return { ok: false, checks };
|
|
3342
|
+
}
|
|
3343
|
+
const digest = await computeKeysetDigest(keysetValue);
|
|
3344
|
+
pushEqual(checks, "workload_keyset_digest", report.workload_keyset_digest, digest);
|
|
3345
|
+
const expectedReportData = await computeReportData(digest, nonce);
|
|
3346
|
+
pushEqual(checks, "report_data", report.attestation.report_data, expectedReportData);
|
|
3347
|
+
const keyset = keysetValue;
|
|
3348
|
+
if (typeof keyset.not_after !== "number") {
|
|
3349
|
+
checks.push({
|
|
3350
|
+
name: "not_after",
|
|
3351
|
+
ok: false,
|
|
3352
|
+
detail: "keyset has no numeric not_after"
|
|
3353
|
+
});
|
|
3354
|
+
} else {
|
|
3355
|
+
const ok = now < keyset.not_after;
|
|
3356
|
+
checks.push({
|
|
3357
|
+
name: "not_after",
|
|
3358
|
+
ok,
|
|
3359
|
+
...ok ? {} : { detail: `now ${now} >= not_after ${keyset.not_after}` }
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
return {
|
|
3363
|
+
ok: checks.every((c2) => c2.ok),
|
|
3364
|
+
checks,
|
|
3365
|
+
workloadKeysetDigest: digest,
|
|
3366
|
+
keyset
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
__name(verifyReportBinding, "verifyReportBinding");
|
|
3370
|
+
function pushEqual(checks, name, actual, expected) {
|
|
3371
|
+
const ok = actual === expected;
|
|
3372
|
+
checks.push({ name, ok, ...ok ? {} : { detail: `report ${actual} != recomputed ${expected}` } });
|
|
3373
|
+
}
|
|
3374
|
+
__name(pushEqual, "pushEqual");
|
|
3375
|
+
|
|
3376
|
+
// src/providers/phala/measurements.ts?ext=privateer-privacy
|
|
3377
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3378
|
+
var RTMR_KEYS = ["rtMr0", "rtMr1", "rtMr2", "rtMr3"];
|
|
3379
|
+
function parseEventLog(raw) {
|
|
3380
|
+
let value = raw;
|
|
3381
|
+
if (typeof raw === "string") {
|
|
3382
|
+
try {
|
|
3383
|
+
value = JSON.parse(raw);
|
|
3384
|
+
} catch {
|
|
3385
|
+
return [];
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
if (!Array.isArray(value)) return [];
|
|
3389
|
+
return value.filter(
|
|
3390
|
+
(e) => !!e && typeof e === "object" && typeof e.imr === "number" && typeof e.digest === "string"
|
|
3391
|
+
);
|
|
3392
|
+
}
|
|
3393
|
+
__name(parseEventLog, "parseEventLog");
|
|
3394
|
+
function replayRtmrs(events) {
|
|
3395
|
+
const out = {};
|
|
3396
|
+
RTMR_KEYS.forEach((key, imr) => {
|
|
3397
|
+
let acc = Buffer.alloc(48);
|
|
3398
|
+
for (const ev of events) {
|
|
3399
|
+
if (ev.imr !== imr) continue;
|
|
3400
|
+
let digest;
|
|
3401
|
+
try {
|
|
3402
|
+
digest = Buffer.from(ev.digest, "hex");
|
|
3403
|
+
} catch {
|
|
3404
|
+
continue;
|
|
3405
|
+
}
|
|
3406
|
+
acc = createHash2("sha384").update(Buffer.concat([acc, digest])).digest();
|
|
3407
|
+
}
|
|
3408
|
+
out[key] = acc.toString("hex");
|
|
3409
|
+
});
|
|
3410
|
+
return out;
|
|
3411
|
+
}
|
|
3412
|
+
__name(replayRtmrs, "replayRtmrs");
|
|
3413
|
+
function appIdentityFrom(events) {
|
|
3414
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3415
|
+
for (const ev of events) {
|
|
3416
|
+
if (ev.imr === 3 && ev.event && typeof ev.event_payload === "string" && ev.event_payload) {
|
|
3417
|
+
byName.set(ev.event, ev.event_payload);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
return {
|
|
3421
|
+
appId: byName.get("app-id"),
|
|
3422
|
+
composeHash: byName.get("compose-hash"),
|
|
3423
|
+
osImageHash: byName.get("os-image-hash"),
|
|
3424
|
+
instanceId: byName.get("instance-id"),
|
|
3425
|
+
mrKms: byName.get("mr-kms"),
|
|
3426
|
+
keyProvider: byName.get("key-provider")
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
__name(appIdentityFrom, "appIdentityFrom");
|
|
3430
|
+
function computeComposeHash(appCompose) {
|
|
3431
|
+
if (typeof appCompose !== "string" || !appCompose) return void 0;
|
|
3432
|
+
return createHash2("sha256").update(Buffer.from(appCompose, "utf8")).digest("hex");
|
|
3433
|
+
}
|
|
3434
|
+
__name(computeComposeHash, "computeComposeHash");
|
|
3435
|
+
function checkReportConsistency(args) {
|
|
3436
|
+
const checks = [];
|
|
3437
|
+
const skipped = [];
|
|
3438
|
+
if (args.events.length === 0) {
|
|
3439
|
+
skipped.push("rtmr-replay");
|
|
3440
|
+
} else {
|
|
3441
|
+
const replayed = replayRtmrs(args.events);
|
|
3442
|
+
for (const key of RTMR_KEYS) {
|
|
3443
|
+
const ok = replayed[key] === args.quoted[key];
|
|
3444
|
+
checks.push({
|
|
3445
|
+
name: `rtmr-replay:${key}`,
|
|
3446
|
+
ok,
|
|
3447
|
+
detail: ok ? void 0 : `replayed ${replayed[key].slice(0, 16)}\u2026 but the quote signed ${args.quoted[key].slice(0, 16)}\u2026`
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
const computed = computeComposeHash(args.appCompose);
|
|
3452
|
+
if (!computed || !args.identity.composeHash) {
|
|
3453
|
+
skipped.push("compose-hash");
|
|
3454
|
+
} else {
|
|
3455
|
+
const ok = computed === args.identity.composeHash;
|
|
3456
|
+
checks.push({
|
|
3457
|
+
name: "compose-hash",
|
|
3458
|
+
ok,
|
|
3459
|
+
detail: ok ? void 0 : `sha256(app_compose)=${computed.slice(0, 16)}\u2026 but the log attests ${args.identity.composeHash.slice(0, 16)}\u2026`
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
return { checks, ok: checks.every((c2) => c2.ok), skipped };
|
|
3463
|
+
}
|
|
3464
|
+
__name(checkReportConsistency, "checkReportConsistency");
|
|
3465
|
+
|
|
3466
|
+
// src/providers/phala/pin.ts?ext=privateer-privacy
|
|
3467
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3468
|
+
import { join as join6 } from "node:path";
|
|
3469
|
+
var PIN_VERSION = 1;
|
|
3470
|
+
function pinPath() {
|
|
3471
|
+
return join6(globalDir(), "phala-enclave-pin.json");
|
|
3472
|
+
}
|
|
3473
|
+
__name(pinPath, "pinPath");
|
|
3474
|
+
function readPin() {
|
|
3475
|
+
try {
|
|
3476
|
+
const parsed = JSON.parse(readFileSync8(pinPath(), "utf8"));
|
|
3477
|
+
if (parsed?.v !== PIN_VERSION || !parsed.pin || typeof parsed.pin !== "object") return null;
|
|
3478
|
+
return parsed;
|
|
3479
|
+
} catch {
|
|
3480
|
+
return null;
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
__name(readPin, "readPin");
|
|
3484
|
+
function writePin(pin, firstSeenAt) {
|
|
3485
|
+
try {
|
|
3486
|
+
mkdirSync5(globalDir(), { recursive: true });
|
|
3487
|
+
writeFileSync6(pinPath(), JSON.stringify({ v: PIN_VERSION, firstSeenAt, pin }, null, 2) + "\n", "utf8");
|
|
3488
|
+
} catch {
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
__name(writePin, "writePin");
|
|
3492
|
+
function checkPin(current, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3493
|
+
const stored = readPin();
|
|
3494
|
+
if (!stored?.pin) {
|
|
3495
|
+
const firstSeenAt = now();
|
|
3496
|
+
writePin(current, firstSeenAt);
|
|
3497
|
+
return { state: "first-seen", changed: [], firstSeenAt };
|
|
3498
|
+
}
|
|
3499
|
+
const changed = [];
|
|
3500
|
+
for (const key of Object.keys(current)) {
|
|
3501
|
+
const was = stored.pin[key];
|
|
3502
|
+
const is = current[key];
|
|
3503
|
+
if (was && is && was !== is) changed.push(`${key}: ${was.slice(0, 12)}\u2026 \u2192 ${is.slice(0, 12)}\u2026`);
|
|
3504
|
+
}
|
|
3505
|
+
if (changed.length === 0) return { state: "unchanged", changed: [], firstSeenAt: stored.firstSeenAt };
|
|
3506
|
+
writePin(current, stored.firstSeenAt ?? now());
|
|
3507
|
+
return { state: "changed", changed, firstSeenAt: stored.firstSeenAt };
|
|
3508
|
+
}
|
|
3509
|
+
__name(checkPin, "checkPin");
|
|
3510
|
+
|
|
3511
|
+
// src/providers/phalaSeal.ts?ext=privateer-privacy
|
|
3512
|
+
var DEFAULT_ACCEPTABLE_TCB = ["UpToDate"];
|
|
3513
|
+
function relayBase() {
|
|
3514
|
+
return `${serverBaseUrl().replace(/\/+$/, "")}/api/sealed/phala`;
|
|
3515
|
+
}
|
|
3516
|
+
__name(relayBase, "relayBase");
|
|
3517
|
+
function requireQuote() {
|
|
3518
|
+
const v = process.env.PRIVATEER_PHALA_REQUIRE_QUOTE;
|
|
3519
|
+
return !(v === "0" || v === "false");
|
|
3520
|
+
}
|
|
3521
|
+
__name(requireQuote, "requireQuote");
|
|
3522
|
+
function pccsUrl() {
|
|
3523
|
+
return process.env.PRIVATEER_PHALA_PCCS_URL || void 0;
|
|
3524
|
+
}
|
|
3525
|
+
__name(pccsUrl, "pccsUrl");
|
|
3526
|
+
function acceptableTcb() {
|
|
3527
|
+
const v = process.env.PRIVATEER_PHALA_TCB;
|
|
3528
|
+
const list = v ? v.split(",").map((s) => s.trim()).filter(Boolean) : DEFAULT_ACCEPTABLE_TCB;
|
|
3529
|
+
return new Set(list);
|
|
3530
|
+
}
|
|
3531
|
+
__name(acceptableTcb, "acceptableTcb");
|
|
3532
|
+
function extractQuoteMeasurements(report) {
|
|
3533
|
+
const td = report.asTd10?.() ?? report.asTd15?.()?.base;
|
|
3534
|
+
const fields = [td?.mrTd, td?.rtMr0, td?.rtMr1, td?.rtMr2, td?.rtMr3];
|
|
3535
|
+
if (fields.some((f) => !f?.length)) return void 0;
|
|
3536
|
+
const [mrTd, rtMr0, rtMr1, rtMr2, rtMr3] = fields.map((f) => toHex(new Uint8Array(f)));
|
|
3537
|
+
return { mrTd, rtMr0, rtMr1, rtMr2, rtMr3 };
|
|
3538
|
+
}
|
|
3539
|
+
__name(extractQuoteMeasurements, "extractQuoteMeasurements");
|
|
3540
|
+
function extractQuoteReportData(report) {
|
|
3541
|
+
const td10 = report.asTd10?.();
|
|
3542
|
+
if (td10?.reportData) return new Uint8Array(td10.reportData);
|
|
3543
|
+
const td15 = report.asTd15?.();
|
|
3544
|
+
if (td15?.base?.reportData) return new Uint8Array(td15.base.reportData);
|
|
3545
|
+
const data = report.data;
|
|
3546
|
+
if (data?.reportData) return new Uint8Array(data.reportData);
|
|
3547
|
+
throw new Error("phala: verified quote report has no reportData");
|
|
3548
|
+
}
|
|
3549
|
+
__name(extractQuoteReportData, "extractQuoteReportData");
|
|
3550
|
+
var attestationPromise = null;
|
|
3551
|
+
function attest() {
|
|
3552
|
+
if (!attestationPromise) {
|
|
3553
|
+
attestationPromise = establishAttestation().catch((err) => {
|
|
3554
|
+
attestationPromise = null;
|
|
3555
|
+
throw err;
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
return attestationPromise;
|
|
3559
|
+
}
|
|
3560
|
+
__name(attest, "attest");
|
|
3561
|
+
async function establishAttestation() {
|
|
3562
|
+
const nonce = toHex(globalThis.crypto.getRandomValues(new Uint8Array(32)));
|
|
3563
|
+
const res = await fetch(`${relayBase()}/attestation?nonce=${nonce}`, { method: "GET" });
|
|
3564
|
+
if (!res.ok) throw new Error(`phala attestation HTTP ${res.status}`);
|
|
3565
|
+
const report = await res.json();
|
|
3566
|
+
const verification = await verifyReportBinding(report, nonce);
|
|
3567
|
+
if (!verification.ok) {
|
|
3568
|
+
const failed = verification.checks.filter((c2) => !c2.ok).map((c2) => c2.name).join(", ");
|
|
3569
|
+
throw new Error(`phala attestation binding failed: ${failed}`);
|
|
3570
|
+
}
|
|
3571
|
+
const enclave = await verifyHardwareQuote(report);
|
|
3572
|
+
return { report, verification, enclave };
|
|
3573
|
+
}
|
|
3574
|
+
__name(establishAttestation, "establishAttestation");
|
|
3575
|
+
async function verifyHardwareQuote(report) {
|
|
3576
|
+
if (!requireQuote()) return void 0;
|
|
3577
|
+
const attestation = report.attestation;
|
|
3578
|
+
const teeType = String(attestation?.tee_type || "");
|
|
3579
|
+
if (teeType !== "tdx") throw new Error(`phala: unsupported/absent tee_type "${teeType}" (only tdx is wired)`);
|
|
3580
|
+
const quoteHex = attestation.evidence?.quote;
|
|
3581
|
+
if (typeof quoteHex !== "string" || !quoteHex) throw new Error("phala: attestation evidence has no TDX quote");
|
|
3582
|
+
const reportDataHex = String(attestation.report_data || "").toLowerCase();
|
|
3583
|
+
if (!reportDataHex) throw new Error("phala: report has no report_data");
|
|
3584
|
+
const { getCollateralAndVerify } = await import("./c-77GO5L5Y.mjs");
|
|
3585
|
+
const verified = await getCollateralAndVerify(fromHex(quoteHex), pccsUrl());
|
|
3586
|
+
const status = String(verified.status);
|
|
3587
|
+
if (!acceptableTcb().has(status)) throw new Error(`phala: TDX quote TCB status not accepted: "${status}"`);
|
|
3588
|
+
const quoteReportData = extractQuoteReportData(verified.report);
|
|
3589
|
+
if (toHex(quoteReportData.slice(0, 32)) !== reportDataHex) {
|
|
3590
|
+
throw new Error("phala: TDX quote report_data does not bind the attested report_data");
|
|
3591
|
+
}
|
|
3592
|
+
const declared = attestation.evidence?.quote_report_data;
|
|
3593
|
+
if (typeof declared === "string" && declared && toHex(quoteReportData) !== declared.toLowerCase()) {
|
|
3594
|
+
throw new Error("phala: evidence.quote_report_data does not match the verified quote");
|
|
3595
|
+
}
|
|
3596
|
+
const measurements = extractQuoteMeasurements(verified.report);
|
|
3597
|
+
if (!measurements) return void 0;
|
|
3598
|
+
const events = parseEventLog(attestation.evidence?.event_log);
|
|
3599
|
+
const identity = appIdentityFrom(events);
|
|
3600
|
+
const consistency = checkReportConsistency({
|
|
3601
|
+
events,
|
|
3602
|
+
quoted: measurements,
|
|
3603
|
+
appCompose: attestation.evidence?.app_compose,
|
|
3604
|
+
identity
|
|
3605
|
+
});
|
|
3606
|
+
if (!consistency.ok) {
|
|
3607
|
+
const failed = consistency.checks.filter((c2) => !c2.ok).map((c2) => `${c2.name} (${c2.detail})`).join("; ");
|
|
3608
|
+
throw new Error(`phala: attestation self-consistency failed: ${failed}`);
|
|
3609
|
+
}
|
|
3610
|
+
const pin = checkPin({
|
|
3611
|
+
mrTd: measurements.mrTd,
|
|
3612
|
+
rtMr0: measurements.rtMr0,
|
|
3613
|
+
rtMr1: measurements.rtMr1,
|
|
3614
|
+
rtMr2: measurements.rtMr2,
|
|
3615
|
+
appId: identity.appId,
|
|
3616
|
+
composeHash: identity.composeHash,
|
|
3617
|
+
osImageHash: identity.osImageHash,
|
|
3618
|
+
mrKms: identity.mrKms
|
|
3619
|
+
});
|
|
3620
|
+
const provenance = attestation.source_provenance;
|
|
3621
|
+
const downstream = attestation.evidence?.downstream_tls_binding;
|
|
3622
|
+
return {
|
|
3623
|
+
measurements,
|
|
3624
|
+
identity,
|
|
3625
|
+
pin,
|
|
3626
|
+
skippedChecks: consistency.skipped,
|
|
3627
|
+
// Named so a human can go read the source; self-declared by the enclave and NOT
|
|
3628
|
+
// proven by the quote (image_digest/image_provenance come back null), so it is a
|
|
3629
|
+
// pointer to audit, never evidence that this binary came from that commit.
|
|
3630
|
+
repoUrl: provenance?.repo_url,
|
|
3631
|
+
repoCommit: provenance?.repo_commit,
|
|
3632
|
+
// The attested enclave forwards to this downstream host over TLS. Surfaced because
|
|
3633
|
+
// it marks where our attested boundary ENDS — that host is a separate trust domain
|
|
3634
|
+
// we do not attest.
|
|
3635
|
+
downstreamDomain: downstream?.domain,
|
|
3636
|
+
downstreamSpkiSha256: downstream?.spki_sha256
|
|
3637
|
+
};
|
|
3638
|
+
}
|
|
3639
|
+
__name(verifyHardwareQuote, "verifyHardwareQuote");
|
|
3640
|
+
async function attestPhala() {
|
|
3641
|
+
try {
|
|
3642
|
+
const { enclave } = await attest();
|
|
3643
|
+
return { ok: true, enclaveIdentity: enclave };
|
|
3644
|
+
} catch (e) {
|
|
3645
|
+
return { ok: false, error: e.message };
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
__name(attestPhala, "attestPhala");
|
|
3649
|
+
|
|
3650
|
+
// src/providers/sealedShim.ts?ext=privateer-privacy
|
|
3651
|
+
function sealedEnabled() {
|
|
3652
|
+
const v = process.env.PRIVATEER_SEALED;
|
|
3653
|
+
return !(v === "0" || v === "false");
|
|
3654
|
+
}
|
|
3655
|
+
__name(sealedEnabled, "sealedEnabled");
|
|
3656
|
+
function sealedProviderFor(modelId) {
|
|
3657
|
+
if (modelId.startsWith("tinfoil/")) return "tinfoil";
|
|
3658
|
+
if (modelId.startsWith("phala/")) return "phala";
|
|
3659
|
+
return null;
|
|
3660
|
+
}
|
|
3661
|
+
__name(sealedProviderFor, "sealedProviderFor");
|
|
3662
|
+
function relayBase2(provider) {
|
|
3663
|
+
return `${serverBaseUrl().replace(/\/+$/, "")}/api/sealed/${provider}`;
|
|
3664
|
+
}
|
|
3665
|
+
__name(relayBase2, "relayBase");
|
|
3666
|
+
var clients = /* @__PURE__ */ new Map();
|
|
3667
|
+
var readyPromises = /* @__PURE__ */ new Map();
|
|
3668
|
+
function client(provider) {
|
|
3669
|
+
let c2 = clients.get(provider);
|
|
3670
|
+
if (!c2) {
|
|
3671
|
+
const base = relayBase2(provider);
|
|
3672
|
+
c2 = new SecureClient({ baseURL: base, attestationBundleURL: base, transport: "ehbp" });
|
|
3673
|
+
clients.set(provider, c2);
|
|
3674
|
+
}
|
|
3675
|
+
return c2;
|
|
3676
|
+
}
|
|
3677
|
+
__name(client, "client");
|
|
3678
|
+
function ready(provider) {
|
|
3679
|
+
let p = readyPromises.get(provider);
|
|
3680
|
+
if (!p) {
|
|
3681
|
+
p = client(provider).ready().catch((err) => {
|
|
3682
|
+
readyPromises.delete(provider);
|
|
3683
|
+
throw err;
|
|
3684
|
+
});
|
|
3685
|
+
readyPromises.set(provider, p);
|
|
3686
|
+
}
|
|
3687
|
+
return p;
|
|
3688
|
+
}
|
|
3689
|
+
__name(ready, "ready");
|
|
3690
|
+
async function attestSealed(provider) {
|
|
3691
|
+
if (provider === "phala") return attestPhala();
|
|
3692
|
+
try {
|
|
3693
|
+
await ready(provider);
|
|
3694
|
+
return { ok: true, enclave: client(provider).getEnclaveURL() };
|
|
3695
|
+
} catch (e) {
|
|
3696
|
+
return { ok: false, error: e.message };
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
__name(attestSealed, "attestSealed");
|
|
3700
|
+
var shimBase = null;
|
|
3701
|
+
function sealedShimBase() {
|
|
3702
|
+
return shimBase;
|
|
3703
|
+
}
|
|
3704
|
+
__name(sealedShimBase, "sealedShimBase");
|
|
3705
|
+
|
|
3706
|
+
// src/providers/account.ts?ext=privateer-privacy
|
|
3707
|
+
var DEFAULT_MODELS = [
|
|
3708
|
+
ACCOUNT_DEFAULT_MODEL_ID,
|
|
3709
|
+
ACCOUNT_NEAR_MODEL_ID,
|
|
3710
|
+
// All three former defaults (see TINFOIL_MODEL_ID for the dates). They stay in the
|
|
3711
|
+
// floor so a user who saved any of them as their own default still resolves it
|
|
3712
|
+
// synchronously at launch, rather than falling through to "first model with
|
|
3713
|
+
// configured auth" — the BYO dead end this seed list exists to prevent.
|
|
3714
|
+
"tinfoil/gpt-oss-120b",
|
|
3715
|
+
"tinfoil/kimi-k2-6",
|
|
3716
|
+
"tinfoil/glm-5-2",
|
|
3717
|
+
"anthropic/claude-opus-5",
|
|
3718
|
+
"anthropic/claude-sonnet-5",
|
|
3719
|
+
"openai/gpt-5.6-sol",
|
|
3720
|
+
"deepseek/deepseek-v4-flash",
|
|
3721
|
+
// 2026-08 releases, confirmed live on OpenRouter (ZDR-covered ids reach the
|
|
3722
|
+
// account catalog automatically; these seeds just make them resolve at launch).
|
|
3723
|
+
"moonshotai/kimi-k3",
|
|
3724
|
+
"z-ai/glm-5.2",
|
|
3725
|
+
// Both verified 2026-08-19 against /endpoints/zdr and GET /api/models: ZDR-covered
|
|
3726
|
+
// and servable. Same reason as the line above — the live catalog already carries
|
|
3727
|
+
// them, this only closes the first-launch window where a saved default that isn't
|
|
3728
|
+
// yet in the cache would fall through to a BYO provider.
|
|
3729
|
+
"x-ai/grok-4.6",
|
|
3730
|
+
"openai/gpt-5.6-luna"
|
|
3731
|
+
];
|
|
3732
|
+
var DEFAULT_CONTEXT_WINDOW = 128e3;
|
|
3733
|
+
var MIN_PUBLISHED_CONTEXT_WINDOW = 8192;
|
|
3734
|
+
var MAX_PUBLISHED_CONTEXT_WINDOW = 1e7;
|
|
3735
|
+
function validContextWindow(value) {
|
|
3736
|
+
return typeof value === "number" && Number.isFinite(value) && value >= MIN_PUBLISHED_CONTEXT_WINDOW && value <= MAX_PUBLISHED_CONTEXT_WINDOW ? Math.floor(value) : void 0;
|
|
3737
|
+
}
|
|
3738
|
+
__name(validContextWindow, "validContextWindow");
|
|
3739
|
+
var DEFAULT_MAX_OUTPUT_TOKENS = 16384;
|
|
3740
|
+
var ACCOUNT_MAX_OUTPUT_TOKENS = 32e3;
|
|
3741
|
+
var MIN_PUBLISHED_MAX_OUTPUT = 1024;
|
|
3742
|
+
function validMaxOutputTokens(value) {
|
|
3743
|
+
return typeof value === "number" && Number.isFinite(value) && value >= MIN_PUBLISHED_MAX_OUTPUT ? Math.min(Math.floor(value), ACCOUNT_MAX_OUTPUT_TOKENS) : void 0;
|
|
3744
|
+
}
|
|
3745
|
+
__name(validMaxOutputTokens, "validMaxOutputTokens");
|
|
3746
|
+
function seedModel(id) {
|
|
3747
|
+
return {
|
|
3748
|
+
id,
|
|
3749
|
+
name: id,
|
|
3750
|
+
// reasoning + how to steer it, for the enclave models where we verified the
|
|
3751
|
+
// control shape live; `reasoning: false` (Pi's "not a thinking model") for the rest.
|
|
3752
|
+
...thinkingProfile(id) ?? { reasoning: false },
|
|
3753
|
+
// Honest modalities. This was hardcoded to text for the whole catalog, which made
|
|
3754
|
+
// Pi strip every image from every account request — including the ones `read`
|
|
3755
|
+
// attaches when the user points at a screenshot. See providers/vision.ts.
|
|
3756
|
+
input: visionInput(id),
|
|
3757
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
3758
|
+
// The model's real window when the server published one, else the fallback.
|
|
3759
|
+
// This is not cosmetic: pi sizes each turn's answer budget as
|
|
3760
|
+
// (contextWindow − estimated prompt − 4096) and clamps max_tokens to it
|
|
3761
|
+
// (pi-ai clampMaxTokensToContext, and see src/engine/contextBudget.ts). A window
|
|
3762
|
+
// that under-states the model's real one therefore throttles the answer — and
|
|
3763
|
+
// used to collapse it to a single token — while the model still has room.
|
|
3764
|
+
contextWindow: accountContextWindow(id) ?? DEFAULT_CONTEXT_WINDOW,
|
|
3765
|
+
// The model's real output cap when published. Not cosmetic either: reasoning and
|
|
3766
|
+
// the answer share this one budget, and pi only compact-and-retries a length stop
|
|
3767
|
+
// that ended BELOW it — a thinking model that spends all of a too-small budget
|
|
3768
|
+
// reasoning ends the run with "Response was truncated before completion."
|
|
3769
|
+
// (measured: glm-5.3-flash spending all 16384 on 50k+ chars of reasoning, whose
|
|
3770
|
+
// real cap is far higher). See also ext/truncationRecovery.ts.
|
|
3771
|
+
maxTokens: accountMaxOutputTokens(id) ?? DEFAULT_MAX_OUTPUT_TOKENS
|
|
3772
|
+
};
|
|
3773
|
+
}
|
|
3774
|
+
__name(seedModel, "seedModel");
|
|
3775
|
+
var CHAT_TEMPLATE_THINKING = {
|
|
3776
|
+
reasoning: true,
|
|
3777
|
+
thinkingLevelMap: { minimal: null, low: null, high: null, xhigh: null },
|
|
3778
|
+
compat: { thinkingFormat: "qwen-chat-template" }
|
|
3779
|
+
};
|
|
3780
|
+
var REASONING_EFFORT_THINKING = {
|
|
3781
|
+
reasoning: true,
|
|
3782
|
+
thinkingLevelMap: { off: "low", minimal: "low", xhigh: null }
|
|
3783
|
+
};
|
|
3784
|
+
var OPENROUTER_THINKING = {
|
|
3785
|
+
reasoning: true,
|
|
3786
|
+
thinkingLevelMap: { off: "low", minimal: "low" },
|
|
3787
|
+
compat: { thinkingFormat: "openrouter" }
|
|
3788
|
+
};
|
|
3789
|
+
var PROXIED_THINKING_MODEL = [
|
|
3790
|
+
/^google\/gemini-/i,
|
|
3791
|
+
// gemini-2.5+ all take an effort; gemma-* deliberately excluded
|
|
3792
|
+
/^z-ai\/glm-/i,
|
|
3793
|
+
// the whole GLM line reasons, vision variants included
|
|
3794
|
+
/^deepseek\/deepseek-(r1|v3\.[12]|v4)/i,
|
|
3795
|
+
// NOT deepseek-chat*, which is the non-thinking split
|
|
3796
|
+
/^x-ai\/grok-4/i,
|
|
3797
|
+
// grok-4.x; grok-build-0.1 is unprobed
|
|
3798
|
+
/^openai\/gpt-(5|6|oss)/i
|
|
3799
|
+
// gpt-4*/gpt-3.5* do not reason
|
|
3800
|
+
];
|
|
3801
|
+
var NON_THINKING_VARIANT = /(?:^|[-/])(?:chat|instruct)(?:[-.]|$)/i;
|
|
3802
|
+
var TEE_MODEL = /^(tinfoil|phala|near)\//;
|
|
3803
|
+
function teeThinkingProfile(id) {
|
|
3804
|
+
if (/instruct/i.test(id)) return null;
|
|
3805
|
+
if (/gpt-oss/i.test(id)) return REASONING_EFFORT_THINKING;
|
|
3806
|
+
if (/glm|qwen/i.test(id)) return CHAT_TEMPLATE_THINKING;
|
|
3807
|
+
return null;
|
|
3808
|
+
}
|
|
3809
|
+
__name(teeThinkingProfile, "teeThinkingProfile");
|
|
3810
|
+
function proxiedThinkingProfile(id) {
|
|
3811
|
+
if (NON_THINKING_VARIANT.test(id)) return null;
|
|
3812
|
+
return PROXIED_THINKING_MODEL.some((re) => re.test(id)) ? OPENROUTER_THINKING : null;
|
|
3813
|
+
}
|
|
3814
|
+
__name(proxiedThinkingProfile, "proxiedThinkingProfile");
|
|
3815
|
+
function thinkingProfile(id) {
|
|
3816
|
+
const profile = TEE_MODEL.test(id) ? teeThinkingProfile(id) : proxiedThinkingProfile(id);
|
|
3817
|
+
return profile && { ...profile, thinkingLevelMap: { ...profile.thinkingLevelMap }, ...profile.compat ? { compat: { ...profile.compat } } : {} };
|
|
3818
|
+
}
|
|
3819
|
+
__name(thinkingProfile, "thinkingProfile");
|
|
3820
|
+
var CATALOG_CACHE_MAX = 2e3;
|
|
3821
|
+
function catalogCachePath() {
|
|
3822
|
+
return join7(globalDir(), "account-models.json");
|
|
3823
|
+
}
|
|
3824
|
+
__name(catalogCachePath, "catalogCachePath");
|
|
3825
|
+
var cachedCapabilities = null;
|
|
3826
|
+
function cachedCapabilityMaps() {
|
|
3827
|
+
if (cachedCapabilities) return cachedCapabilities;
|
|
3828
|
+
cachedCapabilities = { windows: /* @__PURE__ */ new Map(), maxOutputs: /* @__PURE__ */ new Map() };
|
|
3829
|
+
try {
|
|
3830
|
+
const path = catalogCachePath();
|
|
3831
|
+
if (existsSync4(path)) {
|
|
3832
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
3833
|
+
readCapability(parsed.windows, validContextWindow, cachedCapabilities.windows);
|
|
3834
|
+
readCapability(parsed.maxOutputs, validMaxOutputTokens, cachedCapabilities.maxOutputs);
|
|
3835
|
+
}
|
|
3836
|
+
} catch {
|
|
3837
|
+
}
|
|
3838
|
+
return cachedCapabilities;
|
|
3839
|
+
}
|
|
3840
|
+
__name(cachedCapabilityMaps, "cachedCapabilityMaps");
|
|
3841
|
+
function readCapability(raw, valid, into) {
|
|
3842
|
+
if (!raw || typeof raw !== "object") return;
|
|
3843
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
3844
|
+
const n = valid(value);
|
|
3845
|
+
if (n !== void 0 && into.size < CATALOG_CACHE_MAX) into.set(id, n);
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
__name(readCapability, "readCapability");
|
|
3849
|
+
function cachedContextWindows() {
|
|
3850
|
+
return cachedCapabilityMaps().windows;
|
|
3851
|
+
}
|
|
3852
|
+
__name(cachedContextWindows, "cachedContextWindows");
|
|
3853
|
+
function loadCachedCatalogIds() {
|
|
3854
|
+
try {
|
|
3855
|
+
const path = catalogCachePath();
|
|
3856
|
+
if (!existsSync4(path)) return [];
|
|
3857
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
3858
|
+
if (!Array.isArray(parsed.ids)) return [];
|
|
3859
|
+
return parsed.ids.filter((id) => typeof id === "string" && id.length > 0).slice(0, CATALOG_CACHE_MAX);
|
|
3860
|
+
} catch {
|
|
3861
|
+
return [];
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
__name(loadCachedCatalogIds, "loadCachedCatalogIds");
|
|
3865
|
+
function isServableAccountModel(id) {
|
|
3866
|
+
if (!id.startsWith("phala/")) return true;
|
|
3867
|
+
return sealedEnabled() && sealedShimBase() !== null;
|
|
3868
|
+
}
|
|
3869
|
+
__name(isServableAccountModel, "isServableAccountModel");
|
|
3870
|
+
function seedCatalogIds() {
|
|
3871
|
+
const ids = [...DEFAULT_MODELS];
|
|
3872
|
+
const seen = new Set(ids);
|
|
3873
|
+
for (const id of loadCachedCatalogIds()) {
|
|
3874
|
+
if (!seen.has(id)) {
|
|
3875
|
+
seen.add(id);
|
|
3876
|
+
ids.push(id);
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
return ids;
|
|
3880
|
+
}
|
|
3881
|
+
__name(seedCatalogIds, "seedCatalogIds");
|
|
3882
|
+
var accountWindowMap = /* @__PURE__ */ new Map();
|
|
3883
|
+
function accountContextWindow(id) {
|
|
3884
|
+
return accountWindowMap.get(id) ?? cachedContextWindows().get(id);
|
|
3885
|
+
}
|
|
3886
|
+
__name(accountContextWindow, "accountContextWindow");
|
|
3887
|
+
var accountMaxOutputMap = /* @__PURE__ */ new Map();
|
|
3888
|
+
function accountMaxOutputTokens(id) {
|
|
3889
|
+
return accountMaxOutputMap.get(id) ?? cachedCapabilityMaps().maxOutputs.get(id);
|
|
3890
|
+
}
|
|
3891
|
+
__name(accountMaxOutputTokens, "accountMaxOutputTokens");
|
|
3892
|
+
function verificationLink(raw) {
|
|
3893
|
+
const uri = (raw ?? "").trim();
|
|
3894
|
+
if (!uri) return "";
|
|
3895
|
+
if (/^https?:\/\//i.test(uri)) return uri;
|
|
3896
|
+
return `https://${uri.replace(/^\/+/, "")}`;
|
|
3897
|
+
}
|
|
3898
|
+
__name(verificationLink, "verificationLink");
|
|
3899
|
+
var privateerOAuthProvider = {
|
|
3900
|
+
name: "Privateer account",
|
|
3901
|
+
usesCallbackServer: false,
|
|
3902
|
+
// Pi's login dialog passes `signal` (its cancel AbortController) alongside the
|
|
3903
|
+
// callbacks. We MUST thread it into runDeviceLogin — otherwise escape/ctrl+c
|
|
3904
|
+
// aborts the dialog's signal but our poll loop never sees it, the login()
|
|
3905
|
+
// promise never settles, and Pi never restores the editor: the "Waiting for
|
|
3906
|
+
// authentication…" screen hangs with no way out. See auth/privateer.ts
|
|
3907
|
+
// pollForToken, which checks the signal and rejects with "Login cancelled.".
|
|
3908
|
+
async login(cb) {
|
|
3909
|
+
let wasLinked = hasCredentials();
|
|
3910
|
+
if (wasLinked && cb.onSelect) {
|
|
3911
|
+
const user = currentUser();
|
|
3912
|
+
const who = user?.email ?? user?.id ?? "this account";
|
|
3913
|
+
const choice = await cb.onSelect({
|
|
3914
|
+
message: `Already signed in as ${who}.`,
|
|
3915
|
+
options: [
|
|
3916
|
+
{ id: "keep", label: `Stay signed in as ${who}` },
|
|
3917
|
+
{ id: "switch", label: "Sign in as a different account (signs this machine out first)" }
|
|
3918
|
+
]
|
|
3919
|
+
});
|
|
3920
|
+
if (choice === void 0) throw new Error("Login cancelled");
|
|
3921
|
+
if (choice === "switch") {
|
|
3922
|
+
await logout();
|
|
3923
|
+
wasLinked = false;
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
if (!wasLinked) {
|
|
3927
|
+
try {
|
|
3928
|
+
await runDeviceLogin({
|
|
3929
|
+
signal: cb.signal,
|
|
3930
|
+
onCode: /* @__PURE__ */ __name((code) => {
|
|
3931
|
+
const uri = verificationLink(code.verification_uri_complete ?? code.verification_uri);
|
|
3932
|
+
if (uri && canOpenBrowser()) void openInBrowser(uri);
|
|
3933
|
+
cb.onDeviceCode?.({
|
|
3934
|
+
userCode: code.user_code,
|
|
3935
|
+
verificationUri: uri,
|
|
3936
|
+
intervalSeconds: code.interval,
|
|
3937
|
+
expiresInSeconds: code.expires_in
|
|
3938
|
+
});
|
|
3939
|
+
}, "onCode")
|
|
3940
|
+
});
|
|
3941
|
+
} catch (e) {
|
|
3942
|
+
if (cb.signal?.aborted) throw new Error("Login cancelled");
|
|
3943
|
+
throw e;
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
if (cb.signal?.aborted) throw new Error("Login cancelled");
|
|
3947
|
+
const creds = await accountCredential();
|
|
3948
|
+
ensurePiDefaultModel();
|
|
3949
|
+
notifySignedIn();
|
|
3950
|
+
return creds;
|
|
3951
|
+
},
|
|
3952
|
+
async refreshToken(creds) {
|
|
3953
|
+
const mine = armSlot().cred?.refresh;
|
|
3954
|
+
const refresh = mine && creds.refresh !== mine ? mine : creds.refresh;
|
|
3955
|
+
let next;
|
|
3956
|
+
try {
|
|
3957
|
+
next = await refreshAccountCredentials(refresh);
|
|
3958
|
+
} catch {
|
|
3959
|
+
next = await acquireAccountCredential();
|
|
3960
|
+
}
|
|
3961
|
+
rememberAccountCredential(next);
|
|
3962
|
+
return next;
|
|
3963
|
+
},
|
|
3964
|
+
getApiKey(creds) {
|
|
3965
|
+
return creds.access;
|
|
3966
|
+
}
|
|
3967
|
+
};
|
|
3968
|
+
var TEE_PREFIXES = ["near/", "tinfoil/", "phala/"];
|
|
3969
|
+
function privateerChannel(modelId) {
|
|
3970
|
+
return TEE_PREFIXES.some((p) => modelId.startsWith(p)) ? "tee" : "zdr";
|
|
3971
|
+
}
|
|
3972
|
+
__name(privateerChannel, "privateerChannel");
|
|
3973
|
+
async function accountPosture(modelId) {
|
|
3974
|
+
if (privateerChannel(modelId) === "zdr") {
|
|
3975
|
+
return { tier: "zdr-policy" };
|
|
3976
|
+
}
|
|
3977
|
+
const sealedProvider = sealedEnabled() ? sealedProviderFor(modelId) : null;
|
|
3978
|
+
if (sealedProvider) {
|
|
3979
|
+
const att = await attestSealed(sealedProvider);
|
|
3980
|
+
return att.ok ? { tier: "tee-verified", enclaveIdentity: att.enclaveIdentity } : { tier: "tee-unverified", error: att.error };
|
|
3981
|
+
}
|
|
3982
|
+
if (!modelId.startsWith("near/")) {
|
|
3983
|
+
return { tier: "tee-unverified" };
|
|
3984
|
+
}
|
|
3985
|
+
try {
|
|
3986
|
+
const res = await authedFetch(
|
|
3987
|
+
`${serverBaseUrl()}/api/models/near/attestation?model=${encodeURIComponent(modelId)}`
|
|
3988
|
+
);
|
|
3989
|
+
if (!res.ok) return { tier: "tee-unverified", error: `HTTP ${res.status}` };
|
|
3990
|
+
const data = await res.json();
|
|
3991
|
+
const att = interpretReport(modelId, data.nonce ?? "", data.report ?? {});
|
|
3992
|
+
const tp = teePosture(att);
|
|
3993
|
+
const tier = tierFromTeePosture(tp);
|
|
3994
|
+
return { tier, teePosture: tp };
|
|
3995
|
+
} catch (e) {
|
|
3996
|
+
return { tier: "tee-unverified", error: e.message };
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
__name(accountPosture, "accountPosture");
|
|
4000
|
+
function modelEntry(id) {
|
|
4001
|
+
const base = seedModel(id);
|
|
4002
|
+
const provider = sealedEnabled() ? sealedProviderFor(id) : null;
|
|
4003
|
+
const shim = sealedShimBase();
|
|
4004
|
+
return provider && shim ? { ...base, baseUrl: `${shim}/${provider}/v1` } : base;
|
|
4005
|
+
}
|
|
4006
|
+
__name(modelEntry, "modelEntry");
|
|
4007
|
+
function accountProviderConfig(ids) {
|
|
4008
|
+
return {
|
|
4009
|
+
name: "Privateer account",
|
|
4010
|
+
baseUrl: `${serverBaseUrl()}/api/agent/v1`,
|
|
4011
|
+
api: "openai-completions",
|
|
4012
|
+
oauth: privateerOAuthProvider,
|
|
4013
|
+
// Filter HERE rather than at the catalog, so callers keep passing the server's
|
|
4014
|
+
// full list and every registration re-evaluates servability against the CURRENT
|
|
4015
|
+
// shim state. That is what lets the post-shim re-registration in makeAccountProvider
|
|
4016
|
+
// put `phala/*` back: had the ids been filtered upstream, the sealed-only models
|
|
4017
|
+
// would have been dropped from `lastIds` before the shim ever finished starting and
|
|
4018
|
+
// nothing would have brought them back.
|
|
4019
|
+
models: ids.filter(isServableAccountModel).map(modelEntry)
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
__name(accountProviderConfig, "accountProviderConfig");
|
|
4023
|
+
function registerAccountModels(pi) {
|
|
4024
|
+
pi.registerProvider?.("privateer", accountProviderConfig(seedCatalogIds()));
|
|
4025
|
+
}
|
|
4026
|
+
__name(registerAccountModels, "registerAccountModels");
|
|
4027
|
+
var ARMED = /* @__PURE__ */ Symbol.for("privateer.accountCredential");
|
|
4028
|
+
function armSlot() {
|
|
4029
|
+
const g = globalThis;
|
|
4030
|
+
return g[ARMED] ??= {};
|
|
4031
|
+
}
|
|
4032
|
+
__name(armSlot, "armSlot");
|
|
4033
|
+
function rememberAccountCredential(cred) {
|
|
4034
|
+
armSlot().cred = cred;
|
|
4035
|
+
}
|
|
4036
|
+
__name(rememberAccountCredential, "rememberAccountCredential");
|
|
4037
|
+
function liveAccountCredential() {
|
|
4038
|
+
const cred = armSlot().cred;
|
|
4039
|
+
return cred && cred.expires > Date.now() + 6e4 ? cred : void 0;
|
|
4040
|
+
}
|
|
4041
|
+
__name(liveAccountCredential, "liveAccountCredential");
|
|
4042
|
+
async function accountCredential() {
|
|
4043
|
+
const live = liveAccountCredential();
|
|
4044
|
+
if (live) return live;
|
|
4045
|
+
const slot = armSlot();
|
|
4046
|
+
slot.inFlight ??= acquireAccountCredential().then((cred) => {
|
|
4047
|
+
slot.cred = cred;
|
|
4048
|
+
return cred;
|
|
4049
|
+
}).finally(() => {
|
|
4050
|
+
slot.inFlight = void 0;
|
|
4051
|
+
});
|
|
4052
|
+
return slot.inFlight;
|
|
4053
|
+
}
|
|
4054
|
+
__name(accountCredential, "accountCredential");
|
|
4055
|
+
|
|
4056
|
+
// src/config/privacyDisabled.ts?ext=privateer-privacy
|
|
4057
|
+
var ENV = "PRIVATEER_PRIVACY_OFF";
|
|
4058
|
+
var ALT_ENV = "PI_PRIVACY_OFF";
|
|
4059
|
+
var NO_QUARTER_PRIVACY_MARK = "PRIVATEER_PRIVACY_OFF_BY_NO_QUARTER";
|
|
4060
|
+
function privacyDisabled() {
|
|
4061
|
+
return process.env[ENV] === "1" || process.env[ALT_ENV] === "1";
|
|
4062
|
+
}
|
|
4063
|
+
__name(privacyDisabled, "privacyDisabled");
|
|
4064
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
4065
|
+
function setPrivacyDisabled(disabled) {
|
|
4066
|
+
const before = privacyDisabled();
|
|
4067
|
+
delete process.env[NO_QUARTER_PRIVACY_MARK];
|
|
4068
|
+
if (disabled) {
|
|
4069
|
+
process.env[ENV] = "1";
|
|
4070
|
+
process.env[ALT_ENV] = "1";
|
|
4071
|
+
} else {
|
|
4072
|
+
delete process.env[ENV];
|
|
4073
|
+
delete process.env[ALT_ENV];
|
|
4074
|
+
}
|
|
4075
|
+
if (before !== disabled) {
|
|
4076
|
+
for (const fn of listeners) {
|
|
4077
|
+
try {
|
|
4078
|
+
fn(disabled);
|
|
4079
|
+
} catch {
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
return disabled;
|
|
4084
|
+
}
|
|
4085
|
+
__name(setPrivacyDisabled, "setPrivacyDisabled");
|
|
4086
|
+
|
|
4087
|
+
// src/permissions/noQuarter.ts?ext=privateer-privacy
|
|
4088
|
+
var ENV2 = "PRIVATEER_NO_QUARTER";
|
|
4089
|
+
function noQuarterActive() {
|
|
4090
|
+
return process.env[ENV2] === "1";
|
|
4091
|
+
}
|
|
4092
|
+
__name(noQuarterActive, "noQuarterActive");
|
|
4093
|
+
|
|
4094
|
+
// src/config/piiAllow.ts?ext=privateer-privacy
|
|
4095
|
+
import { readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
|
|
4096
|
+
|
|
4097
|
+
// src/util/atomicWrite.ts?ext=privateer-privacy
|
|
4098
|
+
import { chmodSync as chmodSync5, realpathSync, renameSync as renameSync2, statSync, unlinkSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4099
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
4100
|
+
function writeFileAtomic(path, data, opts = {}) {
|
|
4101
|
+
let target = path;
|
|
4102
|
+
let mode = opts.mode ?? 384;
|
|
4103
|
+
try {
|
|
4104
|
+
target = realpathSync(path);
|
|
4105
|
+
if (opts.mode === void 0) mode = statSync(target).mode & 511;
|
|
4106
|
+
} catch {
|
|
4107
|
+
}
|
|
4108
|
+
const tmp = `${target}.${process.pid}.${randomBytes2(4).toString("hex")}.tmp`;
|
|
4109
|
+
try {
|
|
4110
|
+
writeFileSync8(tmp, data, { encoding: "utf8", mode });
|
|
4111
|
+
try {
|
|
4112
|
+
chmodSync5(tmp, mode);
|
|
4113
|
+
} catch {
|
|
4114
|
+
}
|
|
4115
|
+
renameSync2(tmp, target);
|
|
4116
|
+
} catch (e) {
|
|
4117
|
+
try {
|
|
4118
|
+
unlinkSync(tmp);
|
|
4119
|
+
} catch {
|
|
4120
|
+
}
|
|
4121
|
+
throw e;
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
__name(writeFileAtomic, "writeFileAtomic");
|
|
4125
|
+
|
|
4126
|
+
// src/config/piiAllow.ts?ext=privateer-privacy
|
|
4127
|
+
var cached2 = [];
|
|
4128
|
+
var cachedStamp = "";
|
|
4129
|
+
function parse(raw) {
|
|
4130
|
+
const list = raw?.privacy?.piiAllow;
|
|
4131
|
+
if (!Array.isArray(list)) return [];
|
|
4132
|
+
return list.filter((e) => typeof e === "string" && e.trim() !== "").map((e) => e.trim());
|
|
4133
|
+
}
|
|
4134
|
+
__name(parse, "parse");
|
|
4135
|
+
function piiAllowEntries() {
|
|
4136
|
+
try {
|
|
4137
|
+
const st = statSync2(configPath());
|
|
4138
|
+
const stamp = `${st.mtimeMs}:${st.size}`;
|
|
4139
|
+
if (stamp === cachedStamp) return cached2;
|
|
4140
|
+
cachedStamp = stamp;
|
|
4141
|
+
cached2 = parse(JSON.parse(readFileSync10(configPath(), "utf8")));
|
|
4142
|
+
} catch {
|
|
4143
|
+
cachedStamp = "";
|
|
4144
|
+
cached2 = [];
|
|
4145
|
+
}
|
|
4146
|
+
return cached2;
|
|
4147
|
+
}
|
|
4148
|
+
__name(piiAllowEntries, "piiAllowEntries");
|
|
4149
|
+
function invalidate() {
|
|
4150
|
+
cachedStamp = "";
|
|
4151
|
+
}
|
|
4152
|
+
__name(invalidate, "invalidate");
|
|
4153
|
+
function invalidReason(entry) {
|
|
4154
|
+
if (!entry) return "an empty entry matches nothing";
|
|
4155
|
+
if (/^\*+$/.test(entry)) return "a bare `*` would allowlist everything \u2014 turn the gate off explicitly if that's what you mean";
|
|
4156
|
+
return void 0;
|
|
4157
|
+
}
|
|
4158
|
+
__name(invalidReason, "invalidReason");
|
|
4159
|
+
function write(entries) {
|
|
4160
|
+
let cfg = {};
|
|
4161
|
+
try {
|
|
4162
|
+
cfg = JSON.parse(readFileSync10(configPath(), "utf8"));
|
|
4163
|
+
} catch {
|
|
4164
|
+
}
|
|
4165
|
+
const privacy2 = { ...cfg.privacy ?? {}, piiAllow: entries };
|
|
4166
|
+
writeFileAtomic(configPath(), JSON.stringify({ ...cfg, privacy: privacy2 }, null, 2) + "\n");
|
|
4167
|
+
invalidate();
|
|
4168
|
+
}
|
|
4169
|
+
__name(write, "write");
|
|
4170
|
+
function addPiiAllow(raw) {
|
|
4171
|
+
const entry = raw.trim();
|
|
4172
|
+
const bad = invalidReason(entry);
|
|
4173
|
+
if (bad) return { ok: false, message: bad, entries: piiAllowEntries() };
|
|
4174
|
+
const entries = [...piiAllowEntries()];
|
|
4175
|
+
if (entries.includes(entry)) return { ok: true, message: `${entry} is already allowlisted`, entries };
|
|
4176
|
+
entries.push(entry);
|
|
4177
|
+
write(entries);
|
|
4178
|
+
return { ok: true, message: `${entry} is no longer treated as PII in this and future sessions`, entries };
|
|
4179
|
+
}
|
|
4180
|
+
__name(addPiiAllow, "addPiiAllow");
|
|
4181
|
+
function removePiiAllow(raw) {
|
|
4182
|
+
const entry = raw.trim();
|
|
4183
|
+
const entries = [...piiAllowEntries()];
|
|
4184
|
+
const i = entries.indexOf(entry);
|
|
4185
|
+
if (i < 0) return { ok: false, message: `${entry || "(empty)"} is not in the allowlist`, entries };
|
|
4186
|
+
entries.splice(i, 1);
|
|
4187
|
+
write(entries);
|
|
4188
|
+
return { ok: true, message: `${entry} is gated again`, entries };
|
|
4189
|
+
}
|
|
4190
|
+
__name(removePiiAllow, "removePiiAllow");
|
|
4191
|
+
|
|
4192
|
+
// src/ui/palette.ts?ext=privateer-privacy
|
|
4193
|
+
var ESC = "\x1B[";
|
|
4194
|
+
var RESET = `${ESC}0m`;
|
|
4195
|
+
var BOLD = `${ESC}1m`;
|
|
4196
|
+
var c = /* @__PURE__ */ __name((n) => `${ESC}38;5;${n}m`, "c");
|
|
4197
|
+
var FALLBACK = {
|
|
4198
|
+
RESET,
|
|
4199
|
+
BOLD,
|
|
4200
|
+
INK: c(231),
|
|
4201
|
+
ACCENT: c(231),
|
|
4202
|
+
BORDER: c(231),
|
|
4203
|
+
DIM: `${ESC}90m`,
|
|
4204
|
+
GREEN: `${ESC}32m`,
|
|
4205
|
+
YELLOW: `${ESC}33m`,
|
|
4206
|
+
RED: `${ESC}31m`,
|
|
4207
|
+
SKY: c(117),
|
|
4208
|
+
WHITE: c(231)
|
|
4209
|
+
};
|
|
4210
|
+
function paletteFor(theme) {
|
|
4211
|
+
if (!theme || typeof theme.getFgAnsi !== "function") return FALLBACK;
|
|
4212
|
+
const g = /* @__PURE__ */ __name((name, fallback) => {
|
|
4213
|
+
try {
|
|
4214
|
+
const a = theme.getFgAnsi(name);
|
|
4215
|
+
return typeof a === "string" && a.length > 0 ? a : fallback;
|
|
4216
|
+
} catch {
|
|
4217
|
+
return fallback;
|
|
4218
|
+
}
|
|
4219
|
+
}, "g");
|
|
4220
|
+
return {
|
|
4221
|
+
RESET,
|
|
4222
|
+
BOLD,
|
|
4223
|
+
INK: g("text", FALLBACK.INK),
|
|
4224
|
+
ACCENT: g("accent", FALLBACK.ACCENT),
|
|
4225
|
+
BORDER: g("border", FALLBACK.BORDER),
|
|
4226
|
+
DIM: g("dim", FALLBACK.DIM),
|
|
4227
|
+
GREEN: g("success", FALLBACK.GREEN),
|
|
4228
|
+
YELLOW: g("warning", FALLBACK.YELLOW),
|
|
4229
|
+
RED: g("error", FALLBACK.RED),
|
|
4230
|
+
// Fixed colours, not theme tokens — but a pale blue or white vanishes on a light
|
|
4231
|
+
// background, so a light theme gets a deeper blue and plain ink instead.
|
|
4232
|
+
...lightAppearance(theme) ? { SKY: c(32), WHITE: g("text", FALLBACK.INK) } : { SKY: FALLBACK.SKY, WHITE: FALLBACK.WHITE }
|
|
4233
|
+
};
|
|
4234
|
+
}
|
|
4235
|
+
__name(paletteFor, "paletteFor");
|
|
4236
|
+
function lightAppearance(theme) {
|
|
4237
|
+
try {
|
|
4238
|
+
return theme.appearance === "light";
|
|
4239
|
+
} catch {
|
|
4240
|
+
return false;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
__name(lightAppearance, "lightAppearance");
|
|
4244
|
+
var CLI_DARK = {
|
|
4245
|
+
RESET,
|
|
4246
|
+
BOLD,
|
|
4247
|
+
DIM: `${ESC}2m`,
|
|
4248
|
+
GREEN: `${ESC}32m`,
|
|
4249
|
+
YELLOW: `${ESC}33m`,
|
|
4250
|
+
RED: `${ESC}31m`,
|
|
4251
|
+
CYAN: `${ESC}36m`
|
|
4252
|
+
};
|
|
4253
|
+
var CLI_LIGHT = {
|
|
4254
|
+
RESET,
|
|
4255
|
+
BOLD,
|
|
4256
|
+
DIM: c(243),
|
|
4257
|
+
// solid medium gray instead of faint
|
|
4258
|
+
GREEN: c(28),
|
|
4259
|
+
// dark green
|
|
4260
|
+
YELLOW: c(130),
|
|
4261
|
+
// dark amber (plain 33m is invisible-pale on white)
|
|
4262
|
+
RED: c(124),
|
|
4263
|
+
// dark red
|
|
4264
|
+
CYAN: c(24)
|
|
4265
|
+
// dark teal-blue accent
|
|
4266
|
+
};
|
|
4267
|
+
function detectScheme(env = process.env) {
|
|
4268
|
+
const fgbg = env.COLORFGBG;
|
|
4269
|
+
if (fgbg) {
|
|
4270
|
+
const parts = fgbg.split(";");
|
|
4271
|
+
const bg = parseInt(parts[parts.length - 1], 10);
|
|
4272
|
+
if (!Number.isNaN(bg)) return bg === 7 || bg === 15 ? "light" : "dark";
|
|
4273
|
+
}
|
|
4274
|
+
return "dark";
|
|
4275
|
+
}
|
|
4276
|
+
__name(detectScheme, "detectScheme");
|
|
4277
|
+
function cliPalette(scheme = detectScheme()) {
|
|
4278
|
+
return scheme === "light" ? CLI_LIGHT : CLI_DARK;
|
|
4279
|
+
}
|
|
4280
|
+
__name(cliPalette, "cliPalette");
|
|
4281
|
+
|
|
4282
|
+
// extensions/privateer-posture.ts?ext=privateer-privacy
|
|
4283
|
+
var DOT = { green: "\u{1F7E2}", yellow: "\u{1F7E1}", red: "\u{1F534}", neutral: "\u26AA" };
|
|
4284
|
+
function badgeLabel(tier, p) {
|
|
4285
|
+
if (tier === "tee-verified") return `${p.GREEN}\u26C9 Trusted Execution${p.RESET}`;
|
|
4286
|
+
if (tier === "tee-unverified") return `${p.YELLOW}\u26C9 Trusted Execution (unconfirmed)${p.RESET}`;
|
|
4287
|
+
return null;
|
|
4288
|
+
}
|
|
4289
|
+
__name(badgeLabel, "badgeLabel");
|
|
4290
|
+
async function badgeFor(provider, modelId, p) {
|
|
4291
|
+
const res = provider === "privateer" ? await accountPosture(modelId) : await verifyModelPosture(provider, modelId, {
|
|
4292
|
+
apiKey: provider === "nearai" ? process.env.NEARAI_API_KEY ?? process.env.NEAR_AI_API_KEY : void 0
|
|
4293
|
+
});
|
|
4294
|
+
const shield = badgeLabel(res.tier, p);
|
|
4295
|
+
if (shield) return shield;
|
|
4296
|
+
const info = TIERS[res.tier];
|
|
4297
|
+
return `${DOT[info.posture] ?? "\u26AA"} ${info.label}`;
|
|
4298
|
+
}
|
|
4299
|
+
__name(badgeFor, "badgeFor");
|
|
4300
|
+
async function updatePostureBadge(ctx, provider, modelId) {
|
|
4301
|
+
if (!ctx?.ui?.setStatus) return;
|
|
4302
|
+
if (privacyDisabled()) {
|
|
4303
|
+
ctx.ui.setStatus("privacy", "\u2691 privacy off");
|
|
4304
|
+
ctx.ui.setStatus("pi-privacy", void 0);
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
const p = provider ?? ctx?.model?.provider;
|
|
4308
|
+
const m = modelId ?? ctx?.model?.id;
|
|
4309
|
+
if (!p || !m) {
|
|
4310
|
+
ctx.ui.setStatus("privacy", void 0);
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
try {
|
|
4314
|
+
ctx.ui.setStatus("privacy", "\u26C9 \u2026");
|
|
4315
|
+
const badge = await badgeFor(p, m, paletteFor(ctx?.ui?.theme));
|
|
4316
|
+
ctx.ui.setStatus("privacy", badge);
|
|
4317
|
+
} catch {
|
|
4318
|
+
ctx.ui.setStatus("privacy", void 0);
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
__name(updatePostureBadge, "updatePostureBadge");
|
|
4322
|
+
|
|
4323
|
+
// src/config/privacyPolicy.ts?ext=privateer-privacy
|
|
4324
|
+
function renderPiiAutoRedact(notice) {
|
|
4325
|
+
const p = cliPalette(detectScheme());
|
|
4326
|
+
const body = notice.startsWith("\u2691 ") ? notice.slice(2) : notice;
|
|
4327
|
+
return `${p.RED}\u2691${p.RESET} ${p.CYAN}${body}${p.RESET} ${p.DIM}\xB7 /privacy allow <value> if it shouldn't be masked${p.RESET}`;
|
|
4328
|
+
}
|
|
4329
|
+
__name(renderPiiAutoRedact, "renderPiiAutoRedact");
|
|
4330
|
+
function sharedPrivacyOptions() {
|
|
4331
|
+
const ambient = loadConfig();
|
|
4332
|
+
return {
|
|
4333
|
+
...ambient,
|
|
4334
|
+
disabled: privacyDisabled,
|
|
4335
|
+
// The account channel's real posture. pi-privacy ships a `privateer` provider, but it
|
|
4336
|
+
// is the PUBLIC developer-key channel (sk-priv-…, server-proxied and unverifiable
|
|
4337
|
+
// end-to-end), so from the package alone every privateer/* model floors to
|
|
4338
|
+
// zdr-policy. The in-app ACCOUNT channel is a different thing — its own OAuth
|
|
4339
|
+
// session, account server and sealed relay — and only the host can say what it
|
|
4340
|
+
// resolves to: tee-verified for a quote WE checked over the sealed path,
|
|
4341
|
+
// tee-unverified for a proxied enclave we can't bind to this connection, zdr-policy
|
|
4342
|
+
// for the ZDR-channel models. Without this hook a session running an ATTESTED TEE
|
|
4343
|
+
// model is judged unverified, so the badge lies and the PII gate asks about every
|
|
4344
|
+
// turn — the over-warning c2ee0fa fixed for the terminal and nowhere else.
|
|
4345
|
+
resolveTier: /* @__PURE__ */ __name(async (provider, modelId) => {
|
|
4346
|
+
if (provider !== "privateer") return void 0;
|
|
4347
|
+
return (await accountPosture(modelId)).tier;
|
|
4348
|
+
}, "resolveTier"),
|
|
4349
|
+
// pi-privacy ships its own `/models`, a plain select over the models you hold keys
|
|
4350
|
+
// for. Ours (extensions/privateer-models.ts) is the searchable one over the whole
|
|
4351
|
+
// account catalog, and with both registered Pi renamed them /models:1 and /models:2 —
|
|
4352
|
+
// so typing `/models` could land you in the short, unsearchable list.
|
|
4353
|
+
modelPicker: false,
|
|
4354
|
+
// No quarter = unattended. No quarter also takes the whole filter down (noQuarter.ts),
|
|
4355
|
+
// so this only answers when the operator has run `/privacy on` with the moat down.
|
|
4356
|
+
// The PII send-or-redact question would stall a session the
|
|
4357
|
+
// operator explicitly stepped away from, so pi-privacy swallows it the SAFE way —
|
|
4358
|
+
// redact, then send — and reports what it masked as output instead of asking. A live
|
|
4359
|
+
// function, not a boolean: shift+tab / `/no-quarter` flips this mid-session and the
|
|
4360
|
+
// gate re-reads it on every request.
|
|
4361
|
+
piiUnattended: noQuarterActive,
|
|
4362
|
+
renderPiiAutoRedact,
|
|
4363
|
+
// pi-privacy's INGEST gate: credentials arriving in a tool result are masked before
|
|
4364
|
+
// they enter context (otherwise they're re-sent every turn and written to the session
|
|
4365
|
+
// file on disk). We already redact tool output in src/ext/permissionGate.ts, so its
|
|
4366
|
+
// default "warn" would put an interactive prompt in front of something this app has
|
|
4367
|
+
// always handled silently — "redact" keeps our UX and still takes the added coverage.
|
|
4368
|
+
//
|
|
4369
|
+
// The two redactors are COMPLEMENTARY, not duplicative, which is why we run both:
|
|
4370
|
+
// ours masks the configured provider keys by exact value (from env/config) plus the
|
|
4371
|
+
// provider-specific shapes (sk-/AIza/xai-/gsk_/csk-/vapi_/fw_/Z.ai, auth headers);
|
|
4372
|
+
// pi-privacy's catches what shows up in USER code and shell output — AWS AKIA/ASIA,
|
|
4373
|
+
// GitHub gh[pousr]_, JWTs, PEM private-key blocks, Slack, Stripe — none of which our
|
|
4374
|
+
// patterns match.
|
|
4375
|
+
//
|
|
4376
|
+
// Order between the two is NOT guaranteed: pi discovers extensions with a bare
|
|
4377
|
+
// readdirSync and never sorts, so it's filesystem-dependent (alphabetical on this box
|
|
4378
|
+
// today, not by contract). "redact" makes that moot — both handlers run
|
|
4379
|
+
// unconditionally and each masks its own patterns, so the surviving content is the
|
|
4380
|
+
// same either way. Under "warn" the order WOULD matter, since it decides whether the
|
|
4381
|
+
// prompt is raised on a raw key or one we already masked.
|
|
4382
|
+
toolResultPolicy: ambient.toolResultPolicy ?? "redact",
|
|
4383
|
+
// The operator's own "that is not personal data" list, read LIVE (see
|
|
4384
|
+
// ./piiAllow.ts): `/privacy allow @acme.com` applies on the next turn, not the next
|
|
4385
|
+
// launch. Ambient config entries come first — both lists are additive, since an
|
|
4386
|
+
// allowlist can only ever remove detection and there is no sense in which one of
|
|
4387
|
+
// them should silence the other.
|
|
4388
|
+
piiAllow: /* @__PURE__ */ __name(() => [...ambient.piiAllow ?? [], ...piiAllowEntries()], "piiAllow")
|
|
4389
|
+
};
|
|
4390
|
+
}
|
|
4391
|
+
__name(sharedPrivacyOptions, "sharedPrivacyOptions");
|
|
4392
|
+
function registerPrivacyCommand(pi) {
|
|
4393
|
+
const handler = /* @__PURE__ */ __name(async (args, ctx) => {
|
|
4394
|
+
const raw = String(args ?? "").trim();
|
|
4395
|
+
const [verb, ...rest] = raw.split(/\s+/);
|
|
4396
|
+
const value = rest.join(" ").trim();
|
|
4397
|
+
const notify = /* @__PURE__ */ __name((msg, level = "info") => ctx?.ui?.notify?.(msg, level), "notify");
|
|
4398
|
+
if (verb === "off" || verb === "disable") {
|
|
4399
|
+
setPrivacyDisabled(true);
|
|
4400
|
+
await updatePostureBadge(ctx);
|
|
4401
|
+
return notify(
|
|
4402
|
+
"\u2691 pi-privacy is completely OFF for this session. Outbound requests will not be scanned or gated for PII, and tool exfiltration, result, and downgrade guards are disabled. Run /privacy on to restore.",
|
|
4403
|
+
"warning"
|
|
4404
|
+
);
|
|
4405
|
+
}
|
|
4406
|
+
if (verb === "on" || verb === "enable" || verb === "restore") {
|
|
4407
|
+
setPrivacyDisabled(false);
|
|
4408
|
+
await updatePostureBadge(ctx);
|
|
4409
|
+
return notify(
|
|
4410
|
+
"pi-privacy is ON for this session. PII scanning, tool exfiltration guards, and privacy checks restored.",
|
|
4411
|
+
"info"
|
|
4412
|
+
);
|
|
4413
|
+
}
|
|
4414
|
+
if (verb === "status") {
|
|
4415
|
+
const state2 = privacyDisabled() ? "OFF (disabled for this session)" : "ON (active)";
|
|
4416
|
+
const mine2 = piiAllowEntries();
|
|
4417
|
+
return notify(
|
|
4418
|
+
[
|
|
4419
|
+
`pi-privacy status: ${state2}`,
|
|
4420
|
+
mine2.length ? `PII allowlist (~/.privateer/config.json):
|
|
4421
|
+
${mine2.join("\n ")}` : "PII allowlist: empty",
|
|
4422
|
+
"Reserved shapes (example.com, loopback, noreply@\u2026) are allowed by default.",
|
|
4423
|
+
"Commands: /privacy off | /privacy on | /privacy status | /privacy allow <value> | /privacy unallow <value>"
|
|
4424
|
+
].join("\n"),
|
|
4425
|
+
"info"
|
|
4426
|
+
);
|
|
4427
|
+
}
|
|
4428
|
+
if (verb === "allow" || verb === "unallow") {
|
|
4429
|
+
if (!value) return notify(`usage: /privacy ${verb} <value>`, "warning");
|
|
4430
|
+
const r = verb === "allow" ? addPiiAllow(value) : removePiiAllow(value);
|
|
4431
|
+
return notify(r.message, r.ok ? "info" : "warning");
|
|
4432
|
+
}
|
|
4433
|
+
if (verb && verb !== "help") {
|
|
4434
|
+
return notify(`unknown option "${verb}" \u2014 usage: /privacy [off | on | status | allow <value> | unallow <value>]`, "warning");
|
|
4435
|
+
}
|
|
4436
|
+
const state = privacyDisabled() ? "OFF (disabled for this session)" : "ON (active)";
|
|
4437
|
+
const mine = piiAllowEntries();
|
|
4438
|
+
notify(
|
|
4439
|
+
[
|
|
4440
|
+
`pi-privacy status: ${state}`,
|
|
4441
|
+
mine.length ? `PII allowlist (~/.privateer/config.json):
|
|
4442
|
+
${mine.join("\n ")}` : "PII allowlist: empty",
|
|
4443
|
+
"Reserved shapes (example.com, loopback, noreply@\u2026) are allowed by default and not listed here.",
|
|
4444
|
+
"Commands:",
|
|
4445
|
+
" /privacy off \u2014 turn off pi-privacy completely for this session",
|
|
4446
|
+
" /privacy on \u2014 restore pi-privacy protections",
|
|
4447
|
+
" /privacy status \u2014 show current status and allowlist",
|
|
4448
|
+
" /privacy allow <value> \u2014 add value to PII allowlist",
|
|
4449
|
+
" /privacy unallow <value> \u2014 remove value from PII allowlist"
|
|
4450
|
+
].join("\n"),
|
|
4451
|
+
"info"
|
|
4452
|
+
);
|
|
4453
|
+
}, "handler");
|
|
4454
|
+
const spec = {
|
|
4455
|
+
description: "Manage privacy protections: /privacy [off | on | status | allow <value> | unallow <value>]",
|
|
4456
|
+
handler,
|
|
4457
|
+
getArgumentCompletions: /* @__PURE__ */ __name((prefix) => {
|
|
4458
|
+
const p = prefix.trim().toLowerCase();
|
|
4459
|
+
const verbs = ["off", "on", "status", "allow", "unallow"];
|
|
4460
|
+
return verbs.filter((v) => v.startsWith(p)).map((v) => ({ value: v, label: v }));
|
|
4461
|
+
}, "getArgumentCompletions")
|
|
4462
|
+
};
|
|
4463
|
+
pi.registerCommand?.("privacy", spec);
|
|
4464
|
+
pi.registerCommand?.("pi-privacy", {
|
|
4465
|
+
...spec,
|
|
4466
|
+
description: "Alias for /privacy: /pi-privacy [off | on | status | allow <value> | unallow <value>]"
|
|
4467
|
+
});
|
|
4468
|
+
}
|
|
4469
|
+
__name(registerPrivacyCommand, "registerPrivacyCommand");
|
|
4470
|
+
function gatingPrivacyPi(pi) {
|
|
4471
|
+
const on = /* @__PURE__ */ __name((event, handler) => {
|
|
4472
|
+
if (typeof pi?.on !== "function") return;
|
|
4473
|
+
if (typeof handler !== "function") return pi.on(event, handler);
|
|
4474
|
+
if (event === "before_provider_request" || event === "tool_call" || event === "user_bash" || event === "tool_result" || event === "model_select") {
|
|
4475
|
+
return pi.on(event, async (ev, ctx) => {
|
|
4476
|
+
if (privacyDisabled()) return void 0;
|
|
4477
|
+
return handler(ev, ctx);
|
|
4478
|
+
});
|
|
4479
|
+
}
|
|
4480
|
+
return pi.on(event, handler);
|
|
4481
|
+
}, "on");
|
|
4482
|
+
const registerCommand = /* @__PURE__ */ __name((name, spec) => {
|
|
4483
|
+
if (typeof pi?.registerCommand !== "function") return;
|
|
4484
|
+
if (name === "pii" && spec && typeof spec.handler === "function") {
|
|
4485
|
+
const orig = spec.handler;
|
|
4486
|
+
const wrappedHandler = /* @__PURE__ */ __name(async (args, ctx) => {
|
|
4487
|
+
const raw = String(args ?? "").trim().toLowerCase();
|
|
4488
|
+
const [verb] = raw.split(/\s+/);
|
|
4489
|
+
if (verb === "off" || verb === "disable") {
|
|
4490
|
+
setPrivacyDisabled(true);
|
|
4491
|
+
await updatePostureBadge(ctx);
|
|
4492
|
+
} else if (verb === "on" || verb === "enable" || verb === "restore") {
|
|
4493
|
+
setPrivacyDisabled(false);
|
|
4494
|
+
await updatePostureBadge(ctx);
|
|
4495
|
+
}
|
|
4496
|
+
return orig(args, ctx);
|
|
4497
|
+
}, "wrappedHandler");
|
|
4498
|
+
return pi.registerCommand(name, { ...spec, handler: wrappedHandler });
|
|
4499
|
+
}
|
|
4500
|
+
return pi.registerCommand(name, spec);
|
|
4501
|
+
}, "registerCommand");
|
|
4502
|
+
return new Proxy(pi, {
|
|
4503
|
+
get(target, prop, receiver) {
|
|
4504
|
+
if (prop === "on") return on;
|
|
4505
|
+
if (prop === "registerCommand") return registerCommand;
|
|
4506
|
+
const value = Reflect.get(target, prop, receiver);
|
|
4507
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
4508
|
+
}
|
|
4509
|
+
});
|
|
4510
|
+
}
|
|
4511
|
+
__name(gatingPrivacyPi, "gatingPrivacyPi");
|
|
4512
|
+
function privacyExtension() {
|
|
4513
|
+
const privacy2 = makePiPrivacyExtension(sharedPrivacyOptions());
|
|
4514
|
+
return /* @__PURE__ */ __name(function privateerPrivacyCore(pi) {
|
|
4515
|
+
privacy2(gatingPrivacyPi(persistingModelPicks(pi)));
|
|
4516
|
+
registerPrivacyCommand(pi);
|
|
4517
|
+
}, "privateerPrivacyCore");
|
|
4518
|
+
}
|
|
4519
|
+
__name(privacyExtension, "privacyExtension");
|
|
4520
|
+
function persistingModelPicks(pi) {
|
|
4521
|
+
if (typeof pi?.setModel !== "function") return pi;
|
|
4522
|
+
const setModel = /* @__PURE__ */ __name(async (model) => {
|
|
4523
|
+
const ok = await pi.setModel(model);
|
|
4524
|
+
if (ok !== false && model?.provider && model?.id) writePiDefaultModel(`${model.provider}/${model.id}`);
|
|
4525
|
+
return ok;
|
|
4526
|
+
}, "setModel");
|
|
4527
|
+
return new Proxy(pi, {
|
|
4528
|
+
get(target, prop, receiver) {
|
|
4529
|
+
if (prop === "setModel") return setModel;
|
|
4530
|
+
const value = Reflect.get(target, prop, receiver);
|
|
4531
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
4532
|
+
}
|
|
4533
|
+
});
|
|
4534
|
+
}
|
|
4535
|
+
__name(persistingModelPicks, "persistingModelPicks");
|
|
4536
|
+
|
|
4537
|
+
// extensions/privateer-privacy.ts?ext=privateer-privacy
|
|
4538
|
+
var TINFOIL_MODELS = [
|
|
4539
|
+
"gemma4-31b",
|
|
4540
|
+
"kimi-k2-6",
|
|
4541
|
+
"glm-5-2",
|
|
4542
|
+
"deepseek-v4-pro",
|
|
4543
|
+
"gpt-oss-120b",
|
|
4544
|
+
"gpt-oss-safeguard-120b",
|
|
4545
|
+
"llama3-3-70b"
|
|
4546
|
+
];
|
|
4547
|
+
function tinfoilModel(id) {
|
|
4548
|
+
return {
|
|
4549
|
+
id,
|
|
4550
|
+
name: id,
|
|
4551
|
+
reasoning: false,
|
|
4552
|
+
// Tinfoil ids are bare here (`gemma4-31b`), so scope it before asking — the
|
|
4553
|
+
// allowlist is written against full `provider/model` ids. Getting this wrong is
|
|
4554
|
+
// not cosmetic: `input` is what Pi checks before it will send an image at all.
|
|
4555
|
+
input: visionInput(`tinfoil/${id}`),
|
|
4556
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
4557
|
+
contextWindow: 128e3,
|
|
4558
|
+
maxTokens: 4096
|
|
4559
|
+
};
|
|
4560
|
+
}
|
|
4561
|
+
__name(tinfoilModel, "tinfoilModel");
|
|
4562
|
+
var privacy = privacyExtension();
|
|
4563
|
+
function privateerPrivacy(pi) {
|
|
4564
|
+
privacy(pi);
|
|
4565
|
+
pi.registerProvider?.("tinfoil", {
|
|
4566
|
+
name: "Tinfoil (private TEE inference)",
|
|
4567
|
+
baseUrl: "https://inference.tinfoil.sh/v1",
|
|
4568
|
+
api: "openai-completions",
|
|
4569
|
+
apiKey: "${TINFOIL_API_KEY}",
|
|
4570
|
+
authHeader: true,
|
|
4571
|
+
models: TINFOIL_MODELS.map(tinfoilModel)
|
|
4572
|
+
});
|
|
4573
|
+
registerAccountModels(pi);
|
|
4574
|
+
}
|
|
4575
|
+
__name(privateerPrivacy, "privateerPrivacy");
|
|
4576
|
+
export {
|
|
4577
|
+
privateerPrivacy as default
|
|
4578
|
+
};
|