privateer-agent 0.6.10 → 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/README.md +20 -20
- package/SECURITY.md +1 -1
- package/bin/{privateer-daemon.mjs → privateer-harbor.mjs} +7 -7
- package/bin/privateer-launch.mjs +7 -6
- package/bin/privateer-subagent.mjs +1 -1
- package/extensions/privateer-brand.ts +1 -1
- package/extensions/privateer-connect.ts +2 -2
- package/extensions/privateer-tools.ts +1 -1
- package/package.json +4 -2
- package/src/auth/privateer.ts +2 -2
- package/src/channels/run.ts +16 -7
- package/src/channels/status.ts +7 -7
- package/src/cli/chat.ts +39 -5
- package/src/cli/{daemonCli.ts → harborCli.ts} +14 -14
- package/src/config/hosted.ts +5 -5
- package/src/crypto/accountTrust.ts +2 -2
- package/src/crypto/accountVerify.ts +1 -1
- package/src/{daemon → harbor}/index.ts +81 -44
- package/src/harbor/ipc.ts +175 -0
- package/src/{daemon → harbor}/service.ts +61 -28
- package/src/main.ts +1 -1
- package/src/mcp/catalog.ts +106 -0
- package/src/providers/account.ts +58 -11
- package/src/providers/catalog.ts +8 -6
- package/src/providers/defaultModel.ts +1 -1
- 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/channelsControl.ts +8 -8
- package/src/remote/controlAuth.ts +1 -1
- package/src/remote/liveTaskSession.ts +15 -7
- package/src/remote/mcpControl.ts +2 -2
- package/src/remote/relayClient.ts +41 -21
- package/src/remote/remoteBridge.ts +17 -7
- package/src/remote/routinesControl.ts +7 -7
- package/src/remote/workflowsControl.ts +6 -6
- package/src/routines/delivery.ts +6 -6
- package/src/routines/schema.ts +3 -3
- package/src/routines/store.ts +3 -3
- package/src/routines/trigger.ts +1 -1
- package/src/tools/routine.ts +8 -8
- package/src/util/fileMentions.ts +232 -0
- package/src/workflows/expr.ts +1 -1
- package/src/workflows/runner.ts +3 -3
- package/src/workflows/schema.ts +1 -1
- package/src/workflows/store.ts +1 -1
- package/src/daemon/ipc.ts +0 -127
|
@@ -1,28 +1,35 @@
|
|
|
1
|
-
// Install the resident
|
|
1
|
+
// Install the resident harbor as a per-user OS service so it auto-starts at login
|
|
2
2
|
// and survives the terminal closing — the difference between "the CLI is running"
|
|
3
|
-
// and "the
|
|
3
|
+
// and "the harbor is reachable from the app even when no CLI is". macOS → launchd
|
|
4
4
|
// user agent; Linux → systemd --user unit. No root: everything lives under the
|
|
5
5
|
// user's own home and login session.
|
|
6
6
|
//
|
|
7
7
|
// ORDERING NOTE: this module is import-safe (node builtins + our paths only, no Pi),
|
|
8
|
-
// so the
|
|
8
|
+
// so the harbor CLI can load it without going through boot.ts.
|
|
9
9
|
import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from "node:fs";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join, dirname, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { globalDir } from "../config/paths.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { harborIsRunning } from "./ipc.ts";
|
|
16
16
|
|
|
17
|
-
const LABEL = "pro.privateer.
|
|
18
|
-
const UNIT = "privateer-
|
|
17
|
+
const LABEL = "pro.privateer.harbor"; // launchd label / reverse-dns id
|
|
18
|
+
const UNIT = "privateer-harbor.service"; // systemd --user unit name
|
|
19
19
|
|
|
20
|
-
//
|
|
20
|
+
// Pre-rename service identity ("daemon"). Kept ONLY so install/uninstall can evict a
|
|
21
|
+
// service a user installed before the harbor rename — otherwise it lingers as an
|
|
22
|
+
// orphaned launchd agent / systemd unit still running the old launcher. Never written,
|
|
23
|
+
// only torn down.
|
|
24
|
+
const OLD_LABEL = "pro.privateer.daemon";
|
|
25
|
+
const OLD_UNIT = "privateer-daemon.service";
|
|
26
|
+
|
|
27
|
+
// Absolute path to the node launcher that boots + runs the harbor (bin/privateer-harbor.mjs).
|
|
21
28
|
// Resolved from THIS module so it's correct for both a dev checkout and a global npm
|
|
22
|
-
// install (…/node_modules/privateer-agent/bin/privateer-
|
|
23
|
-
function
|
|
24
|
-
const here = dirname(fileURLToPath(import.meta.url)); // …/src/
|
|
25
|
-
return resolve(here, "../../bin/privateer-
|
|
29
|
+
// install (…/node_modules/privateer-agent/bin/privateer-harbor.mjs).
|
|
30
|
+
function harborLauncherPath(): string {
|
|
31
|
+
const here = dirname(fileURLToPath(import.meta.url)); // …/src/harbor
|
|
32
|
+
return resolve(here, "../../bin/privateer-harbor.mjs");
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
// The node binary to bake into the unit. We use the CURRENT interpreter (>=22, the
|
|
@@ -32,12 +39,12 @@ function nodeBinaryPath(): string {
|
|
|
32
39
|
return process.execPath;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
function
|
|
36
|
-
return join(globalDir(), "
|
|
42
|
+
function harborLogPath(): string {
|
|
43
|
+
return join(globalDir(), "harbor.log");
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
// Env we forward into the service so a non-default home / server URL survives. Kept
|
|
40
|
-
// tiny and explicit — the
|
|
47
|
+
// tiny and explicit — the harbor reads the rest from ~/.privateer.
|
|
41
48
|
function forwardedEnv(): Record<string, string> {
|
|
42
49
|
const env: Record<string, string> = {};
|
|
43
50
|
if (process.env.PRIVATEER_HOME) env.PRIVATEER_HOME = process.env.PRIVATEER_HOME;
|
|
@@ -56,13 +63,13 @@ function xmlEscape(s: string): string {
|
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
function launchdPlist(): string {
|
|
59
|
-
const args = [nodeBinaryPath(),
|
|
66
|
+
const args = [nodeBinaryPath(), harborLauncherPath(), "run"];
|
|
60
67
|
const envVars = forwardedEnv();
|
|
61
68
|
const argXml = args.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
|
|
62
69
|
const envXml = Object.entries(envVars)
|
|
63
70
|
.map(([k, v]) => ` <key>${xmlEscape(k)}</key>\n <string>${xmlEscape(v)}</string>`)
|
|
64
71
|
.join("\n");
|
|
65
|
-
const log = xmlEscape(
|
|
72
|
+
const log = xmlEscape(harborLogPath());
|
|
66
73
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
67
74
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
68
75
|
<plist version="1.0">
|
|
@@ -86,7 +93,18 @@ ${envVars.PRIVATEER_HOME || envVars.PRIVATEER_SERVER_URL ? ` <key>EnvironmentVa
|
|
|
86
93
|
`;
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
// Evict a pre-rename launchd agent (pro.privateer.daemon) if one is installed, so the
|
|
97
|
+
// harbor rename doesn't leave the old service running the old launcher alongside it.
|
|
98
|
+
function evictOldLaunchd(): void {
|
|
99
|
+
const oldPlist = join(homedir(), "Library", "LaunchAgents", `${OLD_LABEL}.plist`);
|
|
100
|
+
if (existsSync(oldPlist)) {
|
|
101
|
+
spawnSync("launchctl", ["unload", "-w", oldPlist], { stdio: "ignore" });
|
|
102
|
+
rmSync(oldPlist, { force: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
function installLaunchd(): void {
|
|
107
|
+
evictOldLaunchd();
|
|
90
108
|
const plist = launchAgentPath();
|
|
91
109
|
mkdirSync(dirname(plist), { recursive: true });
|
|
92
110
|
writeFileSync(plist, launchdPlist());
|
|
@@ -100,6 +118,7 @@ function installLaunchd(): void {
|
|
|
100
118
|
}
|
|
101
119
|
|
|
102
120
|
function uninstallLaunchd(): void {
|
|
121
|
+
evictOldLaunchd();
|
|
103
122
|
const plist = launchAgentPath();
|
|
104
123
|
if (existsSync(plist)) {
|
|
105
124
|
spawnSync("launchctl", ["unload", "-w", plist], { stdio: "ignore" });
|
|
@@ -115,12 +134,12 @@ function systemdUnitPath(): string {
|
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
function systemdUnit(): string {
|
|
118
|
-
const exec = [nodeBinaryPath(),
|
|
137
|
+
const exec = [nodeBinaryPath(), harborLauncherPath(), "run"].map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
|
|
119
138
|
const envLines = Object.entries(forwardedEnv())
|
|
120
139
|
.map(([k, v]) => `Environment=${k}=${v}`)
|
|
121
140
|
.join("\n");
|
|
122
141
|
return `[Unit]
|
|
123
|
-
Description=Privateer resident agent
|
|
142
|
+
Description=Privateer resident agent harbor (routines + app-driven task spawns)
|
|
124
143
|
After=network-online.target
|
|
125
144
|
Wants=network-online.target
|
|
126
145
|
|
|
@@ -136,7 +155,20 @@ WantedBy=default.target
|
|
|
136
155
|
`;
|
|
137
156
|
}
|
|
138
157
|
|
|
158
|
+
// Evict a pre-rename systemd --user unit (privateer-daemon.service) if present, so the
|
|
159
|
+
// harbor rename doesn't leave the old unit enabled alongside the new one.
|
|
160
|
+
function evictOldSystemd(): void {
|
|
161
|
+
const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
162
|
+
const oldUnit = join(base, "systemd", "user", OLD_UNIT);
|
|
163
|
+
if (existsSync(oldUnit)) {
|
|
164
|
+
spawnSync("systemctl", ["--user", "disable", "--now", OLD_UNIT], { stdio: "ignore" });
|
|
165
|
+
rmSync(oldUnit, { force: true });
|
|
166
|
+
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
139
170
|
function installSystemd(): void {
|
|
171
|
+
evictOldSystemd();
|
|
140
172
|
const unit = systemdUnitPath();
|
|
141
173
|
mkdirSync(dirname(unit), { recursive: true });
|
|
142
174
|
writeFileSync(unit, systemdUnit());
|
|
@@ -152,6 +184,7 @@ function installSystemd(): void {
|
|
|
152
184
|
}
|
|
153
185
|
|
|
154
186
|
function uninstallSystemd(): void {
|
|
187
|
+
evictOldSystemd();
|
|
155
188
|
const unit = systemdUnitPath();
|
|
156
189
|
spawnSync("systemctl", ["--user", "disable", "--now", UNIT], { stdio: "ignore" });
|
|
157
190
|
if (existsSync(unit)) rmSync(unit, { force: true });
|
|
@@ -182,7 +215,7 @@ export function serviceInfo(): ServiceInfo {
|
|
|
182
215
|
supported: platform === "darwin" || platform === "linux",
|
|
183
216
|
installed: !!unitPath && existsSync(unitPath),
|
|
184
217
|
unitPath,
|
|
185
|
-
logPath:
|
|
218
|
+
logPath: harborLogPath(),
|
|
186
219
|
};
|
|
187
220
|
}
|
|
188
221
|
|
|
@@ -191,7 +224,7 @@ export function installService(): ServiceInfo {
|
|
|
191
224
|
const platform = process.platform;
|
|
192
225
|
if (platform === "darwin") installLaunchd();
|
|
193
226
|
else if (platform === "linux") installSystemd();
|
|
194
|
-
else throw new Error(`Auto-start isn't supported on ${platform}. Run \`privateer
|
|
227
|
+
else throw new Error(`Auto-start isn't supported on ${platform}. Run \`privateer harbor\` yourself, or keep a terminal open.`);
|
|
195
228
|
return serviceInfo();
|
|
196
229
|
}
|
|
197
230
|
|
|
@@ -203,15 +236,15 @@ export function uninstallService(): ServiceInfo {
|
|
|
203
236
|
return serviceInfo();
|
|
204
237
|
}
|
|
205
238
|
|
|
206
|
-
// Human-readable status line for `privateer
|
|
207
|
-
// installed AND whether a
|
|
239
|
+
// Human-readable status line for `privateer harbor status`: whether the service is
|
|
240
|
+
// installed AND whether a harbor is actually answering on the IPC socket right now.
|
|
208
241
|
export async function statusReport(): Promise<string> {
|
|
209
242
|
const info = serviceInfo();
|
|
210
|
-
const live = await
|
|
243
|
+
const live = await harborIsRunning();
|
|
211
244
|
const lines = [
|
|
212
|
-
`platform: ${info.platform}${info.supported ? "" : " (auto-start unsupported — run `privateer
|
|
245
|
+
`platform: ${info.platform}${info.supported ? "" : " (auto-start unsupported — run `privateer harbor` manually)"}`,
|
|
213
246
|
`service: ${info.installed ? `installed (${info.unitPath})` : "not installed"}`,
|
|
214
|
-
`
|
|
247
|
+
`harbor: ${live ? "running (answering IPC)" : "not reachable"}`,
|
|
215
248
|
`logs: ${info.logPath}`,
|
|
216
249
|
];
|
|
217
250
|
// Surface a stale-unit hint: file present but nothing answering usually means it
|
|
@@ -220,11 +253,11 @@ export async function statusReport(): Promise<string> {
|
|
|
220
253
|
return lines.join("\n");
|
|
221
254
|
}
|
|
222
255
|
|
|
223
|
-
// Best-effort read of the tail of the
|
|
256
|
+
// Best-effort read of the tail of the harbor log (for a `status --log` affordance or
|
|
224
257
|
// error surfacing). Returns "" if absent.
|
|
225
|
-
export function
|
|
258
|
+
export function tailHarborLog(maxBytes = 4_000): string {
|
|
226
259
|
try {
|
|
227
|
-
const buf = readFileSync(
|
|
260
|
+
const buf = readFileSync(harborLogPath(), "utf8");
|
|
228
261
|
return buf.length > maxBytes ? buf.slice(buf.length - maxBytes) : buf;
|
|
229
262
|
} catch {
|
|
230
263
|
return "";
|
package/src/main.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Phase 1 skeleton: this prints the resolved boot state so `npm start` proves
|
|
6
6
|
// the boot chain end-to-end (env pinned, dispatcher installed) without a TUI.
|
|
7
|
-
// Phases 4/6 replace the body with the
|
|
7
|
+
// Phases 4/6 replace the body with the harbor/relay wiring and the pi-tui app.
|
|
8
8
|
|
|
9
9
|
import "./boot.ts";
|
|
10
10
|
|
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()
|
|
@@ -322,7 +369,7 @@ export function makeAccountProvider() {
|
|
|
322
369
|
// auth.json entry (see the LIFECYCLE HAZARD note in src/auth/privateer.ts). So a
|
|
323
370
|
// signed-in user who quits and relaunches lands on privateer/* with no key at
|
|
324
371
|
// all, and the first prompt dead-ends on "No API key found for privateer." — even
|
|
325
|
-
// though the banner says "connected". The REPL (cli/chat.ts) and the
|
|
372
|
+
// though the banner says "connected". The REPL (cli/chat.ts) and the harbor
|
|
326
373
|
// already spawn one at startup; this gives the TUI the same seed.
|
|
327
374
|
pi.on?.("session_start", (_e, ctx) => void armAccountCredential(ctx));
|
|
328
375
|
};
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The single source of truth for "which model do we default to?" — shared by every
|
|
2
2
|
// entry point that has to pick a model when the user hasn't named one: the REPL
|
|
3
|
-
// (cli/chat.ts), the
|
|
3
|
+
// (cli/chat.ts), the harbor (routines), the channels runner, and the login-time hook
|
|
4
4
|
// that seeds Pi's TUI default (ensurePiDefaultModel).
|
|
5
5
|
//
|
|
6
6
|
// The bug this fixes: each of those sites used to hardcode `openrouter/openai/gpt-4o-
|
|
@@ -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.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic primitives, all via the Web Crypto API (`globalThis.crypto`) so
|
|
3
|
+
* the same code runs in browsers and in Node 20+ with no third-party deps.
|
|
4
|
+
* Only SHA-256 and Ed25519 verification are needed for Level 1.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AciFormatError, UnsupportedAlgorithmError } from './errors';
|
|
8
|
+
|
|
9
|
+
const subtle = globalThis.crypto.subtle;
|
|
10
|
+
|
|
11
|
+
/** Lowercase-hex encode bytes. */
|
|
12
|
+
export function toHex(bytes: Uint8Array): string {
|
|
13
|
+
let out = '';
|
|
14
|
+
for (const b of bytes) out += b.toString(16).padStart(2, '0');
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Decode hex (optionally `0x`-prefixed) to bytes. */
|
|
19
|
+
export function fromHex(hex: string): Uint8Array {
|
|
20
|
+
const h = hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
|
|
21
|
+
if (h.length % 2 !== 0) {
|
|
22
|
+
throw new AciFormatError(`hex string has odd length: ${hex.length} chars`);
|
|
23
|
+
}
|
|
24
|
+
const out = new Uint8Array(h.length / 2);
|
|
25
|
+
for (let i = 0; i < out.length; i++) {
|
|
26
|
+
const byte = Number.parseInt(h.substr(i * 2, 2), 16);
|
|
27
|
+
if (Number.isNaN(byte)) {
|
|
28
|
+
throw new AciFormatError(`invalid hex at offset ${i * 2}: "${h.substr(i * 2, 2)}"`);
|
|
29
|
+
}
|
|
30
|
+
out[i] = byte;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** SHA-256 of the given bytes. */
|
|
36
|
+
export async function sha256(bytes: Uint8Array): Promise<Uint8Array> {
|
|
37
|
+
return new Uint8Array(await subtle.digest('SHA-256', bytes as BufferSource));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Lowercase-hex SHA-256 of the given bytes. */
|
|
41
|
+
export async function sha256Hex(bytes: Uint8Array): Promise<string> {
|
|
42
|
+
return toHex(await sha256(bytes));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `sha256:<lowercase-hex>` digest string of the given bytes — the ACI digest
|
|
47
|
+
* form (§3) used for `workload_id`, keyset digests, and body hashes.
|
|
48
|
+
*/
|
|
49
|
+
export async function sha256Prefixed(bytes: Uint8Array): Promise<string> {
|
|
50
|
+
return 'sha256:' + (await sha256Hex(bytes));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Verify an Ed25519 signature (RFC 8032, §4.3/§8.5) over `message`.
|
|
55
|
+
* `publicKeyRaw` is the 32-byte raw key; `signature` the 64-byte value.
|
|
56
|
+
* Returns false on a bad signature or malformed key — never throws for those.
|
|
57
|
+
*/
|
|
58
|
+
export async function verifyEd25519(
|
|
59
|
+
publicKeyRaw: Uint8Array,
|
|
60
|
+
signature: Uint8Array,
|
|
61
|
+
message: Uint8Array,
|
|
62
|
+
): Promise<boolean> {
|
|
63
|
+
let key: CryptoKey;
|
|
64
|
+
try {
|
|
65
|
+
key = await subtle.importKey('raw', publicKeyRaw as BufferSource, { name: 'Ed25519' }, false, [
|
|
66
|
+
'verify',
|
|
67
|
+
]);
|
|
68
|
+
} catch {
|
|
69
|
+
// A key that will not import cannot verify anything.
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return await subtle.verify({ name: 'Ed25519' }, key, signature as BufferSource, message as BufferSource);
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Verify a signature by ACI signature `algo`, dispatching on the algorithm the
|
|
81
|
+
* attested keyset entry declares. Only `ed25519` is verifiable here; every other
|
|
82
|
+
* algorithm (including `ecdsa-secp256k1`) raises {@link UnsupportedAlgorithmError}.
|
|
83
|
+
*/
|
|
84
|
+
export async function verifySignature(
|
|
85
|
+
algo: string,
|
|
86
|
+
publicKeyRaw: Uint8Array,
|
|
87
|
+
signature: Uint8Array,
|
|
88
|
+
message: Uint8Array,
|
|
89
|
+
context: string,
|
|
90
|
+
): Promise<boolean> {
|
|
91
|
+
if (algo === 'ed25519') {
|
|
92
|
+
return verifyEd25519(publicKeyRaw, signature, message);
|
|
93
|
+
}
|
|
94
|
+
throw new UnsupportedAlgorithmError(algo, context);
|
|
95
|
+
}
|