privateer-agent 0.6.4 → 0.6.7
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 +183 -22
- package/SECURITY.md +54 -0
- package/bin/apply-patches.d.mts +18 -0
- package/bin/apply-patches.mjs +143 -0
- package/bin/privateer-launch.mjs +49 -6
- package/package.json +7 -3
- package/src/auth/accountSessions.ts +154 -0
- package/src/auth/privateer.ts +131 -19
- package/src/cli/chat.ts +1 -1
- package/src/config/hosted.ts +36 -0
- package/src/config/paths.ts +9 -0
- package/src/daemon/index.ts +77 -9
- package/src/providers/account.ts +59 -4
- package/src/remote/mcpControl.ts +268 -0
- package/src/remote/relayClient.ts +78 -0
- package/src/remote/remoteBridge.ts +7 -0
- package/src/routines/schema.ts +9 -1
package/README.md
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
curl -fsSL https://privateer.pro/install.sh | sh # installs the `privateer` command
|
|
26
|
+
curl -fsSL https://privateer.pro/install.sh | sh # macOS / Linux — installs the `privateer` command
|
|
27
|
+
irm https://privateer.pro/install.ps1 | iex # Windows (PowerShell)
|
|
27
28
|
npx privateer-agent # or run it instantly, nothing installed
|
|
28
29
|
```
|
|
29
30
|
|
|
@@ -33,9 +34,13 @@ Point it at a frontier model today and a local Ollama model tomorrow — **OpenR
|
|
|
33
34
|
inference), **Venice** / **Fireworks** (no-retention inference), and any **custom
|
|
34
35
|
OpenAI-compatible endpoint** (LM Studio, vLLM, llama.cpp…) are interchangeable at
|
|
35
36
|
`/model` time, including mid-session. No model lock-in, no separate code paths. MCP
|
|
36
|
-
servers, sub-agents, scheduled routines,
|
|
37
|
-
included — and every one of the agent's actions runs
|
|
38
|
-
permission gate**.
|
|
37
|
+
servers, sub-agents, scheduled routines, multi-step workflows, chat-app bridges, and
|
|
38
|
+
one-tap approval from your phone are included — and every one of the agent's actions runs
|
|
39
|
+
through a **safe-by-default permission gate**.
|
|
40
|
+
|
|
41
|
+
Privateer runs in three places, over one account and one config: the **terminal**, a
|
|
42
|
+
**background daemon** for unattended work, and the **Privateer app** on
|
|
43
|
+
[phone, web](https://privateer.pro), and [desktop](#the-privateer-app).
|
|
39
44
|
|
|
40
45
|
## Why Privateer?
|
|
41
46
|
|
|
@@ -95,12 +100,21 @@ silently. The moat is swappable; the floor under it holds.
|
|
|
95
100
|
- **Honest privacy posture, graded.** A verified TEE and a "we promise not to retain"
|
|
96
101
|
policy are **never rendered the same** — the badge tells you exactly how strong the
|
|
97
102
|
guarantee is (cryptographically verified → observable → policy → none).
|
|
98
|
-
- **
|
|
99
|
-
|
|
100
|
-
stays on your machine
|
|
103
|
+
- **Drive it from your phone.** Link the terminal with `/remote-access` (off by default) and
|
|
104
|
+
the Privateer app can send prompts, stream output, and Allow/Deny every action — while
|
|
105
|
+
execution stays on your machine. Sub-agent actions surface for approval the same way.
|
|
106
|
+
- **Manage it from the app.** Extensions, skills, routines, workflows, MCP connectors, and
|
|
107
|
+
chat-app channels are all configurable from your phone or the web app, against any linked
|
|
108
|
+
terminal. See [The Privateer app](#the-privateer-app).
|
|
109
|
+
- **A desktop app.** The same agent hosted inside a local Electron shell — no relay hop, works
|
|
110
|
+
offline, multi-window with per-window MCP connectors. Shares your CLI login and config.
|
|
101
111
|
- **Scheduled routines.** A background daemon runs approved tasks unattended — cron or
|
|
102
112
|
one-off — and the agent can schedule its own follow-up work. Results deliver to a file,
|
|
103
113
|
the next session, your phone, email, or a webhook.
|
|
114
|
+
- **Declarative workflows.** Multi-step agent pipelines as YAML — typed steps, conditional
|
|
115
|
+
routing between them, and `human_gate` steps that pause for your approval and resume.
|
|
116
|
+
- **Chat-app channels.** Bridge the agent into Telegram, Slack, Discord, or WhatsApp with
|
|
117
|
+
role-based approval — admins can approve actions, members are read-only.
|
|
104
118
|
- **MCP servers, sub-agents & skills.** Connect Model Context Protocol servers (local stdio
|
|
105
119
|
or remote HTTP with OAuth), delegate work to bounded parallel sub-agents, and drop in
|
|
106
120
|
skills — all gated like everything else.
|
|
@@ -124,14 +138,47 @@ No install at all: `npx privateer-agent`.
|
|
|
124
138
|
## Install
|
|
125
139
|
|
|
126
140
|
```bash
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
# the one-liner installer — downloads a self-contained bundle, no Node needed:
|
|
142
|
+
curl -fsSL https://privateer.pro/install.sh | sh # macOS / Linux
|
|
143
|
+
irm https://privateer.pro/install.ps1 | iex # Windows (PowerShell)
|
|
144
|
+
|
|
145
|
+
# or via npm, if you'd rather manage it yourself (needs Node ≥ 22.19):
|
|
146
|
+
npm install -g privateer-agent
|
|
147
|
+
npx privateer-agent # run without installing
|
|
132
148
|
```
|
|
133
149
|
|
|
134
|
-
**Requirements:** macOS
|
|
150
|
+
**Requirements:** macOS (arm64/x64), Linux (x64), or Windows (x64). The installers ship a
|
|
151
|
+
**pinned Node runtime inside the bundle**, so you don't need Node or npm on your machine at
|
|
152
|
+
all — Node ≥ 22.19.0 is only required for the `npm` / `npx` path.
|
|
153
|
+
|
|
154
|
+
Update in place with **`privateer update`** (bundle-aware: it re-runs the right installer
|
|
155
|
+
for how you installed) or check your version with `privateer --version`.
|
|
156
|
+
|
|
157
|
+
> **Windows:** the agent's command tool needs a bash, which Windows doesn't ship. Install
|
|
158
|
+
> Git for Windows (or WSL) and Privateer will find it; the launcher checks at startup and
|
|
159
|
+
> tells you how to fix it if not. Override the choice with `shellPath` in
|
|
160
|
+
> `~/.privateer/agent/settings.json`. Linux arm64 and Windows arm64 bundles aren't built
|
|
161
|
+
> yet — arm64 Windows runs the x64 bundle under emulation.
|
|
162
|
+
|
|
163
|
+
### Verifying what you're about to run
|
|
164
|
+
|
|
165
|
+
Privateer is a coding agent — it runs shell commands and edits files, so "should I trust
|
|
166
|
+
this package?" is the right question to ask before `npx`. Two things are checkable
|
|
167
|
+
without taking anyone's word for it:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm view privateer-agent dist.attestations # published from CI with npm provenance:
|
|
171
|
+
# a signed link from this tarball to the
|
|
172
|
+
# exact commit and build that produced it
|
|
173
|
+
npm audit signatures # verify registry signatures + provenance
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The package also declares **no install scripts** — no `postinstall`, nothing. Installing
|
|
177
|
+
it writes files and executes nothing; `npm install -g privateer-agent --ignore-scripts`
|
|
178
|
+
gives an identical result. Code runs only when you run `privateer`.
|
|
179
|
+
|
|
180
|
+
See [SECURITY.md](SECURITY.md) for the threat model, the permission gate, and how to
|
|
181
|
+
report a vulnerability.
|
|
135
182
|
|
|
136
183
|
**From source:**
|
|
137
184
|
|
|
@@ -228,13 +275,106 @@ with `/signout`; manage linked terminals from the app.
|
|
|
228
275
|
> to spend on your account. If someone sends you a code and asks you to approve it, don't —
|
|
229
276
|
> that hands *them* a billed session on *your* account.
|
|
230
277
|
|
|
231
|
-
##
|
|
278
|
+
## The Privateer app
|
|
279
|
+
|
|
280
|
+
The same account drives Privateer from **iOS, Android, [the web app](https://privateer.pro),
|
|
281
|
+
and a desktop app**. The terminal stays where the work happens — the app is a remote control
|
|
282
|
+
and a management surface for it.
|
|
283
|
+
|
|
284
|
+
### Linking a terminal
|
|
285
|
+
|
|
286
|
+
1. Run **`privateer`** and **`/signin`**. It prints a short device code.
|
|
287
|
+
2. Open the app → **Link a terminal**, enter the code (or tap the deep link). No password or
|
|
288
|
+
wallet key ever touches the terminal, and the app pins the terminal's public key on first
|
|
289
|
+
link.
|
|
290
|
+
3. In the terminal, turn on **`/remote-access`** (off by default). The terminal now shows
|
|
291
|
+
**Online** in the app.
|
|
292
|
+
|
|
293
|
+
> **Only approve a code you generated yourself.** Approving someone else's code hands *them*
|
|
294
|
+
> a billed session on *your* account.
|
|
295
|
+
|
|
296
|
+
### What you can do from the app
|
|
297
|
+
|
|
298
|
+
| | |
|
|
299
|
+
|---|---|
|
|
300
|
+
| **Drive a session** | Send prompts, watch streamed output, and **Allow/Deny** each proposed action — including actions from sub-agents the session spawned |
|
|
301
|
+
| **Spawn an agent** | Start a one-shot task on the daemon: *background* (headless, read-only toolset, result sealed to your outbox) or *live* (a fresh drivable session) |
|
|
302
|
+
| **Routines** | Create, edit, pause, run, and delete scheduled unattended tasks |
|
|
303
|
+
| **Workflows** | List, run, and monitor multi-step workflows; answer `human_gate` steps to resume a paused run |
|
|
304
|
+
| **MCP connectors** | Add, edit, and enable MCP servers; credentials are sealed to the terminal and write-only |
|
|
305
|
+
| **Channels** | Configure the Telegram / Slack / Discord / WhatsApp bridges — admins, members, posture, tool ceiling, model |
|
|
306
|
+
| **Extensions & skills** | Install Pi extensions from the catalog; create, edit, and run `SKILL.md` skills |
|
|
307
|
+
|
|
308
|
+
Config changes that carry secrets or executable content (MCP credentials, channel bot
|
|
309
|
+
tokens, workflows with `script` steps) are **sealed to the terminal's pinned key and signed
|
|
310
|
+
by your account** — the relay forwards them blind and can neither read nor forge them.
|
|
311
|
+
|
|
312
|
+
The relay itself is live-only (nothing is archived), carries no API keys, and output is
|
|
313
|
+
size-truncated and run through a best-effort secret redactor before it leaves your machine.
|
|
314
|
+
|
|
315
|
+
### Desktop app
|
|
232
316
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
317
|
+
The desktop app hosts the agent **in-process** and talks to it over loopback IPC — no relay,
|
|
318
|
+
no network hop, and it works offline. It reads the same `~/.privateer` home, so it shares
|
|
319
|
+
your CLI login, model config, and MCP catalog. Multi-window, with a per-window subset of
|
|
320
|
+
your MCP connectors and a native folder picker.
|
|
321
|
+
|
|
322
|
+
Download for [macOS](https://privateer.pro/download/mac) (Apple silicon),
|
|
323
|
+
[macOS Intel](https://privateer.pro/download/mac-intel), or
|
|
324
|
+
[Windows](https://privateer.pro/download/windows).
|
|
325
|
+
|
|
326
|
+
It's an early release and **not yet code-signed or notarized** — macOS will warn on first
|
|
327
|
+
open. Routines and channels deliberately aren't hosted here: those belong to the always-on
|
|
328
|
+
daemon, so background work still wants `privateer daemon install`.
|
|
329
|
+
|
|
330
|
+
## Run it unattended — the daemon
|
|
331
|
+
|
|
332
|
+
A resident background daemon runs scheduled routines, executes workflows, and accepts task
|
|
333
|
+
spawns from the app — with no terminal open.
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
privateer daemon install # install as a login service (auto-starts, reachable from the app)
|
|
337
|
+
privateer daemon status # service installed? daemon answering?
|
|
338
|
+
privateer daemon run # or just run it in the foreground
|
|
339
|
+
privateer daemon uninstall
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Installs as a **launchd user agent** on macOS or a **`systemd --user` unit** on Linux — no
|
|
343
|
+
root, no sudo. (There's no Windows service path yet; use `privateer daemon run`.)
|
|
344
|
+
|
|
345
|
+
Everything the daemon does still runs through the permission gate. Actions needing approval
|
|
346
|
+
surface in the app; routines you approved once run on their own schedule.
|
|
347
|
+
|
|
348
|
+
## Workflows
|
|
349
|
+
|
|
350
|
+
A workflow is a **YAML file describing a multi-step agent pipeline** — a flat graph of typed
|
|
351
|
+
steps (`agent`, `script`, `human_gate`) with conditional routes between them and `{{ }}`
|
|
352
|
+
templating to pass values along. A `human_gate` step pauses the run for your approval and
|
|
353
|
+
resumes when you answer it, including from your phone.
|
|
354
|
+
|
|
355
|
+
The engine ships in the standalone
|
|
356
|
+
[`privateer-workflow`](https://www.npmjs.com/package/privateer-workflow) package. Today the
|
|
357
|
+
user-facing surface is the **app** (save, run, monitor, share) and the **daemon** that
|
|
358
|
+
executes them — there's no `/workflow` command in the terminal yet. Schedule one by pointing
|
|
359
|
+
a routine at it.
|
|
360
|
+
|
|
361
|
+
Because a workflow can carry `script` steps, saving one from the app requires your **account
|
|
362
|
+
signature** — the server can't inject a workflow onto your daemon.
|
|
363
|
+
|
|
364
|
+
## Chat-app channels
|
|
365
|
+
|
|
366
|
+
Bridge the agent into **Telegram, Slack, Discord, or WhatsApp** so you can hand it work from
|
|
367
|
+
a group chat. Each channel has:
|
|
368
|
+
|
|
369
|
+
- **Roles** — `admins` can approve actions; `members` are always read-only, no exceptions.
|
|
370
|
+
- **A posture** — `readonly`, `approve` (default), or `auto`.
|
|
371
|
+
- **A hard tool ceiling** — a per-channel allowlist the agent can't exceed even in `auto`.
|
|
372
|
+
|
|
373
|
+
Configure a channel from the app, or by hand in the `channels` block of
|
|
374
|
+
`~/.privateer/config.json`. Changes take effect on restart, by design. Bot tokens set from
|
|
375
|
+
the app are write-only — the app can name them but never read them back. Note that tokens
|
|
376
|
+
live in plaintext in `config.json` on your machine, and every channel action is appended to
|
|
377
|
+
`~/.privateer/channels-audit.log`.
|
|
238
378
|
|
|
239
379
|
## Permission modes
|
|
240
380
|
|
|
@@ -269,14 +409,35 @@ Everything below is a **Pi extension** loaded by discovery (see [Built on Pi](#b
|
|
|
269
409
|
drop your own into `~/.privateer/agent/extensions/` and it loads the same way, gated like the rest.
|
|
270
410
|
|
|
271
411
|
- **MCP servers** (`pi-mcp-adapter`) — declare them and their tools become first-class, gated
|
|
272
|
-
like the rest (local stdio, or remote HTTP with interactive OAuth).
|
|
273
|
-
-
|
|
274
|
-
|
|
412
|
+
like the rest (local stdio, or remote HTTP with interactive OAuth). One catalog at
|
|
413
|
+
`~/.privateer/agent/mcp-desktop.json` is shared by the CLI, the daemon, and the desktop
|
|
414
|
+
app, so a machine has one coherent connector config.
|
|
415
|
+
- **Sub-agents** (`pi-subagents`) — delegate investigations to bounded parallel agents. Children
|
|
416
|
+
run as headless child processes that **inherit the moat**, so their actions hit the same
|
|
417
|
+
permission gate and their approvals surface on your phone.
|
|
275
418
|
- **Routines** — saved tasks the daemon runs unattended; ask the agent to schedule work and
|
|
276
419
|
approve it once.
|
|
420
|
+
- **Workflows** — declarative multi-step pipelines the daemon executes; see
|
|
421
|
+
[Workflows](#workflows).
|
|
277
422
|
- **Web tools** (`rpiv-web-tools`) — private-by-default web search/fetch with pluggable backends
|
|
278
423
|
(self-hosted SearXNG for fully private search).
|
|
279
424
|
|
|
425
|
+
## Command reference
|
|
426
|
+
|
|
427
|
+
| Command | What it does |
|
|
428
|
+
|---|---|
|
|
429
|
+
| `/model` · `/models` | switch model; `/models` is a searchable picker with TEE/ZDR privacy shields |
|
|
430
|
+
| `/mode` | switch permission mode |
|
|
431
|
+
| `/verify` | fetch and check the TEE attestation for the current model |
|
|
432
|
+
| `/signin` · `/signout` | sign in to a Privateer account (device flow) / sign out |
|
|
433
|
+
| `/remote-access` | link this terminal to the app and allow it to drive (off by default) |
|
|
434
|
+
| `/extensions` | list loaded Pi extensions |
|
|
435
|
+
| `/init` | scaffold a starter `PRIVATEER.md` in this directory |
|
|
436
|
+
| `/update` · `/privateer` | update to the latest release / Privateer status and posture |
|
|
437
|
+
|
|
438
|
+
Shell subcommands: `privateer` (interactive), `privateer update`, `privateer daemon …`,
|
|
439
|
+
`privateer --no-quarter`, `privateer --version`.
|
|
440
|
+
|
|
280
441
|
## Develop
|
|
281
442
|
|
|
282
443
|
```bash
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Report privately via [GitHub Security Advisories](https://github.com/privateer-agent/privateer-agent/security/advisories/new),
|
|
6
|
+
or email **support@privateer.pro**. Please don't open a public issue for anything
|
|
7
|
+
exploitable. Expect an initial response within 72 hours.
|
|
8
|
+
|
|
9
|
+
## What you're running
|
|
10
|
+
|
|
11
|
+
Privateer is a terminal coding agent: it reads and writes files, runs shell commands and
|
|
12
|
+
talks to model providers on your behalf. That is the point of it, and it is also the
|
|
13
|
+
threat model. Two things are worth verifying rather than taking on faith.
|
|
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`.)
|
|
20
|
+
|
|
21
|
+
**Releases carry npm provenance.** Published from `.github/workflows/release.yml` with
|
|
22
|
+
`npm publish --provenance`, so npm holds a signed Sigstore attestation binding the
|
|
23
|
+
tarball to this repository, the exact commit and the workflow run that built it. The npm
|
|
24
|
+
package page shows a verified *"Built and signed on GitHub Actions"* badge linking to
|
|
25
|
+
the build. To check it yourself:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm view privateer-agent dist.attestations # attestation metadata exists
|
|
29
|
+
npm audit signatures # verifies registry signatures + provenance
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If a version lacks provenance, it did not come from this workflow. Treat that as
|
|
33
|
+
suspicious and report it.
|
|
34
|
+
|
|
35
|
+
## The permission gate
|
|
36
|
+
|
|
37
|
+
By default every shell command, file write outside the working directory, and
|
|
38
|
+
destructive tool call stops for explicit approval. This is the moat, and it is the main
|
|
39
|
+
thing standing between a prompt-injected model and your filesystem.
|
|
40
|
+
|
|
41
|
+
`--no-quarter` disables it entirely — every action runs unprompted. It exists for
|
|
42
|
+
trusted, disposable environments (throwaway containers, CI). Do not use it on a machine
|
|
43
|
+
whose contents you care about, and do not use it on a repository or task involving
|
|
44
|
+
untrusted content: a coding agent reading an attacker-controlled file is a realistic
|
|
45
|
+
injection path.
|
|
46
|
+
|
|
47
|
+
## Keys and credentials
|
|
48
|
+
|
|
49
|
+
Provider API keys and account credentials live under `~/.privateer/` on your machine and
|
|
50
|
+
are sent only to the provider you selected. Bot tokens for messaging channels are sealed
|
|
51
|
+
to a terminal keypair before they reach our relay, and channel configuration is verified
|
|
52
|
+
against a link-pinned account key — the relay can neither read those tokens nor forge
|
|
53
|
+
configuration. Architecture and residual risks are documented in
|
|
54
|
+
`docs/daemon-channels-and-app.md`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Types for the launcher's patch/resolve helpers. The implementation is plain .mjs
|
|
2
|
+
// because bin/ must run under a bare `node` with no transpiler (the launcher is the
|
|
3
|
+
// very first thing to execute, before tsx/jiti are in play).
|
|
4
|
+
|
|
5
|
+
/** Directory CONTAINING the node_modules that holds `name`, or null if not installed. */
|
|
6
|
+
export function findDepRoot(from: string, name: string): string | null;
|
|
7
|
+
|
|
8
|
+
/** Absolute path to a file inside an installed dependency, or null if absent. */
|
|
9
|
+
export function resolveDep(from: string, name: string, ...rest: string[]): string | null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Apply `patches/` into whichever node_modules the targets landed in. Idempotent and
|
|
13
|
+
* best-effort; see the implementation for why this runs at launch, not on install.
|
|
14
|
+
*/
|
|
15
|
+
export function applyPatchesIfNeeded(
|
|
16
|
+
repo: string,
|
|
17
|
+
nodeBin?: string,
|
|
18
|
+
): "current" | "applied" | "skipped" | "failed";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Apply the patches/ directory into node_modules — at LAUNCH, not at install.
|
|
2
|
+
//
|
|
3
|
+
// Why not a `postinstall` script (the obvious place)? Because `npm install` /
|
|
4
|
+
// `npx privateer-agent` would then execute our code on the user's machine BEFORE
|
|
5
|
+
// they ever decided to run Privateer. That is exactly the install-time-execution
|
|
6
|
+
// risk a careful reviewer — human or agent — flags on an unfamiliar package, and
|
|
7
|
+
// it is the one npm-side signal we can remove outright. With no install scripts,
|
|
8
|
+
// `npm install -g privateer-agent --ignore-scripts` is completely inert: it writes
|
|
9
|
+
// files and runs nothing. Patching moves to the first actual launch, which the
|
|
10
|
+
// user explicitly asked for.
|
|
11
|
+
//
|
|
12
|
+
// Contract: idempotent, cheap on the hot path (a stamp file short-circuits every
|
|
13
|
+
// launch after the first), and BEST-EFFORT — every patch here is a UX/robustness
|
|
14
|
+
// improvement, never a correctness prerequisite, so a failure to apply degrades to
|
|
15
|
+
// stock Pi behaviour rather than blocking launch. That matters for the common
|
|
16
|
+
// `sudo npm install -g` case, where node_modules is root-owned and an unprivileged
|
|
17
|
+
// launch simply cannot write to it.
|
|
18
|
+
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
20
|
+
import crypto from "node:crypto";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
|
|
24
|
+
// Bump when the applier's own semantics change, to force a re-apply on upgrade.
|
|
25
|
+
const STAMP_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Where do our patch targets actually live?
|
|
29
|
+
*
|
|
30
|
+
* NOT necessarily `<repo>/node_modules`. When Privateer is installed as a dependency
|
|
31
|
+
* (`npm i privateer-agent`, `npx privateer-agent`), npm HOISTS pi-coding-agent to the
|
|
32
|
+
* parent project's node_modules and leaves us with no node_modules of our own — so
|
|
33
|
+
* assuming a local one means the patches silently never apply. Resolve the real target
|
|
34
|
+
* from our own package instead, and return the directory that CONTAINS the node_modules
|
|
35
|
+
* it landed in: that is the cwd patch-package needs, in every layout (hoisted, nested,
|
|
36
|
+
* global, bundled).
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Find the directory CONTAINING the node_modules that holds `name`, starting at
|
|
40
|
+
* `from` and walking up. Returns null if the dependency isn't installed anywhere.
|
|
41
|
+
*
|
|
42
|
+
* This is the one resolution primitive both the launcher and the patcher need,
|
|
43
|
+
* because `<repo>/node_modules` is NOT where dependencies reliably live:
|
|
44
|
+
* - `npm i -g privateer-agent` -> nested: <repo>/node_modules/<name>
|
|
45
|
+
* - `npx privateer-agent` -> hoisted: <repo>/../node_modules/<name>
|
|
46
|
+
* - `npm i privateer-agent` -> hoisted into the host project
|
|
47
|
+
* Hardcoding the nested case silently breaks every hoisted install.
|
|
48
|
+
*
|
|
49
|
+
* We walk directories rather than using require.resolve because modern packages
|
|
50
|
+
* (pi-coding-agent among them) declare an `exports` map with no "./package.json"
|
|
51
|
+
* entry, so require.resolve throws ERR_PACKAGE_PATH_NOT_EXPORTED even when the
|
|
52
|
+
* package is sitting right there. Directory lookup sees through `exports`.
|
|
53
|
+
*/
|
|
54
|
+
export function findDepRoot(from, name) {
|
|
55
|
+
const segs = name.split("/");
|
|
56
|
+
for (let dir = path.resolve(from); ; dir = path.dirname(dir)) {
|
|
57
|
+
if (fs.existsSync(path.join(dir, "node_modules", ...segs, "package.json"))) return dir;
|
|
58
|
+
const parent = path.dirname(dir);
|
|
59
|
+
if (parent === dir) return null; // hit the filesystem root
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Absolute path to an installed dependency's file, or null if the dep isn't present. */
|
|
64
|
+
export function resolveDep(from, name, ...rest) {
|
|
65
|
+
const root = findDepRoot(from, name);
|
|
66
|
+
return root ? path.join(root, "node_modules", ...name.split("/"), ...rest) : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolvePatchRoots(repo, patchFiles) {
|
|
70
|
+
const roots = new Set();
|
|
71
|
+
for (const file of patchFiles) {
|
|
72
|
+
// "@earendil-works+pi-coding-agent+0.80.3.patch" -> "@earendil-works/pi-coding-agent"
|
|
73
|
+
const parts = path.basename(file, ".patch").split("+");
|
|
74
|
+
const name = parts[0].startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
75
|
+
const root = findDepRoot(repo, name);
|
|
76
|
+
if (root) roots.add(root);
|
|
77
|
+
}
|
|
78
|
+
return [...roots];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** sha256 over every patch file's name + contents — the identity of "what should be applied". */
|
|
82
|
+
function patchSetHash(patchDir, files) {
|
|
83
|
+
const h = crypto.createHash("sha256").update(String(STAMP_VERSION));
|
|
84
|
+
for (const f of files) {
|
|
85
|
+
h.update(f);
|
|
86
|
+
h.update(fs.readFileSync(path.join(patchDir, f)));
|
|
87
|
+
}
|
|
88
|
+
return h.digest("hex");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Ensure patches/ is applied to repo/node_modules. Returns one of:
|
|
93
|
+
* "current" — already applied (stamp matches); nothing done
|
|
94
|
+
* "applied" — patches were just applied successfully
|
|
95
|
+
* "skipped" — nothing to do (no patches / no node_modules / patch-package absent)
|
|
96
|
+
* "failed" — apply was attempted and did not succeed (caller may warn)
|
|
97
|
+
*/
|
|
98
|
+
export function applyPatchesIfNeeded(repo, nodeBin = process.execPath) {
|
|
99
|
+
try {
|
|
100
|
+
repo = path.resolve(repo); // roots come back absolute; a relative repo would break path.relative
|
|
101
|
+
const patchDir = path.join(repo, "patches");
|
|
102
|
+
if (!fs.existsSync(patchDir)) return "skipped";
|
|
103
|
+
const patchFiles = fs.readdirSync(patchDir).filter((f) => f.endsWith(".patch")).sort();
|
|
104
|
+
if (patchFiles.length === 0) return "skipped";
|
|
105
|
+
|
|
106
|
+
const want = patchSetHash(patchDir, patchFiles);
|
|
107
|
+
const roots = resolvePatchRoots(repo, patchFiles);
|
|
108
|
+
if (roots.length === 0) return "skipped"; // targets not installed
|
|
109
|
+
|
|
110
|
+
// patch-package is a runtime dependency precisely so this works post-install.
|
|
111
|
+
const pp = resolveDep(repo, "patch-package", "index.js");
|
|
112
|
+
if (!pp || !fs.existsSync(pp)) return "skipped";
|
|
113
|
+
|
|
114
|
+
let did = false;
|
|
115
|
+
for (const root of roots) {
|
|
116
|
+
const stampFile = path.join(root, "node_modules", ".privateer-patches.json");
|
|
117
|
+
try {
|
|
118
|
+
if (JSON.parse(fs.readFileSync(stampFile, "utf8")).hash === want) continue; // current
|
|
119
|
+
} catch { /* missing or unreadable stamp — (re)apply */ }
|
|
120
|
+
|
|
121
|
+
// --patch-dir points at OUR patches even though cwd is wherever the deps landed.
|
|
122
|
+
// It MUST be relative: patch-package resolves it against cwd, so an absolute path
|
|
123
|
+
// is silently mangled into a non-existent one and every patch "fails" to apply.
|
|
124
|
+
const relPatchDir = path.relative(root, patchDir);
|
|
125
|
+
const r = spawnSync(nodeBin, [pp, "--error-on-fail", "--patch-dir", relPatchDir], {
|
|
126
|
+
cwd: root,
|
|
127
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
128
|
+
timeout: 60_000,
|
|
129
|
+
windowsHide: true,
|
|
130
|
+
});
|
|
131
|
+
if (r.status !== 0) return "failed";
|
|
132
|
+
did = true;
|
|
133
|
+
|
|
134
|
+
// Only stamp after a clean apply, so a partial/failed run retries next launch.
|
|
135
|
+
try {
|
|
136
|
+
fs.writeFileSync(stampFile, JSON.stringify({ hash: want, at: new Date().toISOString() }) + "\n");
|
|
137
|
+
} catch { /* unwritable node_modules — applied fine, we just re-check next launch */ }
|
|
138
|
+
}
|
|
139
|
+
return did ? "applied" : "current";
|
|
140
|
+
} catch {
|
|
141
|
+
return "failed";
|
|
142
|
+
}
|
|
143
|
+
}
|
package/bin/privateer-launch.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import fs from "node:fs";
|
|
|
17
17
|
import os from "node:os";
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
20
|
+
import { applyPatchesIfNeeded, resolveDep } from "./apply-patches.mjs";
|
|
20
21
|
|
|
21
22
|
const HERE = path.dirname(fileURLToPath(import.meta.url)); // bin/
|
|
22
23
|
const REPO = path.resolve(HERE, "..");
|
|
@@ -72,7 +73,8 @@ const sub = args[0];
|
|
|
72
73
|
if (sub === "--version" || sub === "-V") {
|
|
73
74
|
const ver = (p) => { try { return JSON.parse(fs.readFileSync(p, "utf8")).version; } catch { return null; } };
|
|
74
75
|
const pv = ver(path.join(REPO, "package.json")) || "unknown";
|
|
75
|
-
const
|
|
76
|
+
const piPkg = resolveDep(REPO, "@earendil-works/pi-coding-agent", "package.json");
|
|
77
|
+
const pi = piPkg ? ver(piPkg) : null;
|
|
76
78
|
console.log(`privateer ${pv}${pi ? ` (pi ${pi})` : ""}`);
|
|
77
79
|
process.exit(0);
|
|
78
80
|
}
|
|
@@ -126,6 +128,13 @@ else {
|
|
|
126
128
|
// time the agent tries to run a command. Unix always has a shell, so this is a no-op.
|
|
127
129
|
ensureShellOrExit();
|
|
128
130
|
|
|
131
|
+
// Apply our pi-coding-agent patches. This happens HERE, on a launch the user asked
|
|
132
|
+
// for, rather than in a postinstall — so installing the package runs no code at all.
|
|
133
|
+
// Stamped, so it's a single file read on every launch after the first. Best-effort:
|
|
134
|
+
// both patches are UX fixes, so a root-owned node_modules (sudo npm i -g) just means
|
|
135
|
+
// stock Pi behaviour, not a broken boot. Bundles ship pre-patched and no-op here.
|
|
136
|
+
ensurePatches();
|
|
137
|
+
|
|
129
138
|
const AGENT_DIR = path.join(PRIVATEER_HOME, "agent");
|
|
130
139
|
const EXT_DIR = path.join(AGENT_DIR, "extensions");
|
|
131
140
|
fs.mkdirSync(EXT_DIR, { recursive: true });
|
|
@@ -142,9 +151,17 @@ else {
|
|
|
142
151
|
for (const name of MANAGED) fs.rmSync(path.join(EXT_DIR, `${name}.ts`), { force: true });
|
|
143
152
|
|
|
144
153
|
const ext = (...p) => path.join(REPO, "extensions", ...p);
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
// Resolve dependencies by walking the node_modules chain, NOT as REPO/node_modules.
|
|
155
|
+
// npm only nests deps under us for a global install; `npx privateer-agent` and
|
|
156
|
+
// `npm i privateer-agent` HOIST them to a sibling/parent node_modules, where the
|
|
157
|
+
// hardcoded path resolves to nothing and every shim below points at a missing file.
|
|
158
|
+
const dep = (name, ...rest) => resolveDep(REPO, name, ...rest);
|
|
159
|
+
// A missing target means that optional tool pack isn't installed — skip its shim
|
|
160
|
+
// rather than writing one that points at nothing (which fails at extension load).
|
|
161
|
+
const shim = (name, target) => {
|
|
162
|
+
if (!target || !fs.existsSync(target)) return;
|
|
147
163
|
fs.writeFileSync(path.join(EXT_DIR, `${name}.ts`), `export { default } from ${JSON.stringify(pathToFileURL(target).href)};\n`);
|
|
164
|
+
};
|
|
148
165
|
|
|
149
166
|
shim("privateer-brand", ext("privateer-brand.ts")); // banner, ⚓ badge, /signin /signout
|
|
150
167
|
shim("privateer-context", ext("privateer-context.ts")); // PRIVATEER.md context + /init
|
|
@@ -154,12 +171,21 @@ else {
|
|
|
154
171
|
shim("privateer-posture", ext("privateer-posture.ts"));
|
|
155
172
|
shim("privateer-tools", ext("privateer-tools.ts"));
|
|
156
173
|
shim("privateer-privacy", ext("privateer-privacy.ts")); // pi-privacy + account tier resolver
|
|
157
|
-
shim("rpiv-web-tools", dep("@juicesharp
|
|
174
|
+
shim("rpiv-web-tools", dep("@juicesharp/rpiv-web-tools", "index.ts")); // private web tools
|
|
158
175
|
shim("pi-mcp-adapter", dep("pi-mcp-adapter", "index.ts"));
|
|
159
|
-
shim("pi-hypa", dep("@hypabolic
|
|
176
|
+
shim("pi-hypa", dep("@hypabolic/pi-hypa", "extensions", "index.ts"));
|
|
160
177
|
shim("pi-subagents", dep("pi-subagents", "src", "extension", "index.ts"));
|
|
161
178
|
|
|
162
|
-
|
|
179
|
+
// Unlike the tool packs above, Pi's CLI is not optional — it IS the agent. If it
|
|
180
|
+
// didn't resolve, the install is broken; say so instead of spawning `undefined`.
|
|
181
|
+
const CLI = dep("@earendil-works/pi-coding-agent", "dist", "cli.js");
|
|
182
|
+
if (!CLI || !fs.existsSync(CLI)) {
|
|
183
|
+
console.error(
|
|
184
|
+
"privateer: couldn't find pi-coding-agent — the install looks incomplete.\n" +
|
|
185
|
+
" Try reinstalling: npm install -g privateer-agent@latest",
|
|
186
|
+
);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
163
189
|
process.env.PI_CODING_AGENT_DIR = AGENT_DIR;
|
|
164
190
|
// The binary pi-subagents spawns for each child. Point it at OUR cli.js so the child
|
|
165
191
|
// reads this same PI_CODING_AGENT_DIR and DISCOVERS the moat shims (gated + private,
|
|
@@ -286,6 +312,23 @@ function findWindowsBash() {
|
|
|
286
312
|
return null;
|
|
287
313
|
}
|
|
288
314
|
|
|
315
|
+
// Run the patch applier and, on the one interesting outcome (we tried and couldn't),
|
|
316
|
+
// tell the user why in a way they can act on. "current"/"applied"/"skipped" are silent.
|
|
317
|
+
function ensurePatches() {
|
|
318
|
+
if (applyPatchesIfNeeded(REPO, NODE_BIN) !== "failed") return;
|
|
319
|
+
process.stderr.write(
|
|
320
|
+
[
|
|
321
|
+
"",
|
|
322
|
+
" ⚓ Couldn't apply Privateer's bundled patches to node_modules — continuing without them.",
|
|
323
|
+
" Two upstream fixes (retry-loop guard, /model → /models redirect) stay off.",
|
|
324
|
+
` Usually a permissions issue: ${path.join(REPO, "node_modules")} isn't writable`,
|
|
325
|
+
" by this user (a `sudo npm install -g` install). Re-run once with sudo, or",
|
|
326
|
+
" install without sudo (nvm, or an npm prefix you own) to fix it for good.",
|
|
327
|
+
"",
|
|
328
|
+
].join("\n") + "\n",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
289
332
|
function haveTinfoilKey() {
|
|
290
333
|
return haveKey("TINFOIL_API_KEY");
|
|
291
334
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "Privateer — a provider-agnostic, safe-by-default terminal coding agent with TEE/Tinfoil attestation, rebuilt on the Pi toolkit. Bring your own model across 20 providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/privateer-agent/privateer-agent.git"
|
|
11
11
|
},
|
|
12
|
+
"homepage": "https://privateer.pro",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/privateer-agent/privateer-agent/issues"
|
|
15
|
+
},
|
|
12
16
|
"keywords": [
|
|
13
17
|
"ai",
|
|
14
18
|
"coding-agent",
|
|
@@ -39,6 +43,7 @@
|
|
|
39
43
|
"extensions",
|
|
40
44
|
"skills",
|
|
41
45
|
"patches",
|
|
46
|
+
"SECURITY.md",
|
|
42
47
|
"README.md",
|
|
43
48
|
"LICENSE"
|
|
44
49
|
],
|
|
@@ -48,8 +53,7 @@
|
|
|
48
53
|
"channels": "node --env-file=.env --import tsx src/channels/run.ts",
|
|
49
54
|
"dev": "tsx watch src/main.ts",
|
|
50
55
|
"typecheck": "tsc --noEmit",
|
|
51
|
-
"test": "for f in tests/*.test.ts; do node --import tsx --test \"$f\" || exit 1; done"
|
|
52
|
-
"postinstall": "patch-package --error-on-fail"
|
|
56
|
+
"test": "for f in tests/*.test.ts; do node --import tsx --test \"$f\" || exit 1; done"
|
|
53
57
|
},
|
|
54
58
|
"engines": {
|
|
55
59
|
"node": ">=22.19.0"
|