privateer-agent 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/channels/run.ts +11 -2
- package/src/cli/chat.ts +10 -2
- package/src/harbor/index.ts +45 -8
- package/src/harbor/ipc.ts +82 -34
- package/src/mcp/catalog.ts +106 -0
- package/src/providers/account.ts +57 -10
- package/src/providers/catalog.ts +8 -6
- package/src/providers/phala/aci-verifier/VENDORED.md +23 -0
- package/src/providers/phala/aci-verifier/crypto.ts +95 -0
- package/src/providers/phala/aci-verifier/digest.ts +116 -0
- package/src/providers/phala/aci-verifier/e2ee-channel.ts +242 -0
- package/src/providers/phala/aci-verifier/e2ee.ts +73 -0
- package/src/providers/phala/aci-verifier/errors.ts +41 -0
- package/src/providers/phala/aci-verifier/index.ts +87 -0
- package/src/providers/phala/aci-verifier/jcs.ts +69 -0
- package/src/providers/phala/aci-verifier/receipt.ts +139 -0
- package/src/providers/phala/aci-verifier/report.ts +126 -0
- package/src/providers/phala/aci-verifier/types.ts +139 -0
- package/src/providers/phala/sse.ts +43 -0
- package/src/providers/phala/webcrypto-globals.d.ts +17 -0
- package/src/providers/phalaSeal.ts +200 -0
- package/src/providers/sealedShim.ts +295 -0
- package/src/remote/liveTaskSession.ts +11 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Privateer — a provider-agnostic, safe-by-default terminal coding agent with TEE/Tinfoil attestation, rebuilt on the Pi toolkit. Bring your own model across 20 providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -67,12 +67,14 @@
|
|
|
67
67
|
"@noble/ciphers": "^2.1.1",
|
|
68
68
|
"@noble/curves": "^1.9.7",
|
|
69
69
|
"@noble/hashes": "^1.7.1",
|
|
70
|
+
"@phala/dcap-qvl": "^0.5.2",
|
|
70
71
|
"patch-package": "^8.0.1",
|
|
71
72
|
"pi-mcp-adapter": "^2.11.0",
|
|
72
|
-
"pi-privacy": "^0.
|
|
73
|
+
"pi-privacy": "^0.7.0",
|
|
73
74
|
"pi-subagents": "^0.34.0",
|
|
74
75
|
"picomatch": "^4.0.4",
|
|
75
76
|
"privateer-workflow": "^0.1.0",
|
|
77
|
+
"tinfoil": "^1.1.11",
|
|
76
78
|
"tsx": "^4.16.0",
|
|
77
79
|
"typebox": "^1.3.4",
|
|
78
80
|
"undici": "^7.28.0",
|
package/src/channels/run.ts
CHANGED
|
@@ -109,7 +109,8 @@ async function main() {
|
|
|
109
109
|
const { makePermissionGate } = await import("../ext/permissionGate.ts");
|
|
110
110
|
type GateController = import("../ext/permissionGate.ts").GateController;
|
|
111
111
|
const { makePiPrivacyExtension } = await import("pi-privacy");
|
|
112
|
-
const { makeAccountProvider } = await import("../providers/account.ts");
|
|
112
|
+
const { makeAccountProvider, privateerChannel } = await import("../providers/account.ts");
|
|
113
|
+
const { hasCredentials } = await import("../auth/privateer.ts");
|
|
113
114
|
const { resolveDefaultModel } = await import("../providers/defaultModel.ts");
|
|
114
115
|
const { agentDir, configPath, globalDir } = await import("../config/paths.ts");
|
|
115
116
|
const { redactText, collectSecrets } = await import("../util/redact.ts");
|
|
@@ -181,7 +182,15 @@ async function main() {
|
|
|
181
182
|
cwd,
|
|
182
183
|
agentDir: agentDir(),
|
|
183
184
|
resourceLoaderOptions: {
|
|
184
|
-
extensionFactories: [
|
|
185
|
+
extensionFactories: [
|
|
186
|
+
makePermissionGate(gate),
|
|
187
|
+
// Per-model verified-TEE label for the /models picker (see harbor/index.ts):
|
|
188
|
+
// TEE-channel Privateer models verify on select when logged in; ZDR stays floored.
|
|
189
|
+
makePiPrivacyExtension({
|
|
190
|
+
privateerVerifiedTee: (m) => hasCredentials() && privateerChannel(m.id ?? "") === "tee",
|
|
191
|
+
}),
|
|
192
|
+
makeAccountProvider(),
|
|
193
|
+
] as any,
|
|
185
194
|
},
|
|
186
195
|
});
|
|
187
196
|
|
package/src/cli/chat.ts
CHANGED
|
@@ -38,7 +38,7 @@ async function main() {
|
|
|
38
38
|
const { authorizeControl } = await import("../remote/controlAuth.ts");
|
|
39
39
|
const { resolveMentions, completeMention, searchFiles } = await import("../util/fileMentions.ts");
|
|
40
40
|
const priv = await import("../auth/privateer.ts");
|
|
41
|
-
const { makeAccountProvider, accountPosture } = await import("../providers/account.ts");
|
|
41
|
+
const { makeAccountProvider, accountPosture, privateerChannel } = await import("../providers/account.ts");
|
|
42
42
|
const { agentVersion } = await import("../config/version.ts");
|
|
43
43
|
const { resolveDefaultModel, resolveSignedInModel } = await import("../providers/defaultModel.ts");
|
|
44
44
|
|
|
@@ -329,7 +329,15 @@ async function main() {
|
|
|
329
329
|
agentDir: agentDir(),
|
|
330
330
|
resourceLoaderOptions: {
|
|
331
331
|
// Structural ext types are intentionally narrow; cast to Pi's ExtensionFactory.
|
|
332
|
-
extensionFactories: [
|
|
332
|
+
extensionFactories: [
|
|
333
|
+
makePermissionGate(gate),
|
|
334
|
+
// Per-model verified-TEE label for the /models picker (see harbor/index.ts):
|
|
335
|
+
// TEE-channel Privateer models verify on select when logged in; ZDR stays floored.
|
|
336
|
+
makePiPrivacyExtension({
|
|
337
|
+
privateerVerifiedTee: (m) => priv.hasCredentials() && privateerChannel(m.id ?? "") === "tee",
|
|
338
|
+
}),
|
|
339
|
+
makeAccountProvider(),
|
|
340
|
+
] as any,
|
|
333
341
|
},
|
|
334
342
|
});
|
|
335
343
|
for (const d of services.diagnostics) if (d.type === "error") console.log(`${RED}! ${d.message}${RESET}`);
|
package/src/harbor/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { agentVersion } from "../config/version.ts";
|
|
|
14
14
|
import { createEngineEventAdapter } from "../bridge/engineAdapter.ts";
|
|
15
15
|
import { makePermissionGate, type GateController } from "../ext/permissionGate.ts";
|
|
16
16
|
import { makePiPrivacyExtension } from "pi-privacy";
|
|
17
|
-
import { makeAccountProvider } from "../providers/account.ts";
|
|
17
|
+
import { makeAccountProvider, privateerChannel } from "../providers/account.ts";
|
|
18
18
|
import { resolveDefaultModel } from "../providers/defaultModel.ts";
|
|
19
19
|
import { RelayClient, type TaskSpec } from "../remote/relayClient.ts";
|
|
20
20
|
import { createLiveTaskSession, type LiveTaskHandle } from "../remote/liveTaskSession.ts";
|
|
@@ -30,7 +30,7 @@ import { openJsonFromApp } from "../crypto/terminalUnseal.ts";
|
|
|
30
30
|
import { verifyChannelSave, verifyOutboxKey } from "../crypto/accountVerify.ts";
|
|
31
31
|
import { loadAccountSignKey, loadLastControlTs, saveLastControlTs } from "../crypto/accountTrust.ts";
|
|
32
32
|
import { authorizeControl } from "../remote/controlAuth.ts";
|
|
33
|
-
import { hasCredentials, revokeLocalSessions, revokeAccountSession, apiRequest, acquireAccountCredential, handleServerRevoke } from "../auth/privateer.ts";
|
|
33
|
+
import { hasCredentials, revokeLocalSessions, revokeAccountSession, apiRequest, acquireAccountCredential, handleServerRevoke, defaultDeviceLabel } from "../auth/privateer.ts";
|
|
34
34
|
import {
|
|
35
35
|
loadRoutines,
|
|
36
36
|
upsertRoutine,
|
|
@@ -50,7 +50,7 @@ import { splitRoutineTools } from "../routines/toolSelect.ts";
|
|
|
50
50
|
import { deliver, type RelayPusher, type CloudPusher } from "../routines/delivery.ts";
|
|
51
51
|
import { sealJson, decodeAccountPublicKey } from "../crypto/outboxSeal.ts";
|
|
52
52
|
import { redactText, collectSecrets } from "../util/redact.ts";
|
|
53
|
-
import { startIpcServer, type IpcRequest, type IpcResponse } from "./ipc.ts";
|
|
53
|
+
import { startIpcServer, HarborAlreadyRunningError, type IpcRequest, type IpcResponse } from "./ipc.ts";
|
|
54
54
|
import { isHosted, publishRelayPub } from "../config/hosted.ts";
|
|
55
55
|
|
|
56
56
|
// The safe, read-only toolset for unattended runs — Pi builtins with no
|
|
@@ -226,13 +226,19 @@ export class Harbor {
|
|
|
226
226
|
return "queued";
|
|
227
227
|
};
|
|
228
228
|
|
|
229
|
-
start(): void {
|
|
229
|
+
async start(): Promise<void> {
|
|
230
|
+
// Single-instance lock FIRST, before any other side effect: binding the IPC
|
|
231
|
+
// socket is the machine's mutex. If a live harbor already holds it this throws
|
|
232
|
+
// HarborAlreadyRunningError — two harbors under one ~/.privateer share a single
|
|
233
|
+
// routineRelayId(), so a second would collide on the relay and double-fire
|
|
234
|
+
// routines. Doing this first means a rejected second instance never publishes a
|
|
235
|
+
// relay key, connects, or fires a tick.
|
|
236
|
+
this.server = await startIpcServer((req) => this.handleIpc(req));
|
|
230
237
|
// Hosted only: publish our relay pubkey for the host to bind into the SEV-SNP
|
|
231
238
|
// report. Before syncRelay() so the key exists by the time we're reachable.
|
|
232
239
|
publishRelayPub();
|
|
233
240
|
this.primeSchedule();
|
|
234
241
|
this.timer = setInterval(() => void this.tick(), TICK_MS);
|
|
235
|
-
this.server = startIpcServer((req) => this.handleIpc(req));
|
|
236
242
|
this.syncRelay();
|
|
237
243
|
void this.flushPendingCloud();
|
|
238
244
|
const count = loadRoutines().filter((r) => r.enabled).length;
|
|
@@ -517,11 +523,22 @@ export class Harbor {
|
|
|
517
523
|
}
|
|
518
524
|
}
|
|
519
525
|
|
|
526
|
+
// This machine's origin tag, embedded (E2EE) in every sealed result so the app can
|
|
527
|
+
// show WHICH box/environment produced it — the outbox record itself is account-only,
|
|
528
|
+
// so attribution can only live inside the sealed blob (where hostnames are allowed;
|
|
529
|
+
// the server never sees it). `id` is this install's stable relay id; `label` is the
|
|
530
|
+
// hostname-based device name. Cached — it never changes for the process lifetime.
|
|
531
|
+
private originCache?: { id: string; label: string };
|
|
532
|
+
private machineOrigin(): { id: string; label: string } {
|
|
533
|
+
if (!this.originCache) this.originCache = { id: routineRelayId(), label: defaultDeviceLabel() };
|
|
534
|
+
return this.originCache;
|
|
535
|
+
}
|
|
536
|
+
|
|
520
537
|
private async postOutbox(name: string, at: string, status: "ok" | "error", content: string, kind: "routine" | "task" = "routine"): Promise<boolean> {
|
|
521
538
|
const pub = await this.ensureOutboxPub();
|
|
522
539
|
if (!pub) return false;
|
|
523
540
|
const body = content.length > MAX_CLOUD_PLAINTEXT ? content.slice(0, MAX_CLOUD_PLAINTEXT) + "\n…truncated" : content;
|
|
524
|
-
const sealed = sealJson(pub, { v: 1, kind, name, status, at, content: body });
|
|
541
|
+
const sealed = sealJson(pub, { v: 1, kind, name, status, at, content: body, origin: this.machineOrigin() });
|
|
525
542
|
try {
|
|
526
543
|
const res = await apiRequest("/api/outbox", {
|
|
527
544
|
method: "POST",
|
|
@@ -727,7 +744,18 @@ export class Harbor {
|
|
|
727
744
|
cwd: spec.cwd,
|
|
728
745
|
agentDir: agentDir(),
|
|
729
746
|
resourceLoaderOptions: {
|
|
730
|
-
extensionFactories: [
|
|
747
|
+
extensionFactories: [
|
|
748
|
+
makePermissionGate(gate),
|
|
749
|
+
// Per-model verified-TEE capability for pi-privacy's /models picker: show
|
|
750
|
+
// Privateer's TEE-channel models (near/tinfoil/phala) as "◆ Verifiable TEE"
|
|
751
|
+
// when logged in; ZDR-channel models stay at their honest floor. The live
|
|
752
|
+
// verdict still comes from accountPosture on select — this only lifts the label.
|
|
753
|
+
makePiPrivacyExtension({
|
|
754
|
+
privateerVerifiedTee: (m) => hasCredentials() && privateerChannel(m.id ?? "") === "tee",
|
|
755
|
+
}),
|
|
756
|
+
makeAccountProvider(),
|
|
757
|
+
mcpAdapter,
|
|
758
|
+
] as any,
|
|
731
759
|
},
|
|
732
760
|
});
|
|
733
761
|
servicesRef = services as any;
|
|
@@ -1034,7 +1062,6 @@ export class Harbor {
|
|
|
1034
1062
|
// Entry point for `privateer harbor`. Caller must have imported ./boot.ts first.
|
|
1035
1063
|
export function runHarbor(): void {
|
|
1036
1064
|
const harbor = new Harbor();
|
|
1037
|
-
harbor.start();
|
|
1038
1065
|
const shutdown = () => {
|
|
1039
1066
|
log("shutting down");
|
|
1040
1067
|
harbor.stop();
|
|
@@ -1042,4 +1069,14 @@ export function runHarbor(): void {
|
|
|
1042
1069
|
};
|
|
1043
1070
|
process.on("SIGINT", shutdown);
|
|
1044
1071
|
process.on("SIGTERM", shutdown);
|
|
1072
|
+
harbor.start().catch((err) => {
|
|
1073
|
+
if (err instanceof HarborAlreadyRunningError) {
|
|
1074
|
+
// A resident harbor already owns this machine — leave it in charge. Exit 0 so a
|
|
1075
|
+
// manual `privateer harbor run` beside the installed login service isn't an error.
|
|
1076
|
+
process.stderr.write("A Harbor is already running on this machine — leaving the existing one in charge.\n");
|
|
1077
|
+
process.exit(0);
|
|
1078
|
+
}
|
|
1079
|
+
process.stderr.write(`Harbor failed to start: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1080
|
+
process.exit(1);
|
|
1081
|
+
});
|
|
1045
1082
|
}
|
package/src/harbor/ipc.ts
CHANGED
|
@@ -33,44 +33,82 @@ export interface IpcResponse {
|
|
|
33
33
|
|
|
34
34
|
export type IpcHandler = (req: IpcRequest) => Promise<IpcResponse> | IpcResponse;
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
|
|
36
|
+
// Probe whether a LIVE process is listening on the socket at `path`. Used as the
|
|
37
|
+
// single-instance test: a successful connect (or a slow-to-answer one) means a real
|
|
38
|
+
// harbor holds the lock; ECONNREFUSED/ENOENT means the socket file is stale (no
|
|
39
|
+
// listener behind it) and is safe to reclaim. Conservative — any ambiguous error
|
|
40
|
+
// resolves `true` so we never steal a path that might still be owned.
|
|
41
|
+
function probeExistingListener(path: string, timeoutMs = 1000): Promise<boolean> {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const sock = createConnection(path);
|
|
44
|
+
const done = (live: boolean) => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
try { sock.destroy(); } catch { /* already gone */ }
|
|
47
|
+
resolve(live);
|
|
48
|
+
};
|
|
49
|
+
const timer = setTimeout(() => done(true), timeoutMs); // slow to answer ⇒ assume live
|
|
50
|
+
sock.on("connect", () => done(true));
|
|
51
|
+
sock.on("error", (err: NodeJS.ErrnoException) => {
|
|
52
|
+
done(!(err.code === "ECONNREFUSED" || err.code === "ENOENT"));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Start the harbor-side socket server. Resolves with the Server (so the caller can
|
|
58
|
+
// close it), or REJECTS with HarborAlreadyRunningError if a live harbor already owns
|
|
59
|
+
// the socket — the bind is the machine's single-instance lock. Two harbors under one
|
|
60
|
+
// ~/.privateer share a single routineRelayId(), so a second instance would collide on
|
|
61
|
+
// the relay and double-fire routines; refusing to start is the fix. A stale socket
|
|
62
|
+
// file (crash with no live listener) is detected and reclaimed, so recovery still works.
|
|
63
|
+
export function startIpcServer(handler: IpcHandler): Promise<Server> {
|
|
38
64
|
const path = harborSocketPath();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
const build = (): Server =>
|
|
66
|
+
createServer((sock: Socket) => {
|
|
67
|
+
let buf = "";
|
|
68
|
+
sock.on("data", (chunk) => {
|
|
69
|
+
buf += chunk.toString("utf8");
|
|
70
|
+
const nl = buf.indexOf("\n");
|
|
71
|
+
if (nl < 0) return; // wait for the full line
|
|
72
|
+
const line = buf.slice(0, nl);
|
|
73
|
+
void (async () => {
|
|
74
|
+
let res: IpcResponse;
|
|
75
|
+
try {
|
|
76
|
+
res = await handler(JSON.parse(line) as IpcRequest);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
res = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
79
|
+
}
|
|
80
|
+
sock.end(JSON.stringify(res) + "\n");
|
|
81
|
+
})();
|
|
82
|
+
});
|
|
83
|
+
sock.on("error", () => sock.destroy());
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return new Promise<Server>((resolve, reject) => {
|
|
87
|
+
// `reclaimed` guards a single stale-socket reclaim so a persistent bind failure
|
|
88
|
+
// can't loop. On EADDRINUSE we probe for a live listener rather than unlinking
|
|
89
|
+
// blindly (the old behavior, which let a second harbor silently steal the path).
|
|
90
|
+
const attempt = (reclaimed: boolean) => {
|
|
91
|
+
const server = build();
|
|
92
|
+
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
93
|
+
if (err.code !== "EADDRINUSE") { reject(err); return; }
|
|
94
|
+
void probeExistingListener(path).then((live) => {
|
|
95
|
+
if (live) { reject(new HarborAlreadyRunningError()); return; }
|
|
96
|
+
if (reclaimed) { reject(err); return; } // already reclaimed once — give up
|
|
97
|
+
try { unlinkSync(path); } catch { /* ignore — retry surfaces a clearer error */ }
|
|
98
|
+
attempt(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
server.listen(path, () => {
|
|
56
102
|
try {
|
|
57
|
-
|
|
58
|
-
} catch
|
|
59
|
-
|
|
103
|
+
chmodSync(path, 0o600); // owner-only IPC endpoint
|
|
104
|
+
} catch {
|
|
105
|
+
/* non-POSIX — best effort */
|
|
60
106
|
}
|
|
61
|
-
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
});
|
|
66
|
-
server.listen(path, () => {
|
|
67
|
-
try {
|
|
68
|
-
chmodSync(path, 0o600); // owner-only IPC endpoint
|
|
69
|
-
} catch {
|
|
70
|
-
/* non-POSIX — best effort */
|
|
71
|
-
}
|
|
107
|
+
resolve(server);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
attempt(false);
|
|
72
111
|
});
|
|
73
|
-
return server;
|
|
74
112
|
}
|
|
75
113
|
|
|
76
114
|
// Client side: send one request, resolve with the response. Rejects if the harbor
|
|
@@ -116,6 +154,16 @@ export class HarborNotRunningError extends Error {
|
|
|
116
154
|
}
|
|
117
155
|
}
|
|
118
156
|
|
|
157
|
+
// Thrown by startIpcServer when a live harbor already holds this machine's socket —
|
|
158
|
+
// i.e. a second instance is trying to start under the same ~/.privateer. The caller
|
|
159
|
+
// (runHarbor) treats this as a clean no-op exit, not a crash.
|
|
160
|
+
export class HarborAlreadyRunningError extends Error {
|
|
161
|
+
constructor() {
|
|
162
|
+
super("A Harbor is already running on this machine.");
|
|
163
|
+
this.name = "HarborAlreadyRunningError";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
119
167
|
// Convenience: is the harbor reachable right now?
|
|
120
168
|
export async function harborIsRunning(): Promise<boolean> {
|
|
121
169
|
try {
|
package/src/mcp/catalog.ts
CHANGED
|
@@ -130,6 +130,112 @@ export const MCP_CATALOG: CatalogEntry[] = [
|
|
|
130
130
|
args: ["-y", "@modelcontextprotocol/server-memory"],
|
|
131
131
|
needs: "none",
|
|
132
132
|
},
|
|
133
|
+
|
|
134
|
+
// ── Remote, browser-authorized (OAuth) ──────────────────────────────────────
|
|
135
|
+
{
|
|
136
|
+
id: "sentry",
|
|
137
|
+
name: "sentry",
|
|
138
|
+
label: "Sentry",
|
|
139
|
+
blurb: "Errors, issues, and releases. Sign in via browser.",
|
|
140
|
+
transport: "http",
|
|
141
|
+
url: "https://mcp.sentry.dev/mcp",
|
|
142
|
+
oauth: true,
|
|
143
|
+
needs: "oauth",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "atlassian",
|
|
147
|
+
name: "atlassian",
|
|
148
|
+
label: "Jira & Confluence",
|
|
149
|
+
blurb: "Atlassian issues and pages. Sign in via browser.",
|
|
150
|
+
transport: "http",
|
|
151
|
+
url: "https://mcp.atlassian.com/v1/sse",
|
|
152
|
+
oauth: true,
|
|
153
|
+
needs: "oauth",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "stripe",
|
|
157
|
+
name: "stripe",
|
|
158
|
+
label: "Stripe",
|
|
159
|
+
blurb: "Payments, customers, and invoices. Sign in via browser.",
|
|
160
|
+
transport: "http",
|
|
161
|
+
url: "https://mcp.stripe.com",
|
|
162
|
+
oauth: true,
|
|
163
|
+
needs: "oauth",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "asana",
|
|
167
|
+
name: "asana",
|
|
168
|
+
label: "Asana",
|
|
169
|
+
blurb: "Tasks and projects. Sign in via browser.",
|
|
170
|
+
transport: "http",
|
|
171
|
+
url: "https://mcp.asana.com/sse",
|
|
172
|
+
oauth: true,
|
|
173
|
+
needs: "oauth",
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// ── Local, token-authorized ─────────────────────────────────────────────────
|
|
177
|
+
{
|
|
178
|
+
id: "brave-search",
|
|
179
|
+
name: "brave-search",
|
|
180
|
+
label: "Brave Search",
|
|
181
|
+
blurb: "Web and local search results.",
|
|
182
|
+
transport: "stdio",
|
|
183
|
+
command: "npx",
|
|
184
|
+
args: ["-y", "@modelcontextprotocol/server-brave-search"],
|
|
185
|
+
env: { BRAVE_API_KEY: "" },
|
|
186
|
+
needs: "token",
|
|
187
|
+
fill: "BRAVE_API_KEY",
|
|
188
|
+
credUrl: "https://brave.com/search/api/",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "google-maps",
|
|
192
|
+
name: "google-maps",
|
|
193
|
+
label: "Google Maps",
|
|
194
|
+
blurb: "Places, directions, and geocoding.",
|
|
195
|
+
transport: "stdio",
|
|
196
|
+
command: "npx",
|
|
197
|
+
args: ["-y", "@modelcontextprotocol/server-google-maps"],
|
|
198
|
+
env: { GOOGLE_MAPS_API_KEY: "" },
|
|
199
|
+
needs: "token",
|
|
200
|
+
fill: "GOOGLE_MAPS_API_KEY",
|
|
201
|
+
credUrl: "https://console.cloud.google.com/google/maps-apis/credentials",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "supabase",
|
|
205
|
+
name: "supabase",
|
|
206
|
+
label: "Supabase",
|
|
207
|
+
blurb: "Query and manage your Supabase project.",
|
|
208
|
+
transport: "stdio",
|
|
209
|
+
command: "npx",
|
|
210
|
+
args: ["-y", "@supabase/mcp-server-supabase@latest"],
|
|
211
|
+
env: { SUPABASE_ACCESS_TOKEN: "" },
|
|
212
|
+
needs: "token",
|
|
213
|
+
fill: "SUPABASE_ACCESS_TOKEN",
|
|
214
|
+
credUrl: "https://supabase.com/dashboard/account/tokens",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: "figma",
|
|
218
|
+
name: "figma",
|
|
219
|
+
label: "Figma",
|
|
220
|
+
blurb: "Read designs, frames, and components.",
|
|
221
|
+
transport: "stdio",
|
|
222
|
+
command: "npx",
|
|
223
|
+
args: ["-y", "figma-developer-mcp", "--stdio"],
|
|
224
|
+
env: { FIGMA_API_KEY: "" },
|
|
225
|
+
needs: "token",
|
|
226
|
+
fill: "FIGMA_API_KEY",
|
|
227
|
+
credUrl: "https://www.figma.com/developers/api#access-tokens",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: "sequential-thinking",
|
|
231
|
+
name: "sequential-thinking",
|
|
232
|
+
label: "Sequential Thinking",
|
|
233
|
+
blurb: "A step-by-step reasoning scratchpad.",
|
|
234
|
+
transport: "stdio",
|
|
235
|
+
command: "npx",
|
|
236
|
+
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
|
237
|
+
needs: "none",
|
|
238
|
+
},
|
|
133
239
|
];
|
|
134
240
|
|
|
135
241
|
export function catalogEntry(id: string): CatalogEntry | undefined {
|
package/src/providers/account.ts
CHANGED
|
@@ -22,6 +22,13 @@ import {
|
|
|
22
22
|
} from "../auth/privateer.ts";
|
|
23
23
|
import { interpretReport, teePosture, tierFromTeePosture, type PrivacyTier } from "pi-privacy";
|
|
24
24
|
import { ACCOUNT_DEFAULT_MODEL_ID, ACCOUNT_NEAR_MODEL_ID, ensurePiDefaultModel } from "./defaultModel.ts";
|
|
25
|
+
import {
|
|
26
|
+
sealedEnabled,
|
|
27
|
+
sealedProviderFor,
|
|
28
|
+
sealedShimBase,
|
|
29
|
+
ensureSealedShim,
|
|
30
|
+
attestSealed,
|
|
31
|
+
} from "./sealedShim.ts";
|
|
25
32
|
|
|
26
33
|
// Seed/fallback catalog: registered synchronously so the account provider has real
|
|
27
34
|
// models the instant it loads (before the live /api/models fetch resolves) — in
|
|
@@ -73,7 +80,9 @@ const VALID_TIERS = new Set<PrivacyTier>([
|
|
|
73
80
|
// upgrades it to tee-verified live via attestation (accountPosture). Everything
|
|
74
81
|
// else with no server signal is "standard": we don't assert ZDR we can't back.
|
|
75
82
|
function tierFromPrefix(modelId: string): PrivacyTier {
|
|
76
|
-
return modelId.startsWith("near/") || modelId.startsWith("tinfoil/")
|
|
83
|
+
return modelId.startsWith("near/") || modelId.startsWith("tinfoil/") || modelId.startsWith("phala/")
|
|
84
|
+
? "tee-unverified"
|
|
85
|
+
: "standard";
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
function normalizeTier(tier: string | undefined, modelId: string): PrivacyTier {
|
|
@@ -248,13 +257,26 @@ export async function accountPosture(modelId: string): Promise<AccountPosture> {
|
|
|
248
257
|
if (privateerChannel(modelId) === "zdr") {
|
|
249
258
|
return { tier: "zdr-policy" };
|
|
250
259
|
}
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// that
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
260
|
+
// Sealed (EHBP) path. When sealed mode is on and the model has a Node sealed
|
|
261
|
+
// client (tinfoil/*), inference goes through the blind relay with the body
|
|
262
|
+
// HPKE-sealed to the enclave, and we attest that enclave client-side with the SAME
|
|
263
|
+
// SecureClient that carries the tokens. A green ready() is a quote WE checked,
|
|
264
|
+
// bound to the HPKE key we seal to — so it earns tee-verified. A failure stays
|
|
265
|
+
// tee-unverified with the reason surfaced (never a silent green). See
|
|
266
|
+
// docs/tee-privateer-tinfoil-ehbp.md.
|
|
267
|
+
const sealedProvider = sealedEnabled() ? sealedProviderFor(modelId) : null;
|
|
268
|
+
if (sealedProvider) {
|
|
269
|
+
const att = await attestSealed(sealedProvider);
|
|
270
|
+
return att.ok ? { tier: "tee-verified" } : { tier: "tee-unverified", error: att.error };
|
|
271
|
+
}
|
|
272
|
+
// Honest labelling for the non-NEAR enclaves without sealed mode. Tinfoil and Phala
|
|
273
|
+
// publish real attestations, but the server proxies the inference in cleartext, so
|
|
274
|
+
// from here we cannot bind a quote to the connection actually carrying our tokens —
|
|
275
|
+
// only the account's word that it did. That's `tee-unverified` (yellow "confidential
|
|
276
|
+
// compute, unconfirmed"), never the green tee-verified we reserve for a quote we
|
|
277
|
+
// checked ourselves. Turn on sealed mode (PRIVATEER_SEALED=1) for the verified
|
|
278
|
+
// shield, or set TINFOIL_API_KEY and run `tinfoil/*` direct (pi-privacy attests
|
|
279
|
+
// client-side over the TLS binding).
|
|
258
280
|
if (!modelId.startsWith("near/")) {
|
|
259
281
|
return { tier: "tee-unverified" };
|
|
260
282
|
}
|
|
@@ -299,15 +321,40 @@ export function makeAccountProvider() {
|
|
|
299
321
|
on?: (event: string, handler: (e: unknown, ctx: unknown) => void) => void;
|
|
300
322
|
}): void => {
|
|
301
323
|
if (typeof pi.registerProvider !== "function") return;
|
|
302
|
-
|
|
324
|
+
// A model entry, with a per-model baseUrl override for sealed models once the
|
|
325
|
+
// EHBP shim is listening: `tinfoil/*` then route through the loopback shim (which
|
|
326
|
+
// seals to the blind relay) instead of the cleartext `/api/agent/v1` proxy.
|
|
327
|
+
// Everything else keeps the provider baseUrl below. Until the shim is up (or when
|
|
328
|
+
// sealed mode is off) sealed models fall back to the cleartext path — and the
|
|
329
|
+
// badge stays honestly `tee-unverified` (see accountPosture).
|
|
330
|
+
const modelEntry = (id: string) => {
|
|
331
|
+
const base = seedModel(id);
|
|
332
|
+
const provider = sealedEnabled() ? sealedProviderFor(id) : null;
|
|
333
|
+
const shim = sealedShimBase();
|
|
334
|
+
return provider && shim ? { ...base, baseUrl: `${shim}/${provider}/v1` } : base;
|
|
335
|
+
};
|
|
336
|
+
let lastIds: string[] = DEFAULT_MODELS;
|
|
337
|
+
const register = (ids: string[]): void => {
|
|
338
|
+
lastIds = ids;
|
|
303
339
|
pi.registerProvider!("privateer", {
|
|
304
340
|
name: "Privateer account",
|
|
305
341
|
baseUrl: `${serverBaseUrl()}/api/agent/v1`,
|
|
306
342
|
api: "openai-completions",
|
|
307
343
|
oauth: privateerOAuthProvider,
|
|
308
|
-
models: ids.map(
|
|
344
|
+
models: ids.map(modelEntry),
|
|
309
345
|
});
|
|
346
|
+
};
|
|
310
347
|
register(DEFAULT_MODELS); // immediate: provider exists this tick
|
|
348
|
+
// Bring up the sealed shim, then re-register so sealed models pick up their shim
|
|
349
|
+
// baseUrl. Registration re-runs anyway after the catalog fetch; this just makes
|
|
350
|
+
// sure the switch lands even if the fetch is slow or fails.
|
|
351
|
+
if (sealedEnabled()) {
|
|
352
|
+
void ensureSealedShim()
|
|
353
|
+
.then(() => register(lastIds))
|
|
354
|
+
.catch(() => {
|
|
355
|
+
/* shim failed to start → sealed models stay on the cleartext path */
|
|
356
|
+
});
|
|
357
|
+
}
|
|
311
358
|
// Refine to the live catalog. fetchAccountCatalog also populates accountTierMap
|
|
312
359
|
// as a side effect, so the /models picker can shield each row without re-fetching.
|
|
313
360
|
void fetchAccountCatalog()
|
package/src/providers/catalog.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// becomes reachable under Pi. Written after verifying (2026-07-07) that:
|
|
3
3
|
// - pi-ai ships STATIC model catalogs for its 14 built-in providers, so they're
|
|
4
4
|
// selectable once a key is present — privateer emits NO models.json entry for them;
|
|
5
|
-
// - the `pi-privacy` extension registers the
|
|
6
|
-
// venice, ollama,
|
|
7
|
-
// them either;
|
|
5
|
+
// - the `pi-privacy` extension registers the privacy providers (tinfoil, nearai,
|
|
6
|
+
// venice, ollama, and — since pi-privacy 0.7 — `privateer`, its posture-aware
|
|
7
|
+
// public dev-key surface) at load — privateer emits NO entry for them either;
|
|
8
8
|
// - so the ONLY provider this generator must emit is `qwen` (config-only,
|
|
9
9
|
// non-privacy, no built-in catalog), and `privateer` (the account OAuth channel)
|
|
10
10
|
// is handled in code, not config (Phase 4).
|
|
@@ -66,9 +66,11 @@ export const PROVIDERS: ProviderEntry[] = [
|
|
|
66
66
|
{ id: "tinfoil", source: "pi-privacy" },
|
|
67
67
|
{ id: "venice", source: "pi-privacy" },
|
|
68
68
|
{ id: "custom", source: "pi-privacy" },
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
69
|
+
// The in-app account OAuth channel. NOTE: since pi-privacy 0.7 the extension ALSO
|
|
70
|
+
// registers a `privateer` provider (its posture-aware public sk-priv- dev-key surface,
|
|
71
|
+
// floored to zdr-policy) — but makeAccountProvider runs AFTER pi-privacy in the
|
|
72
|
+
// extension list and re-registers the same id, so the account channel wins. There is
|
|
73
|
+
// no longer a separate `privateer-api` id (renamed to `privateer` upstream).
|
|
72
74
|
{ id: "privateer", source: "account" },
|
|
73
75
|
];
|
|
74
76
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Vendored: `@dstack/aci-verifier`
|
|
2
|
+
|
|
3
|
+
Faithful copy of the zero-dependency TypeScript ACI verifier from
|
|
4
|
+
[Dstack-TEE/private-ai-gateway](https://github.com/Dstack-TEE/private-ai-gateway)
|
|
5
|
+
(`clients/verifier-ts/src`), Apache-2.0. It is `private: true` upstream (not on
|
|
6
|
+
npm), so it is vendored here rather than installed.
|
|
7
|
+
|
|
8
|
+
Provides the pieces `PhalaProvider` needs:
|
|
9
|
+
- **`verifyReportBinding`** (`report.ts`) — §10.1 checks 2–6 (crypto binding of the
|
|
10
|
+
attestation report to the attested keyset for a supplied nonce). NOT the hardware
|
|
11
|
+
TDX quote (check 1) — that is layered on with `@phala/dcap-qvl` in the provider.
|
|
12
|
+
- **`openE2eeChannel`** (`e2ee-channel.ts`) — the ACI E2EE channel:
|
|
13
|
+
`x25519-aes-256-gcm-hkdf-sha256`, per-field seal/open, `X-E2EE-*` headers.
|
|
14
|
+
|
|
15
|
+
## Local adaptation (the only change from upstream)
|
|
16
|
+
- Relative import specifiers had their `.js` extension stripped (`'./jcs.js'` →
|
|
17
|
+
`'./jcs'`) so Metro + TS (`moduleResolution: bundler`) resolve to the `.ts` files.
|
|
18
|
+
|
|
19
|
+
Everything else is byte-for-byte upstream. The crypto runs on `globalThis.crypto`
|
|
20
|
+
(Web Crypto: X25519, HKDF, AES-GCM, Ed25519, `getRandomValues`). In privateer-agent
|
|
21
|
+
(Node ≥ 22) these are all native — **no polyfills needed** (unlike the treeview RN app,
|
|
22
|
+
which bridges them via `react-native-quick-crypto`). Re-pull from upstream to update;
|
|
23
|
+
re-apply only the `.js`-extension strip.
|