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 +29 -10
- package/SECURITY.md +41 -5
- package/bin/privateer-launch.mjs +10 -0
- package/bin/privateer-verify.d.mts +10 -0
- package/bin/privateer-verify.mjs +289 -0
- package/extensions/privateer-brand.ts +8 -2
- package/extensions/privateer-hints.ts +8 -2
- package/package.json +25 -25
- package/patches/{@earendil-works+pi-coding-agent+0.80.3.patch → @earendil-works+pi-coding-agent+0.84.1.patch} +138 -70
- package/src/acp/run.ts +5 -4
- package/src/channels/run.ts +3 -1
- package/src/cli/chat.ts +7 -5
- package/src/harbor/index.ts +7 -6
- package/src/providers/account.ts +37 -29
- package/src/providers/piAuthStore.ts +129 -0
- package/src/remote/liveTaskSession.ts +6 -6
- package/src/session.ts +26 -9
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; **
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 **
|
|
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
|
|
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 (**
|
|
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
|
-
**
|
|
16
|
-
`postinstall` or `prepare` hook
|
|
17
|
-
|
|
18
|
-
runs only when you run `privateer`. (Dependency patching happens at first launch —
|
|
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
|
package/bin/privateer-launch.mjs
CHANGED
|
@@ -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
|
-
|
|
408
|
-
|
|
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 :
|
|
55
|
+
return typeof shortcut === "string" ? shortcut : DEFAULT_TALK_KEY;
|
|
50
56
|
} catch {
|
|
51
|
-
return
|
|
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.
|
|
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": "
|
|
73
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
74
|
-
"@earendil-works/pi-tui": "
|
|
75
|
-
"@juicesharp/rpiv-ask-user-question": "
|
|
76
|
-
"@juicesharp/rpiv-web-tools": "
|
|
77
|
-
"@noble/ciphers": "
|
|
78
|
-
"@noble/curves": "
|
|
79
|
-
"@noble/hashes": "
|
|
80
|
-
"@phala/dcap-qvl": "
|
|
81
|
-
"@zed-industries/agent-client-protocol": "
|
|
82
|
-
"patch-package": "
|
|
83
|
-
"pi-mcp-adapter": "
|
|
84
|
-
"pi-privacy": "
|
|
85
|
-
"pi-subagents": "
|
|
86
|
-
"picomatch": "
|
|
87
|
-
"privateer-speak": "0.2.
|
|
88
|
-
"privateer-workflow": "
|
|
89
|
-
"tinfoil": "
|
|
90
|
-
"tsx": "
|
|
91
|
-
"typebox": "
|
|
92
|
-
"undici": "
|
|
93
|
-
"ws": "
|
|
94
|
-
"yaml": "
|
|
95
|
-
"zod": "
|
|
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",
|