privateer-agent 0.12.8 → 0.12.10

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 CHANGED
@@ -12,9 +12,6 @@
12
12
  <a href="https://www.npmjs.com/package/privateer-agent">
13
13
  <img src="https://img.shields.io/npm/v/privateer-agent" alt="npm" />
14
14
  </a>
15
- <a href="https://www.npmjs.com/package/privateer-agent">
16
- <img src="https://img.shields.io/npm/dm/privateer-agent" alt="npm downloads" />
17
- </a>
18
15
  <a href="https://github.com/privateer-agent/privateer-agent/releases">
19
16
  <img src="https://img.shields.io/badge/changelog-what's%20new-5b8def" alt="Changelog" />
20
17
  </a>
@@ -125,7 +122,7 @@ silently. The moat is swappable; the floor under it holds.
125
122
  shots stay continuous, cut them together, then score and narrate the result. Generated media
126
123
  is handed straight back as files on your machine; none of it is stored in our cloud. See
127
124
  [docs/media-generation.md](docs/media-generation.md).
128
- - **Talk to it.** `/speak on` reads answers aloud as they're written; **ctrl+x** is push-to-talk
125
+ - **Talk to it.** `/speak on` reads answers aloud as they're written; **alt+t** is push-to-talk
129
126
  and the mic closes when you stop talking. Your OS voice by default — nothing leaves the
130
127
  machine — or your account's confidential-compute TTS/STT once you sign in. `/talk loop on`
131
128
  makes it hands-free. See [Talk to it](#talk-to-it--voice-both-directions).
@@ -187,9 +184,31 @@ npm view privateer-agent dist.attestations # published from CI with npm proven
187
184
  npm audit signatures # verify registry signatures + provenance
188
185
  ```
189
186
 
190
- The package also declares **no install scripts** no `postinstall`, nothing. Installing
191
- it writes files and executes nothing; `npm install -g privateer-agent --ignore-scripts`
192
- gives an identical result. Code runs only when you run `privateer`.
187
+ And **after** installing the check nobody else offers, because every signal above is
188
+ an install-time one:
189
+
190
+ ```bash
191
+ privateer verify # is the install on THIS disk still the one we published?
192
+ ```
193
+
194
+ It reports the install shape, whether this exact version is published and carries a
195
+ provenance attestation, whether any dependency has drifted from its pinned version,
196
+ and which launch-time patches are applied. Inconclusive checks say so rather than
197
+ counting as a pass; `--offline` skips the ones that need the registry.
198
+
199
+ The package also declares **no install scripts** — no `postinstall`, nothing — so
200
+ nothing we publish executes at install time, and `--ignore-scripts` gives an identical
201
+ result. That is a claim about *our* tarball, not your whole install: a handful of
202
+ packages in the ~500-package dependency tree do declare install hooks, and none of them
203
+ are needed here, so the install we recommend and test is
204
+
205
+ ```bash
206
+ npm install -g privateer-agent --ignore-scripts
207
+ ```
208
+
209
+ Or skip the dependency graph entirely with a bundle (the `curl | sh` one-liner above),
210
+ which verifies a published SHA-256 and, from the next release on, a signed build
211
+ attestation before it unpacks anything.
193
212
 
194
213
  See [SECURITY.md](SECURITY.md) for the threat model, the permission gate, and how to
195
214
  report a vulnerability.
@@ -295,11 +314,11 @@ with `/signout`; manage linked terminals from the app.
295
314
  ## Talk to it — voice, both directions
296
315
 
297
316
  `/speak on` and answers are read aloud **as they are written**, sentence by sentence, with
298
- code blocks, tables and URLs stripped. Press **ctrl+x** and talk; the mic closes when you
317
+ code blocks, tables and URLs stripped. Press **alt+t** and talk; the mic closes when you
299
318
  stop talking and the transcript lands in the composer for you to read before Enter sends it.
300
319
 
301
320
  ```
302
- /speak on read answers aloud ctrl+x push to talk (press again to send)
321
+ /speak on read answers aloud alt+t push to talk (press again to send)
303
322
  /speak voice <name> pick a speaker /talk the same thing, typed
304
323
  /speak rate <n> 0.5–3× pace /talk loop on conversation mode, hands-free
305
324
  /speak stream off wait for the full answer instead of speaking as it arrives
@@ -610,7 +629,7 @@ drop your own into `~/.privateer/agent/extensions/` and it loads the same way, g
610
629
  | `/signin` · `/signout` | sign in to a Privateer account (device flow) / sign out |
611
630
  | `/remote-access` | link this terminal to the app and allow it to drive (off by default) |
612
631
  | `/connect` · `/mcp` | add, enable, or remove MCP connectors / see what actually connected |
613
- | `/speak` · `/talk` | read answers aloud / voice input (**ctrl+x** is push-to-talk) |
632
+ | `/speak` · `/talk` | read answers aloud / voice input (**alt+t** is push-to-talk) |
614
633
  | `/extensions` | list loaded Pi extensions |
615
634
  | `/init` | scaffold a starter `PRIVATEER.md` in this directory |
616
635
  | `/update` · `/privateer` | update to the latest release / Privateer status and posture |
package/SECURITY.md CHANGED
@@ -12,11 +12,27 @@ Privateer is a terminal coding agent: it reads and writes files, runs shell comm
12
12
  talks to model providers on your behalf. That is the point of it, and it is also the
13
13
  threat model. Two things are worth verifying rather than taking on faith.
14
14
 
15
- **The package runs no install scripts.** There is no `preinstall`, `install`,
16
- `postinstall` or `prepare` hook. `npm install -g privateer-agent` writes files and
17
- executes nothing; installing with `--ignore-scripts` produces an identical result. Code
18
- runs only when you run `privateer`. (Dependency patching happens at first launch — see
19
- `bin/apply-patches.mjs` and `docs/shipping.md`.)
15
+ **This package runs no install scripts.** There is no `preinstall`, `install`,
16
+ `postinstall` or `prepare` hook in `privateer-agent`: nothing we publish executes at
17
+ install time, and installing with `--ignore-scripts` produces an identical result. Our
18
+ code runs only when you run `privateer`. (Dependency patching happens at first launch —
19
+ see `bin/apply-patches.mjs` and `docs/shipping.md`.)
20
+
21
+ That is a claim about *our* tarball, not about your whole install, and the difference
22
+ matters. `npm install -g privateer-agent` resolves a tree of roughly 500 packages, and
23
+ seven of them do declare install scripts — `esbuild`, `koffi`, `fsevents`, `protobufjs`
24
+ (×2) and `@google/genai` (×2). On a default npm config those execute on your machine, as
25
+ they would for anything else that depends on them. Every one is a build-from-source or
26
+ compatibility-warning fallback we don't need, so install with scripts off:
27
+
28
+ ```bash
29
+ npm install -g privateer-agent --ignore-scripts
30
+ ```
31
+
32
+ That is the install we test and the shape CI publishes from, and it produces a working
33
+ `privateer`. To skip the dependency graph altogether, use a release bundle
34
+ (`curl -fsSL https://privateer.pro/install.sh | sh`): a fixed set of files with a pinned
35
+ Node, no resolution step on your machine at all.
20
36
 
21
37
  **Releases carry npm provenance.** Published from `.github/workflows/release.yml` with
22
38
  `npm publish --provenance`, so npm holds a signed Sigstore attestation binding the
@@ -29,6 +45,26 @@ npm view privateer-agent dist.attestations # attestation metadata exists
29
45
  npm audit signatures # verifies registry signatures + provenance
30
46
  ```
31
47
 
48
+ **You can re-check the install later.** Everything above is an install-time signal, which
49
+ is not much use to someone asking the question weeks afterwards. `privateer verify` reads
50
+ the install actually on disk and reports the shape it took, whether this version is
51
+ published with provenance, whether any dependency has drifted from its pinned version,
52
+ and which launch-time patches are applied. It distinguishes "verified" from "couldn't
53
+ check" and never reports the second as the first. It is not a security boundary — anything
54
+ that can rewrite `node_modules` can rewrite the checker — but it catches the accidental
55
+ and opportunistic cases, which is nearly all of them.
56
+
57
+ **Bundle downloads are verified before they unpack.** `install.sh` / `install.ps1` fetch
58
+ the release's published SHA-256 and refuse to install on a mismatch, a missing digest, or
59
+ a machine with no way to compute one (`PRIVATEER_SKIP_CHECKSUM=1` overrides, loudly). A
60
+ checksum served from the same origin as the file proves integrity but not provenance, so
61
+ from the release following this change the bundles also carry a GitHub build attestation,
62
+ which the installers verify automatically when `gh` is available:
63
+
64
+ ```bash
65
+ gh attestation verify privateer-darwin-arm64.tar.gz --repo privateer-agent/privateer-agent
66
+ ```
67
+
32
68
  If a version lacks provenance, it did not come from this workflow. Treat that as
33
69
  suspicious and report it — with one documented exception: **0.6.7 is the first release
34
70
  published this way.** Trusted publishing was misconfigured until then, so every earlier
@@ -294,6 +294,16 @@ else if (sub === "harbor" || sub === "daemon") {
294
294
  runToCompletion(NODE_BIN, [...nodeArgs, path.join(REPO, "bin", "privateer-harbor.mjs"), ...args.slice(1)]);
295
295
  }
296
296
 
297
+ // --- `privateer verify` ----------------------------------------------------
298
+ // Check the install that is on this disk right now. Every other trust signal we
299
+ // publish is an install-time one (npm provenance at `npm i`, a checksum inside
300
+ // install.sh, a badge on a web page nobody revisits); this is the one a user can
301
+ // run afterwards, on the machine they're worried about. Runs BEFORE ensurePatches
302
+ // so it can report the real patch state rather than the state it just created.
303
+ else if (sub === "verify") {
304
+ runToCompletion(NODE_BIN, [path.join(REPO, "bin", "privateer-verify.mjs"), ...args.slice(1)]);
305
+ }
306
+
297
307
  // --- `privateer acp` -------------------------------------------------------
298
308
  // Privateer as an Agent Client Protocol server, spawned by an ACP host (Buzz's
299
309
  // `buzz-acp`, Zed, …) and driven over newline-delimited JSON-RPC on stdio.
@@ -0,0 +1,10 @@
1
+ // Types for `privateer verify`. The implementation is plain .mjs for the same
2
+ // reason apply-patches.mjs is: bin/ runs under a bare `node`, before any transpiler.
3
+
4
+ /**
5
+ * Check the install rooted at `repo` and print a report. Returns a process exit
6
+ * code: 0 when nothing FAILED (inconclusive checks are not failures), 1 otherwise.
7
+ *
8
+ * `offline` skips every check that needs the npm registry.
9
+ */
10
+ export function verify(opts: { repo: string; offline?: boolean }): Promise<number>;
@@ -0,0 +1,289 @@
1
+ // `privateer verify` — check the install that is actually on this disk.
2
+ //
3
+ // Every other trust signal we offer is an INSTALL-TIME signal: npm provenance is
4
+ // checked when you install, install.sh verifies a checksum before it unpacks, the
5
+ // npm page shows a green badge to whoever is looking at the npm page. None of that
6
+ // answers the question a user actually has three weeks later, which is "is the
7
+ // thing on my machine right now still the thing you published?"
8
+ //
9
+ // So this command reads local state first and the network second, and it is careful
10
+ // about the difference between "verified", "couldn't check", and "wrong". A check
11
+ // that cannot reach the registry says so; it never reports a pass it didn't make.
12
+ //
13
+ // Deliberately NOT a security boundary: anything that can rewrite node_modules can
14
+ // rewrite this file too. It is a tampering *smoke alarm* and an honest inventory —
15
+ // useful exactly because most breakage is accidental (a half-finished upgrade, a
16
+ // stale patch stamp, a hand-edited dependency) rather than adversarial.
17
+
18
+ import crypto from "node:crypto";
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ import { findDepRoot } from "./apply-patches.mjs";
24
+
25
+ const REGISTRY = process.env.npm_config_registry || "https://registry.npmjs.org";
26
+ const NET_TIMEOUT_MS = 8000;
27
+
28
+ // --- tiny reporter ---------------------------------------------------------
29
+ // Three outcomes, and the third one is the point: "unknown" must never be
30
+ // rendered as a pass. Exit code reflects only real failures, so `privateer verify`
31
+ // is safe to put in a CI step without breaking on an offline runner.
32
+ const C = process.stdout.isTTY && !process.env.NO_COLOR;
33
+ const dim = (s) => (C ? `\x1b[2m${s}\x1b[0m` : s);
34
+ const bold = (s) => (C ? `\x1b[1m${s}\x1b[0m` : s);
35
+ const green = (s) => (C ? `\x1b[32m${s}\x1b[0m` : s);
36
+ const red = (s) => (C ? `\x1b[31m${s}\x1b[0m` : s);
37
+ const yellow = (s) => (C ? `\x1b[33m${s}\x1b[0m` : s);
38
+
39
+ let failures = 0;
40
+ let unknowns = 0;
41
+
42
+ function pass(label, detail) {
43
+ console.log(` ${green("✓")} ${label}${detail ? ` ${dim(detail)}` : ""}`);
44
+ }
45
+ function fail(label, detail) {
46
+ failures++;
47
+ console.log(` ${red("✗")} ${label}${detail ? ` ${dim(detail)}` : ""}`);
48
+ }
49
+ function unknown(label, detail) {
50
+ unknowns++;
51
+ console.log(` ${yellow("?")} ${label}${detail ? ` ${dim(detail)}` : ""}`);
52
+ }
53
+ function note(text) {
54
+ console.log(` ${dim(text)}`);
55
+ }
56
+ function heading(text) {
57
+ console.log(`\n${bold(text)}`);
58
+ }
59
+
60
+ const readJson = (p) => {
61
+ try {
62
+ return JSON.parse(fs.readFileSync(p, "utf8"));
63
+ } catch {
64
+ return null;
65
+ }
66
+ };
67
+
68
+ async function getJson(url) {
69
+ const ac = new AbortController();
70
+ const t = setTimeout(() => ac.abort(), NET_TIMEOUT_MS);
71
+ try {
72
+ const r = await fetch(url, { signal: ac.signal, headers: { accept: "application/json" } });
73
+ if (!r.ok) return { error: `HTTP ${r.status}` };
74
+ return { data: await r.json() };
75
+ } catch (e) {
76
+ return { error: e.name === "AbortError" ? "timed out" : e.message };
77
+ } finally {
78
+ clearTimeout(t);
79
+ }
80
+ }
81
+
82
+ /** sha256 over every patch file's name + contents — must match apply-patches.mjs. */
83
+ function patchSetHash(patchDir, files, stampVersion) {
84
+ const h = crypto.createHash("sha256").update(String(stampVersion));
85
+ for (const f of files) {
86
+ h.update(f);
87
+ h.update(fs.readFileSync(path.join(patchDir, f)));
88
+ }
89
+ return h.digest("hex");
90
+ }
91
+
92
+ export async function verify({ repo, offline = false } = {}) {
93
+ // Reset: the counters are module state, so a second call in one process (a test,
94
+ // or any future programmatic caller) would otherwise inherit the first's verdict.
95
+ failures = 0;
96
+ unknowns = 0;
97
+
98
+ const pkg = readJson(path.join(repo, "package.json")) || {};
99
+ const version = pkg.version || "unknown";
100
+ const bundleInfo = readJson(path.join(repo, "BUNDLE_INFO.json"));
101
+
102
+ console.log(`\n${bold("⚓ Privateer install check")} ${dim(`v${version}`)}`);
103
+
104
+ // --- 1. what kind of install is this ------------------------------------
105
+ heading("Install");
106
+ if (bundleInfo) {
107
+ pass("self-contained bundle", `${bundleInfo.target || "?"} · pinned Node ${bundleInfo.node || "?"}`);
108
+ note("bundles ship a fixed file set — nothing was resolved on this machine at install time");
109
+ } else {
110
+ pass("npm install", "dependencies were resolved on this machine");
111
+ }
112
+ console.log(` ${dim("path")} ${repo}`);
113
+ console.log(` ${dim("node")} ${process.version}`);
114
+
115
+ // --- 2. is this version the one the registry knows about ----------------
116
+ // The strongest thing checkable from here without re-downloading a tarball:
117
+ // does this exact version exist upstream, and does it carry a provenance
118
+ // attestation? A version the registry has never heard of is the loud case.
119
+ heading("Published provenance");
120
+ if (offline) {
121
+ unknown("skipped (--offline)");
122
+ } else {
123
+ const { data, error } = await getJson(`${REGISTRY}/${pkg.name || "privateer-agent"}/${version}`);
124
+ if (error) {
125
+ unknown("couldn't reach the npm registry", `(${error})`);
126
+ note("re-run with a network connection, or use --offline to skip this section");
127
+ } else if (!data || data.error) {
128
+ fail(`the registry has no ${pkg.name}@${version}`, "this build was not published");
129
+ note("an unpublished version is not automatically malicious — an unreleased local");
130
+ note("checkout looks identical — but a machine you only ever installed from npm");
131
+ note("should never be in this state.");
132
+ } else {
133
+ pass(`published as ${pkg.name}@${version}`);
134
+ const att = data.dist?.attestations;
135
+ if (att?.provenance?.predicateType) {
136
+ pass("carries an npm provenance attestation", att.provenance.predicateType);
137
+ note("verify the signature chain end-to-end with: npm audit signatures");
138
+ } else {
139
+ fail("no provenance attestation on this version");
140
+ note("every release from 0.6.7 on is published from CI with --provenance;");
141
+ note("a later version without one did not come from that workflow.");
142
+ }
143
+ const npmUser = data._npmUser?.trustedPublisher?.id;
144
+ if (npmUser) pass("published by a trusted publisher", npmUser);
145
+ else if (att) unknown("publisher identity not reported by the registry");
146
+ }
147
+ }
148
+
149
+ // --- 3. did the dependency tree drift from what we declared -------------
150
+ // Every direct dependency is pinned to an exact version, precisely so this
151
+ // check can be exact: a resolved version that differs from the declared one
152
+ // means the tree was changed after install.
153
+ heading("Dependencies");
154
+ const deps = Object.entries(pkg.dependencies || {});
155
+ const drift = [];
156
+ const ranged = []; // declared with a range, so there is nothing exact to check against
157
+ let checked = 0;
158
+ let missing = 0;
159
+ for (const [name, want] of deps) {
160
+ const root = findDepRoot(repo, name);
161
+ const manifest = root && readJson(path.join(root, "node_modules", ...name.split("/"), "package.json"));
162
+ if (!manifest) {
163
+ missing++;
164
+ continue;
165
+ }
166
+ // An exact pin is the only thing this check can be exact about. A caret range
167
+ // is not a failure, but counting it as a match would be a lie — it would report
168
+ // "matches its pinned version" for a dependency that has no pinned version.
169
+ if (!/^\d/.test(want)) {
170
+ ranged.push(`${name} ${want} (resolved ${manifest.version})`);
171
+ continue;
172
+ }
173
+ checked++;
174
+ if (manifest.version !== want) drift.push(`${name} ${want} → ${manifest.version}`);
175
+ }
176
+ if (missing) {
177
+ unknown(`${missing} of ${deps.length} direct dependencies not found on disk`);
178
+ note("normal inside a bundle, which flattens its own tree; unexpected for an npm install");
179
+ }
180
+ if (drift.length > 0) {
181
+ fail(`${drift.length} dependenc${drift.length === 1 ? "y differs" : "ies differ"} from the pinned version`);
182
+ for (const d of drift) note(d);
183
+ } else if (checked > 0) {
184
+ pass(`${checked} direct dependenc${checked === 1 ? "y matches its" : "ies match their"} exact pin`);
185
+ }
186
+ if (ranged.length > 0) {
187
+ // Versions before 0.12.9 shipped caret ranges, so an older install lands here.
188
+ unknown(`${ranged.length} direct dependenc${ranged.length === 1 ? "y is" : "ies are"} declared as a range — cannot be checked exactly`);
189
+ for (const d of ranged.slice(0, 5)) note(d);
190
+ if (ranged.length > 5) note(`… and ${ranged.length - 5} more`);
191
+ }
192
+
193
+ const piRoot = findDepRoot(repo, "@earendil-works/pi-coding-agent");
194
+ const piPkg =
195
+ piRoot && readJson(path.join(piRoot, "node_modules", "@earendil-works", "pi-coding-agent", "package.json"));
196
+ if (piPkg) console.log(` ${dim("runtime")} pi-coding-agent ${piPkg.version}`);
197
+
198
+ // --- 4. patch state ------------------------------------------------------
199
+ // Patches are applied at LAUNCH into node_modules, so the running tree is
200
+ // legitimately not byte-identical to the published tarball. That is by design
201
+ // (it is how we avoid an install script), but it means "the files changed" is
202
+ // not by itself a finding — this is the section that says which changes are ours.
203
+ heading("Launch-time patches");
204
+ const patchDir = path.join(repo, "patches");
205
+ const patchFiles = fs.existsSync(patchDir)
206
+ ? fs
207
+ .readdirSync(patchDir)
208
+ .filter((f) => f.endsWith(".patch"))
209
+ .sort()
210
+ : [];
211
+ if (patchFiles.length === 0) {
212
+ pass("none shipped");
213
+ } else {
214
+ const stampFile = piRoot && path.join(piRoot, "node_modules", ".privateer-patches.json");
215
+ const stamp = stampFile && readJson(stampFile);
216
+ // STAMP_VERSION is private to apply-patches.mjs; try the current value and
217
+ // report a mismatch as "stale" rather than guessing at the applier's internals.
218
+ const want = patchSetHash(patchDir, patchFiles, 1);
219
+ if (!stamp) {
220
+ unknown(`${patchFiles.length} patch(es) shipped, none recorded as applied`);
221
+ note("expected before the first launch, or when node_modules is root-owned");
222
+ note("(sudo npm i -g) — Privateer then runs on stock Pi and says so at startup");
223
+ } else if (stamp.hash === want) {
224
+ pass(`${patchFiles.length} patch(es) applied and current`, `at ${stamp.at || "?"}`);
225
+ for (const f of patchFiles) note(f);
226
+ } else {
227
+ unknown("recorded patch set does not match the shipped one", "— will re-apply next launch");
228
+ }
229
+ }
230
+
231
+ // --- 5. where config and credentials live -------------------------------
232
+ heading("Config");
233
+ const home = process.env.PRIVATEER_HOME || path.join(process.env.HOME || "", ".privateer");
234
+ console.log(` ${dim("home")} ${home}${fs.existsSync(home) ? "" : dim(" (not created yet)")}`);
235
+ for (const d of [".privateer", ".pi"]) {
236
+ const p = path.join(process.cwd(), d);
237
+ if (fs.existsSync(p)) console.log(` ${dim("project")} ${p}`);
238
+ }
239
+
240
+ // --- verdict -------------------------------------------------------------
241
+ console.log("");
242
+ if (failures > 0) {
243
+ console.log(
244
+ red(`✗ ${failures} check(s) failed`) +
245
+ (unknowns ? dim(`, ${unknowns} inconclusive`) : "") +
246
+ "\n" +
247
+ dim(" If you did not expect this, reinstall from a known-good source:\n") +
248
+ dim(" curl -fsSL https://privateer.pro/install.sh | sh\n") +
249
+ dim(" and report it at https://github.com/privateer-agent/privateer-agent/issues"),
250
+ );
251
+ return 1;
252
+ }
253
+ if (unknowns > 0) {
254
+ console.log(yellow(`✓ no failures, ${unknowns} check(s) inconclusive`));
255
+ return 0;
256
+ }
257
+ console.log(green("✓ everything checks out"));
258
+ return 0;
259
+ }
260
+
261
+ // --- CLI -------------------------------------------------------------------
262
+ // Guarded, so importing this module (from a test, or from the launcher) does not
263
+ // run the command and exit the process out from under the caller.
264
+ //
265
+ // fileURLToPath, not `new URL(...).pathname`: on Windows the latter yields
266
+ // "/C:/Users/..." with percent-encoded spaces, so every path built from it is wrong
267
+ // — and win32 is a platform we ship a bundle for.
268
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
269
+
270
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
271
+ const argv = process.argv.slice(2);
272
+ if (argv.includes("--help") || argv.includes("-h")) {
273
+ const cmd = process.env.PRIVATEER_CMD || "privateer";
274
+ console.log(
275
+ [
276
+ `${cmd} verify — check the Privateer install on this machine.`,
277
+ "",
278
+ ` ${cmd} verify run every check`,
279
+ ` ${cmd} verify --offline skip the checks that need the npm registry`,
280
+ "",
281
+ "Reports the install shape, whether this version is published with npm",
282
+ "provenance, whether the dependency tree still matches its pinned versions,",
283
+ "and which launch-time patches are applied.",
284
+ ].join("\n"),
285
+ );
286
+ process.exit(0);
287
+ }
288
+ process.exit(await verify({ repo: path.resolve(HERE, ".."), offline: argv.includes("--offline") }));
289
+ }
@@ -404,8 +404,14 @@ export default function privateerBrand(pi: any): void {
404
404
  // entry is dead for every terminal;
405
405
  // - the default ownership-checked drop for THIS process's exit, which must not
406
406
  // delete the credential another running terminal is using.
407
- const dropPersistedAccount = (ctx: any, opts: { force?: boolean } = {}): void => {
408
- dropPersistedAccountCredential(ctx, opts);
407
+ // `ctx` is no longer passed through: pi 0.84 removed ctx.modelRegistry.authStorage, so
408
+ // the store is resolved from piAuthStore() inside. Kept in the signature because every
409
+ // caller here is a Pi command handler that has one, and dropping the parameter would
410
+ // churn call sites for no gain. Fire-and-forget as before — the drop is best-effort and
411
+ // the server-side TTL is the fallback — but the promise is now caught explicitly rather
412
+ // than left to become an unhandled rejection.
413
+ const dropPersistedAccount = (_ctx: any, opts: { force?: boolean } = {}): void => {
414
+ void dropPersistedAccountCredential(opts).catch(() => { /* best effort — server TTL covers it */ });
409
415
  };
410
416
 
411
417
  // /update — run the global npm install in a child process and report the outcome via
@@ -39,6 +39,12 @@ function key(binding: Parameters<typeof keyText>[0], fallback: string): string {
39
39
  }
40
40
  }
41
41
 
42
+ // Mirror of privateer-speak's DEFAULT_SHORTCUT. One constant, not a literal at each
43
+ // return, so the two paths below can't drift apart from each other — they can still
44
+ // drift from the package, which is why the value is named here rather than inlined:
45
+ // grep for it when bumping privateer-speak.
46
+ const DEFAULT_TALK_KEY = "alt+t";
47
+
42
48
  // Push-to-talk is pi-speak's own binding, not a Pi keybinding action, so keyText
43
49
  // can't see it — it lives in speak.json beside config.json and /talk key rebinds
44
50
  // it. Read it the same defensive way (any failure = the default).
@@ -46,9 +52,9 @@ function talkKey(): string {
46
52
  try {
47
53
  const shortcut = JSON.parse(readFileSync(join(globalDir(), "speak.json"), "utf8"))?.input?.shortcut;
48
54
  // An explicit "" is "unbound", not "missing" — only an absent field defaults.
49
- return typeof shortcut === "string" ? shortcut : "ctrl+x";
55
+ return typeof shortcut === "string" ? shortcut : DEFAULT_TALK_KEY;
50
56
  } catch {
51
- return "ctrl+x";
57
+ return DEFAULT_TALK_KEY;
52
58
  }
53
59
  }
54
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.12.8",
3
+ "version": "0.12.10",
4
4
  "description": "Privacy-first terminal coding agent — bring your own model across 20 providers (Anthropic, OpenAI, OpenRouter, Google, local Ollama…). Safe-by-default permissions, MCP, sub-agents, workflows, and verifiable TEE inference. Built on the Pi toolkit.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -69,30 +69,30 @@
69
69
  "node": ">=22.19.0"
70
70
  },
71
71
  "dependencies": {
72
- "@earendil-works/pi-ai": "^0.80.3",
73
- "@earendil-works/pi-coding-agent": "0.80.3",
74
- "@earendil-works/pi-tui": "^0.80.3",
75
- "@juicesharp/rpiv-ask-user-question": "^2.2.0",
76
- "@juicesharp/rpiv-web-tools": "^1.20.0",
77
- "@noble/ciphers": "^2.1.1",
78
- "@noble/curves": "^1.9.7",
79
- "@noble/hashes": "^1.7.1",
80
- "@phala/dcap-qvl": "^0.5.2",
81
- "@zed-industries/agent-client-protocol": "^0.4.5",
82
- "patch-package": "^8.0.1",
83
- "pi-mcp-adapter": "^2.11.0",
84
- "pi-privacy": "^0.11.0",
85
- "pi-subagents": "^0.34.0",
86
- "picomatch": "^4.0.4",
87
- "privateer-speak": "0.2.0",
88
- "privateer-workflow": "^0.1.0",
89
- "tinfoil": "^1.1.11",
90
- "tsx": "^4.16.0",
91
- "typebox": "^1.3.4",
92
- "undici": "^7.28.0",
93
- "ws": "^8.21.0",
94
- "yaml": "^2.5.0",
95
- "zod": "^4.0.16"
72
+ "@earendil-works/pi-ai": "0.84.1",
73
+ "@earendil-works/pi-coding-agent": "0.84.1",
74
+ "@earendil-works/pi-tui": "0.84.1",
75
+ "@juicesharp/rpiv-ask-user-question": "2.2.0",
76
+ "@juicesharp/rpiv-web-tools": "1.20.0",
77
+ "@noble/ciphers": "2.2.0",
78
+ "@noble/curves": "1.9.7",
79
+ "@noble/hashes": "1.8.0",
80
+ "@phala/dcap-qvl": "0.5.2",
81
+ "@zed-industries/agent-client-protocol": "0.4.5",
82
+ "patch-package": "8.0.1",
83
+ "pi-mcp-adapter": "2.11.0",
84
+ "pi-privacy": "0.11.0",
85
+ "pi-subagents": "0.34.0",
86
+ "picomatch": "4.0.5",
87
+ "privateer-speak": "0.2.2",
88
+ "privateer-workflow": "0.1.0",
89
+ "tinfoil": "1.1.11",
90
+ "tsx": "4.23.0",
91
+ "typebox": "1.3.4",
92
+ "undici": "7.29.0",
93
+ "ws": "8.21.0",
94
+ "yaml": "2.9.0",
95
+ "zod": "4.4.3"
96
96
  },
97
97
  "devDependencies": {
98
98
  "@types/node": "^20.14.0",