privateer-agent 0.12.30 → 0.12.32
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/bin/privateer-launch.mjs +29 -0
- package/bin/privateer-splash.mjs +73 -11
- package/extensions/privateer-brand.ts +7 -0
- package/extensions/privateer-computer.ts +24 -0
- package/native/linux/privateer-computer.mjs +536 -0
- package/native/win/PrivateerComputer.ps1 +502 -0
- package/package.json +2 -1
- package/src/acp/run.ts +6 -1
- package/src/cli/chat.ts +7 -1
- package/src/computer/helper.ts +426 -0
- package/src/computer/preview.ts +124 -0
- package/src/computer/space.ts +289 -0
- package/src/config/computerControl.ts +67 -0
- package/src/config/moat.ts +46 -6
- package/src/config/moatManifest.json +1 -0
- package/src/config/privacyPolicy.ts +40 -1
- package/src/config/relayExposure.ts +26 -0
- package/src/ext/permissionGate.ts +25 -0
- package/src/harbor/index.ts +20 -0
- package/src/harbor/ipc.ts +4 -1
- package/src/permissions/classify.ts +92 -0
- package/src/permissions/gate.ts +55 -1
- package/src/permissions/mode.ts +10 -0
- package/src/providers/account.ts +6 -0
- package/src/providers/defaultModel.ts +42 -7
- package/src/remote/relayClient.ts +34 -1
- package/src/tools/computer.ts +442 -0
- package/src/util/gzipRequestBody.ts +150 -0
package/bin/privateer-launch.mjs
CHANGED
|
@@ -165,6 +165,35 @@ if (NO_QUARTER) {
|
|
|
165
165
|
);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
// `--allow-computer-control` — arm GUI control (screenshots, mouse, keyboard) for this
|
|
169
|
+
// session. Stripped before Pi's cli.js sees it, exactly like --no-quarter above.
|
|
170
|
+
//
|
|
171
|
+
// A FLAG AND NOT A SETTING, deliberately. Screen control is the one capability that
|
|
172
|
+
// reaches outside every other limit the gate enforces — a mouse can open a terminal and
|
|
173
|
+
// type what the denylist would have caught — so it is armed per session, by the person
|
|
174
|
+
// starting it, rather than left on in a config file from a week ago. Arming is not
|
|
175
|
+
// approving: every action still prompts (permissions/mode.ts), and the tools do not
|
|
176
|
+
// exist at all without this (config/computerControl.ts).
|
|
177
|
+
const ALLOW_COMPUTER = args.some((a) => a === "--allow-computer-control");
|
|
178
|
+
if (ALLOW_COMPUTER) {
|
|
179
|
+
for (let i = args.length - 1; i >= 0; i--) if (args[i] === "--allow-computer-control") args.splice(i, 1);
|
|
180
|
+
process.env.PRIVATEER_COMPUTER_CONTROL = "1";
|
|
181
|
+
process.stderr.write(
|
|
182
|
+
[
|
|
183
|
+
"",
|
|
184
|
+
" ⚓ \x1b[1;33mScreen control armed\x1b[0m — this session may see your screen and move your mouse.",
|
|
185
|
+
" Every action still asks first. It can reach anything you can: other apps, other windows,",
|
|
186
|
+
" a browser you are signed into. Your OS will also ask for screen and accessibility permission.",
|
|
187
|
+
NO_QUARTER
|
|
188
|
+
? " \x1b[1;31mNo quarter is ALSO on — screen actions will run with NO prompt.\x1b[0m"
|
|
189
|
+
: "",
|
|
190
|
+
"",
|
|
191
|
+
]
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
.join("\n") + "\n",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
168
197
|
const sub = args[0];
|
|
169
198
|
|
|
170
199
|
// `privateer --version` — report OUR version, not Pi's. Left to Pi's cli.js it would
|
package/bin/privateer-splash.mjs
CHANGED
|
@@ -37,8 +37,19 @@
|
|
|
37
37
|
// never lands in front of real output.
|
|
38
38
|
//
|
|
39
39
|
// The wave is drawn on STDERR; stdout belongs to the TUI's canvas.
|
|
40
|
+
//
|
|
41
|
+
// WHY EVERY WRITE OF OURS IS fs.writeSync. On Windows a write to a TTY stream is
|
|
42
|
+
// ASYNCHRONOUS — process.stderr.write only queues the bytes for the event loop — and the
|
|
43
|
+
// whole point of this file is that Pi's boot never gives the event loop a turn. Through
|
|
44
|
+
// process.stderr, every erase we issue during the wait would land after the output it was
|
|
45
|
+
// meant to clear, and the cursor restore on `exit` would never flush at all: a Windows
|
|
46
|
+
// console left with wave fragments in front of Pi's first frame and no cursor afterwards.
|
|
47
|
+
// fs.writeSync goes straight to fd 2, which is also what the drawing thread uses, so the
|
|
48
|
+
// two threads' output stays in the order it was issued. Pi's OWN stderr still goes
|
|
49
|
+
// through the stream — that write belongs to the caller, return value and all.
|
|
40
50
|
|
|
41
51
|
import { spawnSync } from "node:child_process";
|
|
52
|
+
import fs from "node:fs";
|
|
42
53
|
import path from "node:path";
|
|
43
54
|
import { Worker } from "node:worker_threads";
|
|
44
55
|
|
|
@@ -62,6 +73,32 @@ if (enabled && process.platform === "win32") {
|
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
// WHICH GLYPHS THE CONSOLE CAN ACTUALLY DRAW. Code page 65001 above settles the ENCODING;
|
|
77
|
+
// it says nothing about the FONT. Legacy conhost — a plain cmd.exe or PowerShell window,
|
|
78
|
+
// which is still what `privateer` gets when it isn't launched from Windows Terminal —
|
|
79
|
+
// defaults to Lucida Console or a raster font, and those cover exactly the CP437 block
|
|
80
|
+
// elements (█ ▄ ▀ ░ ▒ ▓) and nothing else. The eighth-block ramp, the anchor and the
|
|
81
|
+
// ellipsis are all absent there, so the "wave" drew as a row of tofu boxes that changed
|
|
82
|
+
// shape every frame. Every modern host announces itself in the environment (Windows
|
|
83
|
+
// Terminal, VS Code, ConEmu/ANSICON, anything mintty-ish that sets TERM), and on those the
|
|
84
|
+
// eighth blocks are the better picture, so the fallback is only for the ones that don't.
|
|
85
|
+
const legacyConsole =
|
|
86
|
+
process.platform === "win32" &&
|
|
87
|
+
!(
|
|
88
|
+
process.env.WT_SESSION ||
|
|
89
|
+
process.env.WT_PROFILE_ID ||
|
|
90
|
+
process.env.TERM_PROGRAM ||
|
|
91
|
+
process.env.ConEmuANSI ||
|
|
92
|
+
process.env.ANSICON ||
|
|
93
|
+
process.env.TERM
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Eight levels either way, so the wave keeps its shape: height where the font has the
|
|
97
|
+
// eighth blocks, density where it only has the CP437 shades.
|
|
98
|
+
const BLOCKS = legacyConsole ? " \u2591\u2591\u2592\u2592\u2593\u2593\u2588" : "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
99
|
+
const ANCHOR = legacyConsole ? "~" : "\u2693";
|
|
100
|
+
const ELLIPSIS = legacyConsole ? "..." : "\u2026";
|
|
101
|
+
|
|
65
102
|
// Bytes of stdout after TUI.start() that mean "this is the first frame, not a control
|
|
66
103
|
// sequence". Everything Pi writes between raw mode and the frame is short (the paste
|
|
67
104
|
// toggle, a Kitty protocol query, the cursor hide, an OSC window title — 42 bytes all
|
|
@@ -86,10 +123,17 @@ if (enabled) {
|
|
|
86
123
|
const sab = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
|
|
87
124
|
const state = new Int32Array(sab);
|
|
88
125
|
|
|
89
|
-
// Room for " ⚓ " + wave + message + elapsed, clamped so a narrow
|
|
90
|
-
// wrap (a wrapped line survives our `\r\x1b[K` erase only on its last
|
|
126
|
+
// Room for " ⚓ " + wave + " " + message + ellipsis + elapsed, clamped so a narrow
|
|
127
|
+
// terminal doesn't wrap (a wrapped line survives our `\r\x1b[K` erase only on its last
|
|
128
|
+
// row). The reserve is measured rather than guessed, because the pieces are no longer
|
|
129
|
+
// fixed: the anchor is TWO cells wherever ⚓ keeps its emoji presentation, one where it
|
|
130
|
+
// fell back to ASCII, and the ellipsis is one cell or three.
|
|
131
|
+
const MSGS = ["hoisting sail", "raising the colours"];
|
|
132
|
+
const anchorCells = ANCHOR === "\u2693" ? 2 : 1; // U+2693 carries emoji presentation
|
|
133
|
+
const reserve =
|
|
134
|
+
2 + anchorCells + 1 + 1 + Math.max(...MSGS.map((m) => m.length)) + ELLIPSIS.length + 5;
|
|
91
135
|
const cols = err.columns && err.columns > 0 ? err.columns : 80;
|
|
92
|
-
const width = Math.max(6, Math.min(28, cols -
|
|
136
|
+
const width = Math.max(6, Math.min(28, cols - reserve));
|
|
93
137
|
|
|
94
138
|
// The worker source is plain logic with no escape sequences of its own — every ANSI
|
|
95
139
|
// string is handed over in workerData, so nothing here has to survive two rounds of
|
|
@@ -122,7 +166,7 @@ if (enabled) {
|
|
|
122
166
|
const secs = Math.round((Date.now() - t0) / 1000);
|
|
123
167
|
const msg = w.msgs[Atomics.load(s, PHASE)];
|
|
124
168
|
const age = secs >= 3 ? w.dim + " " + secs + "s" + w.off : "";
|
|
125
|
-
fs.writeSync(2, w.cr + " " + w.anchor + " " + wave(frame++ * 0.35) + " " + w.dim + msg +
|
|
169
|
+
fs.writeSync(2, w.cr + " " + w.anchor + " " + wave(frame++ * 0.35) + " " + w.dim + msg + w.ellipsis + w.off + age + w.clearEol);
|
|
126
170
|
}
|
|
127
171
|
|
|
128
172
|
// Atomics.wait doubles as the sleep: an exact 80ms tick that the main thread can cut
|
|
@@ -146,9 +190,10 @@ if (enabled) {
|
|
|
146
190
|
sab,
|
|
147
191
|
width,
|
|
148
192
|
hold: HOLD_MS,
|
|
149
|
-
blocks:
|
|
150
|
-
msgs:
|
|
151
|
-
|
|
193
|
+
blocks: BLOCKS,
|
|
194
|
+
msgs: MSGS,
|
|
195
|
+
ellipsis: ELLIPSIS,
|
|
196
|
+
anchor: `\x1b[38;5;69m${ANCHOR}\x1b[0m`,
|
|
152
197
|
crest: "\x1b[38;5;109m",
|
|
153
198
|
trough: "\x1b[38;5;67m",
|
|
154
199
|
dim: "\x1b[2m",
|
|
@@ -176,8 +221,23 @@ if (enabled) {
|
|
|
176
221
|
if (!Atomics.load(state, ACK)) Atomics.wait(state, ACK, 0, 50);
|
|
177
222
|
}
|
|
178
223
|
|
|
224
|
+
// Our own control sequences, written straight to fd 2 and synchronously — see the note
|
|
225
|
+
// at the top of the file for why process.stderr will not do. A short write or an EAGAIN
|
|
226
|
+
// from a non-blocking tty is retried; anything else is swallowed, because a splash is
|
|
227
|
+
// never worth a crash.
|
|
228
|
+
function writeCtl(s) {
|
|
229
|
+
const buf = Buffer.from(s, "utf8");
|
|
230
|
+
for (let off = 0, tries = 0; off < buf.length && tries < 100; tries++) {
|
|
231
|
+
try {
|
|
232
|
+
off += fs.writeSync(2, buf, off);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
if (e?.code !== "EAGAIN") return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
179
239
|
function clearLine() {
|
|
180
|
-
if (Atomics.load(state, DREW))
|
|
240
|
+
if (Atomics.load(state, DREW)) writeCtl("\r\x1b[K");
|
|
181
241
|
}
|
|
182
242
|
|
|
183
243
|
function stop() {
|
|
@@ -187,7 +247,7 @@ if (enabled) {
|
|
|
187
247
|
clearLine();
|
|
188
248
|
// Only give the cursor back if Pi hasn't deliberately hidden it — the TUI hides it
|
|
189
249
|
// for the whole session and would never get the chance to hide it again.
|
|
190
|
-
if (Atomics.load(state, DREW) && !appHidCursor)
|
|
250
|
+
if (Atomics.load(state, DREW) && !appHidCursor) writeCtl("\x1b[?25h");
|
|
191
251
|
process.stdout.write = outWrite;
|
|
192
252
|
err.write = errWrite;
|
|
193
253
|
worker.terminate();
|
|
@@ -246,10 +306,12 @@ if (enabled) {
|
|
|
246
306
|
} catch (e) {
|
|
247
307
|
if (e?.code !== "EIO") throw e;
|
|
248
308
|
stop();
|
|
249
|
-
errWrite(
|
|
309
|
+
// writeCtl, not errWrite: process.exit() below does not flush a stream write that
|
|
310
|
+
// Windows has merely queued, and this message is the only thing the user gets.
|
|
311
|
+
writeCtl(
|
|
250
312
|
[
|
|
251
313
|
"",
|
|
252
|
-
|
|
314
|
+
` ${ANCHOR} Privateer couldn't take the helm — this terminal stopped accepting keyboard`,
|
|
253
315
|
" control while the agent was still loading (setRawMode EIO).",
|
|
254
316
|
"",
|
|
255
317
|
" That usually means the window, tab or ssh session it started in went away.",
|
|
@@ -615,6 +615,13 @@ export default function privateerBrand(pi: any): void {
|
|
|
615
615
|
ctx?.ui?.notify?.(`Signed in. Run /models to pick a model — ${spec} isn't loaded yet.`, "warning");
|
|
616
616
|
return;
|
|
617
617
|
}
|
|
618
|
+
// Not persisted, on purpose. Every DELIBERATE switch now writes itself into Pi's
|
|
619
|
+
// settings.json (see writePiDefaultModel in providers/defaultModel.ts), but this
|
|
620
|
+
// one is ours, not the user's — we move them onto the confidential model because
|
|
621
|
+
// they signed in. Writing it would pin a BYO-keyed user to `privateer/…` for
|
|
622
|
+
// good, and hand them a model with no credential the day they log out. The check
|
|
623
|
+
// above already stays off a saved pick; this leaves the unsaved case resolving
|
|
624
|
+
// fresh every launch, which is what it did before signing in.
|
|
618
625
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
619
626
|
try {
|
|
620
627
|
const ok = await pi.setModel(model);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// GUI control for Pi's TUI: see the screen, move the mouse, type.
|
|
2
|
+
//
|
|
3
|
+
// Registered ONLY when the machine has been armed — `privateer --allow-computer-control`,
|
|
4
|
+
// or the desktop's Screen control switch. Omitting the factory rather than hiding the
|
|
5
|
+
// tools is the same call privateer-media.ts makes for the same reason: a tool that
|
|
6
|
+
// exists and refuses every call teaches the model to keep retrying, where a tool that
|
|
7
|
+
// isn't there makes it say what the user would need to do and move on.
|
|
8
|
+
//
|
|
9
|
+
// A SUBAGENT CHILD NEVER GETS THESE, and unlike media there is no grant that lifts it.
|
|
10
|
+
// A child is a headless process with nobody to approve an action, and every computer
|
|
11
|
+
// action asks (permissions/mode.ts) — so the tools could only ever wedge on a prompt
|
|
12
|
+
// with no one to answer it. Media has childSpend.ts because a parent can meaningfully
|
|
13
|
+
// pre-authorize a bounded, billed call it named itself; there is no equivalent for
|
|
14
|
+
// "click wherever you decide to click", and inventing one would be inventing the
|
|
15
|
+
// unattended GUI agent this whole design is arranged to avoid.
|
|
16
|
+
import { makeComputerTools } from "../src/tools/computer.ts";
|
|
17
|
+
import { computerControlArmed } from "../src/config/computerControl.ts";
|
|
18
|
+
import { isSubagentChild } from "../src/remote/subagentRelay.ts";
|
|
19
|
+
|
|
20
|
+
export default function privateerComputer(pi: any): void {
|
|
21
|
+
if (!computerControlArmed()) return;
|
|
22
|
+
if (isSubagentChild()) return;
|
|
23
|
+
makeComputerTools()(pi);
|
|
24
|
+
}
|