privateer-agent 0.12.30 → 0.12.31
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/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
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
<#
|
|
2
|
+
PrivateerComputer.ps1 — the Windows screen-control helper.
|
|
3
|
+
|
|
4
|
+
One long-lived process, spoken to in newline-delimited JSON on stdin/stdout. The client
|
|
5
|
+
is src/computer/helper.ts; the coordinate contract is src/computer/space.ts. Read that
|
|
6
|
+
file before changing anything here — the two halves must agree exactly or every click
|
|
7
|
+
lands wrong in a way nothing reports.
|
|
8
|
+
|
|
9
|
+
── Why PowerShell and not a compiled .exe ───────────────────────────────────
|
|
10
|
+
|
|
11
|
+
Windows PowerShell 5.1 ships with every supported version of Windows, and `Add-Type`
|
|
12
|
+
compiles the P/Invoke surface below at startup. So this is a real Win32 helper with no
|
|
13
|
+
build step, no toolchain, no native npm module, and nothing to sign separately from the
|
|
14
|
+
app. The cost is ~300ms of one-time JIT on first launch, which the long-lived process
|
|
15
|
+
pays once rather than per action.
|
|
16
|
+
|
|
17
|
+
── DPI IS THE WHOLE BALLGAME ON WINDOWS ─────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
A process that has not declared DPI awareness is LIED TO by the operating system.
|
|
20
|
+
GetMonitorInfo returns virtualised 96-DPI coordinates, and CopyFromScreen returns a
|
|
21
|
+
stretched, blurry image of a smaller desktop. On a 150% display that is a 1.5x error in
|
|
22
|
+
every direction — and nothing reports it, because from inside the process the numbers
|
|
23
|
+
are self-consistent. So awareness is declared FIRST, before any screen call, and with a
|
|
24
|
+
three-step fallback because the good API is recent:
|
|
25
|
+
|
|
26
|
+
SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2) Win10 1703+ — per-monitor, correct
|
|
27
|
+
SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE) Win8.1+ — per-monitor
|
|
28
|
+
SetProcessDPIAware() Vista+ — system-wide only
|
|
29
|
+
|
|
30
|
+
Once per-monitor-aware, every rectangle Windows hands us is in PHYSICAL PIXELS, which is
|
|
31
|
+
exactly the space the agent's display-local device pixels live in. `scale` is then
|
|
32
|
+
reported for information only (the agent never converts through it on Windows).
|
|
33
|
+
|
|
34
|
+
── UIPI, and what "input: true" does not promise ────────────────────────────
|
|
35
|
+
|
|
36
|
+
Windows has no consent gate for screen capture or synthetic input, so both grants are
|
|
37
|
+
reported true. But User Interface Privilege Isolation silently DISCARDS input sent from a
|
|
38
|
+
normal-integrity process to an elevated window — so a UAC prompt, or an app launched as
|
|
39
|
+
administrator, cannot be driven from here and the events vanish with no error. There is
|
|
40
|
+
no reliable way to detect that before the fact (querying the foreground process's token
|
|
41
|
+
usually fails with access denied, which is itself ambiguous), so it is not reported as a
|
|
42
|
+
grant. It is documented in the tool description instead, which is the honest place for a
|
|
43
|
+
limitation we cannot measure.
|
|
44
|
+
#>
|
|
45
|
+
|
|
46
|
+
param(
|
|
47
|
+
[switch]$Serve,
|
|
48
|
+
[switch]$Grants,
|
|
49
|
+
[switch]$Prompt,
|
|
50
|
+
[switch]$Version
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
$ErrorActionPreference = 'Stop'
|
|
54
|
+
|
|
55
|
+
Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @'
|
|
56
|
+
using System;
|
|
57
|
+
using System.Drawing;
|
|
58
|
+
using System.Drawing.Drawing2D;
|
|
59
|
+
using System.Drawing.Imaging;
|
|
60
|
+
using System.IO;
|
|
61
|
+
using System.Collections.Generic;
|
|
62
|
+
using System.Runtime.InteropServices;
|
|
63
|
+
|
|
64
|
+
public class PComp {
|
|
65
|
+
// ── DPI ──────────────────────────────────────────────────────────────
|
|
66
|
+
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
67
|
+
[DllImport("user32.dll")] public static extern IntPtr SetProcessDpiAwarenessContext(IntPtr value);
|
|
68
|
+
[DllImport("shcore.dll")] public static extern int SetProcessDpiAwareness(int value);
|
|
69
|
+
[DllImport("shcore.dll")] public static extern int GetDpiForMonitor(IntPtr hmon, int dpiType, out uint dpiX, out uint dpiY);
|
|
70
|
+
|
|
71
|
+
public static void DeclareDpiAwareness() {
|
|
72
|
+
// -4 is DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2. Each call throws
|
|
73
|
+
// EntryPointNotFoundException on an OS too old to have it, so they are tried in
|
|
74
|
+
// order of preference — see the header on why being wrong here is invisible.
|
|
75
|
+
try { SetProcessDpiAwarenessContext(new IntPtr(-4)); return; } catch {}
|
|
76
|
+
try { SetProcessDpiAwareness(2); return; } catch {}
|
|
77
|
+
try { SetProcessDPIAware(); } catch {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Monitors ─────────────────────────────────────────────────────────
|
|
81
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
82
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
83
|
+
|
|
84
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
85
|
+
public struct MONITORINFOEX {
|
|
86
|
+
public int cbSize;
|
|
87
|
+
public RECT rcMonitor;
|
|
88
|
+
public RECT rcWork;
|
|
89
|
+
public uint dwFlags;
|
|
90
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string szDevice;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdc, ref RECT rect, IntPtr data);
|
|
94
|
+
[DllImport("user32.dll")] public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr clip, MonitorEnumProc cb, IntPtr data);
|
|
95
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX info);
|
|
96
|
+
[DllImport("user32.dll")] public static extern int GetSystemMetrics(int index);
|
|
97
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
98
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
|
99
|
+
|
|
100
|
+
public class Mon {
|
|
101
|
+
public string Id; public string Label; public int X, Y, W, H; public double Scale; public bool Primary;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public static List<Mon> Monitors() {
|
|
105
|
+
var found = new List<Mon>();
|
|
106
|
+
MonitorEnumProc cb = delegate(IntPtr h, IntPtr hdc, ref RECT r, IntPtr d) {
|
|
107
|
+
var info = new MONITORINFOEX();
|
|
108
|
+
info.cbSize = Marshal.SizeOf(typeof(MONITORINFOEX));
|
|
109
|
+
if (!GetMonitorInfo(h, ref info)) return true;
|
|
110
|
+
double scale = 1.0;
|
|
111
|
+
try {
|
|
112
|
+
uint dx, dy;
|
|
113
|
+
// 0 = MDT_EFFECTIVE_DPI. 96 is the unscaled baseline.
|
|
114
|
+
if (GetDpiForMonitor(h, 0, out dx, out dy) == 0 && dx > 0) scale = dx / 96.0;
|
|
115
|
+
} catch {}
|
|
116
|
+
found.Add(new Mon {
|
|
117
|
+
Id = info.szDevice,
|
|
118
|
+
Label = info.szDevice,
|
|
119
|
+
X = info.rcMonitor.Left,
|
|
120
|
+
Y = info.rcMonitor.Top,
|
|
121
|
+
W = info.rcMonitor.Right - info.rcMonitor.Left,
|
|
122
|
+
H = info.rcMonitor.Bottom - info.rcMonitor.Top,
|
|
123
|
+
Scale = scale,
|
|
124
|
+
// MONITORINFOF_PRIMARY
|
|
125
|
+
Primary = (info.dwFlags & 1) != 0
|
|
126
|
+
});
|
|
127
|
+
return true;
|
|
128
|
+
};
|
|
129
|
+
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, cb, IntPtr.Zero);
|
|
130
|
+
return found;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public static int ForegroundPid() {
|
|
134
|
+
try { uint pid; GetWindowThreadProcessId(GetForegroundWindow(), out pid); return (int)pid; }
|
|
135
|
+
catch { return 0; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Capture ──────────────────────────────────────────────────────────
|
|
139
|
+
//
|
|
140
|
+
// Two steps in one call: grab the monitor's physical pixels, then resize to EXACTLY
|
|
141
|
+
// the requested size. Exactly, because the agent's coordinate space is defined by
|
|
142
|
+
// that size — a resize that rounded, or that returned the native frame, would put
|
|
143
|
+
// agent space and the picture into disagreement and every click would be off by the
|
|
144
|
+
// reduction factor.
|
|
145
|
+
//
|
|
146
|
+
// HighQualityBicubic for the same reason macOS uses high interpolation: a screenshot
|
|
147
|
+
// is nearly always being REDUCED, and point-sampling text does not look soft, it
|
|
148
|
+
// aliases — strokes break up and the model misreads them.
|
|
149
|
+
static byte[] Encode(Bitmap source, int w, int h) {
|
|
150
|
+
using (var scaled = new Bitmap(w, h, PixelFormat.Format32bppArgb)) {
|
|
151
|
+
using (var g = Graphics.FromImage(scaled)) {
|
|
152
|
+
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
|
153
|
+
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
|
154
|
+
g.DrawImage(source, new Rectangle(0, 0, w, h));
|
|
155
|
+
}
|
|
156
|
+
using (var ms = new MemoryStream()) {
|
|
157
|
+
// PNG, not JPEG: UI is flat colour and hard edges, and JPEG ringing on
|
|
158
|
+
// small text is exactly the detail being read.
|
|
159
|
+
scaled.Save(ms, ImageFormat.Png);
|
|
160
|
+
return ms.ToArray();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Returns { model-sized frame, dialog-sized preview }; the preview is null when it
|
|
166
|
+
/// was not asked for. ONE CopyFromScreen feeding both, deliberately — grabbing twice
|
|
167
|
+
/// would be two moments in time, so a person could approve a click against a screen
|
|
168
|
+
/// the model never saw.
|
|
169
|
+
public static byte[][] Capture(int x, int y, int w, int h, int tw, int th, int pw, int ph) {
|
|
170
|
+
using (var shot = new Bitmap(w, h, PixelFormat.Format32bppArgb)) {
|
|
171
|
+
using (var g = Graphics.FromImage(shot)) {
|
|
172
|
+
g.CopyFromScreen(x, y, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy);
|
|
173
|
+
}
|
|
174
|
+
var main = Encode(shot, tw, th);
|
|
175
|
+
byte[] preview = null;
|
|
176
|
+
if (pw > 0 && ph > 0) {
|
|
177
|
+
// Best-effort: the model's frame is already in hand, and a dialog that
|
|
178
|
+
// falls back to text is far better than a failed capture.
|
|
179
|
+
try { preview = Encode(shot, pw, ph); } catch {}
|
|
180
|
+
}
|
|
181
|
+
return new byte[][] { main, preview };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Input ────────────────────────────────────────────────────────────
|
|
186
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
187
|
+
public struct MOUSEINPUT { public int dx, dy; public uint mouseData, dwFlags, time; public IntPtr dwExtraInfo; }
|
|
188
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
189
|
+
public struct KEYBDINPUT { public ushort wVk, wScan; public uint dwFlags, time; public IntPtr dwExtraInfo; }
|
|
190
|
+
[StructLayout(LayoutKind.Explicit)]
|
|
191
|
+
public struct INPUTUNION { [FieldOffset(0)] public MOUSEINPUT mi; [FieldOffset(0)] public KEYBDINPUT ki; }
|
|
192
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
193
|
+
public struct INPUT { public uint type; public INPUTUNION u; }
|
|
194
|
+
|
|
195
|
+
[DllImport("user32.dll", SetLastError = true)] public static extern uint SendInput(uint n, INPUT[] inputs, int cbSize);
|
|
196
|
+
|
|
197
|
+
const uint MOUSEEVENTF_MOVE = 0x0001, MOUSEEVENTF_ABSOLUTE = 0x8000, MOUSEEVENTF_VIRTUALDESK = 0x4000;
|
|
198
|
+
const uint MOUSEEVENTF_WHEEL = 0x0800, MOUSEEVENTF_HWHEEL = 0x1000;
|
|
199
|
+
const uint KEYEVENTF_KEYUP = 0x0002, KEYEVENTF_UNICODE = 0x0004;
|
|
200
|
+
|
|
201
|
+
static void Send(INPUT[] inputs) {
|
|
202
|
+
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(INPUT)));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Absolute mouse coordinates are NORMALISED to 0..65535 across the whole virtual
|
|
206
|
+
/// desktop, not given in pixels — the single most common way Windows mouse
|
|
207
|
+
/// automation ends up clicking the wrong place. VIRTUALDESK makes the normalisation
|
|
208
|
+
/// span every monitor rather than only the primary, which is what makes a second
|
|
209
|
+
/// display reachable at all.
|
|
210
|
+
static INPUT MouseAt(int gx, int gy, uint extraFlags, uint mouseData) {
|
|
211
|
+
int vx = GetSystemMetrics(76), vy = GetSystemMetrics(77);
|
|
212
|
+
int vw = GetSystemMetrics(78), vh = GetSystemMetrics(79);
|
|
213
|
+
if (vw < 2) vw = 2;
|
|
214
|
+
if (vh < 2) vh = 2;
|
|
215
|
+
var i = new INPUT();
|
|
216
|
+
i.type = 0; // INPUT_MOUSE
|
|
217
|
+
i.u.mi.dx = (int)Math.Round((gx - vx) * 65535.0 / (vw - 1));
|
|
218
|
+
i.u.mi.dy = (int)Math.Round((gy - vy) * 65535.0 / (vh - 1));
|
|
219
|
+
i.u.mi.mouseData = mouseData;
|
|
220
|
+
i.u.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK | extraFlags;
|
|
221
|
+
return i;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static void ButtonCodes(string button, out uint down, out uint up) {
|
|
225
|
+
if (button == "right") { down = 0x0008; up = 0x0010; }
|
|
226
|
+
else if (button == "middle") { down = 0x0020; up = 0x0040; }
|
|
227
|
+
else { down = 0x0002; up = 0x0004; }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public static void Move(int gx, int gy) { Send(new INPUT[] { MouseAt(gx, gy, 0, 0) }); }
|
|
231
|
+
|
|
232
|
+
public static void Click(int gx, int gy, string button, int clicks) {
|
|
233
|
+
uint down, up;
|
|
234
|
+
ButtonCodes(button, out down, out up);
|
|
235
|
+
// The pointer moves first and as its own event: applications track hover from
|
|
236
|
+
// the move, and a click arriving where the pointer has never been is missed by
|
|
237
|
+
// menus and anything with a hover-to-open affordance.
|
|
238
|
+
Send(new INPUT[] { MouseAt(gx, gy, 0, 0) });
|
|
239
|
+
for (int n = 0; n < Math.Max(1, clicks); n++) {
|
|
240
|
+
Send(new INPUT[] { MouseAt(gx, gy, down, 0), MouseAt(gx, gy, up, 0) });
|
|
241
|
+
// Windows decides double-click by TIMING, not by a click count, so the pairs
|
|
242
|
+
// must arrive inside the system double-click interval — hence no sleep here.
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public static void Drag(int gx, int gy, int tx, int ty, string button) {
|
|
247
|
+
uint down, up;
|
|
248
|
+
ButtonCodes(button, out down, out up);
|
|
249
|
+
Send(new INPUT[] { MouseAt(gx, gy, 0, 0), MouseAt(gx, gy, down, 0) });
|
|
250
|
+
// Interpolated, for the same reason as macOS: a drag delivered as down-then-up
|
|
251
|
+
// at the destination is ignored by anything that starts its gesture on the first
|
|
252
|
+
// move — most drag-and-drop, sliders, and every text selection.
|
|
253
|
+
const int steps = 12;
|
|
254
|
+
for (int i = 1; i <= steps; i++) {
|
|
255
|
+
double t = (double)i / steps;
|
|
256
|
+
Send(new INPUT[] { MouseAt((int)Math.Round(gx + (tx - gx) * t), (int)Math.Round(gy + (ty - gy) * t), 0, 0) });
|
|
257
|
+
System.Threading.Thread.Sleep(8);
|
|
258
|
+
}
|
|
259
|
+
Send(new INPUT[] { MouseAt(tx, ty, up, 0) });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public static void Scroll(int gx, int gy, int dx, int dy) {
|
|
263
|
+
Send(new INPUT[] { MouseAt(gx, gy, 0, 0) });
|
|
264
|
+
// WHEEL_DELTA is 120 per notch. Windows scrolls DOWN on a negative delta and the
|
|
265
|
+
// tool's contract is that negative scrolls UP, so the sign flips exactly once.
|
|
266
|
+
if (dy != 0) Send(new INPUT[] { MouseAt(gx, gy, MOUSEEVENTF_WHEEL, unchecked((uint)(-dy * 120))) });
|
|
267
|
+
if (dx != 0) Send(new INPUT[] { MouseAt(gx, gy, MOUSEEVENTF_HWHEEL, unchecked((uint)(dx * 120))) });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
static INPUT Key(ushort vk, ushort scan, uint flags) {
|
|
271
|
+
var i = new INPUT();
|
|
272
|
+
i.type = 1; // INPUT_KEYBOARD
|
|
273
|
+
i.u.ki.wVk = vk;
|
|
274
|
+
i.u.ki.wScan = scan;
|
|
275
|
+
i.u.ki.dwFlags = flags;
|
|
276
|
+
return i;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Type literal text as UNICODE scan codes (wVk = 0), the exact counterpart of
|
|
280
|
+
/// macOS's keyboardSetUnicodeString: it delivers the characters themselves, so it is
|
|
281
|
+
/// correct on every keyboard layout and handles accents, CJK and emoji, none of which
|
|
282
|
+
/// have a virtual-key code to look up. Surrogate pairs work because the string is
|
|
283
|
+
/// walked as UTF-16 code units and each is sent in turn.
|
|
284
|
+
public static void TypeText(string text) {
|
|
285
|
+
foreach (char c in text) {
|
|
286
|
+
Send(new INPUT[] {
|
|
287
|
+
Key(0, c, KEYEVENTF_UNICODE),
|
|
288
|
+
Key(0, c, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP)
|
|
289
|
+
});
|
|
290
|
+
// Applications with their own input handling (Electron, Java, terminals)
|
|
291
|
+
// drop events delivered faster than a person could type.
|
|
292
|
+
System.Threading.Thread.Sleep(6);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
public static void PressKeys(ushort[] modifiers, ushort vk) {
|
|
297
|
+
var down = new List<INPUT>();
|
|
298
|
+
foreach (var m in modifiers) down.Add(Key(m, 0, 0));
|
|
299
|
+
down.Add(Key(vk, 0, 0));
|
|
300
|
+
Send(down.ToArray());
|
|
301
|
+
var up = new List<INPUT>();
|
|
302
|
+
up.Add(Key(vk, 0, KEYEVENTF_KEYUP));
|
|
303
|
+
// Modifiers release in REVERSE order, the way a hand leaves a chord. Releasing
|
|
304
|
+
// ctrl before the key it modified leaves applications seeing a bare keypress.
|
|
305
|
+
for (int i = modifiers.Length - 1; i >= 0; i--) up.Add(Key(modifiers[i], 0, KEYEVENTF_KEYUP));
|
|
306
|
+
Send(up.ToArray());
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
'@
|
|
310
|
+
|
|
311
|
+
# BEFORE any screen call. See the header — getting this wrong is invisible from inside.
|
|
312
|
+
[PComp]::DeclareDpiAwareness()
|
|
313
|
+
|
|
314
|
+
# ── Named keys ───────────────────────────────────────────────────────────────
|
|
315
|
+
# Only keys whose position is fixed. Letters and digits are here for CHORDS only
|
|
316
|
+
# (ctrl+s), where every application's shortcut table is written against the US
|
|
317
|
+
# position; literal characters go through TypeText, which is layout-independent.
|
|
318
|
+
$VK = @{
|
|
319
|
+
'return' = 0x0D; 'enter' = 0x0D; 'tab' = 0x09; 'space' = 0x20
|
|
320
|
+
'backspace' = 0x08; 'delete' = 0x2E; 'forwarddelete' = 0x2E
|
|
321
|
+
'escape' = 0x1B; 'esc' = 0x1B
|
|
322
|
+
'left' = 0x25; 'up' = 0x26; 'right' = 0x27; 'down' = 0x28
|
|
323
|
+
'home' = 0x24; 'end' = 0x23; 'pageup' = 0x21; 'pagedown' = 0x22
|
|
324
|
+
'f1' = 0x70; 'f2' = 0x71; 'f3' = 0x72; 'f4' = 0x73; 'f5' = 0x74; 'f6' = 0x75
|
|
325
|
+
'f7' = 0x76; 'f8' = 0x77; 'f9' = 0x78; 'f10' = 0x79; 'f11' = 0x7A; 'f12' = 0x7B
|
|
326
|
+
}
|
|
327
|
+
$MODS = @{
|
|
328
|
+
'ctrl' = 0x11; 'control' = 0x11
|
|
329
|
+
'alt' = 0x12; 'option' = 0x12; 'opt' = 0x12
|
|
330
|
+
'shift' = 0x10
|
|
331
|
+
# cmd/meta/super map to the Windows key. Accepted so a model that learned "cmd+s"
|
|
332
|
+
# on macOS does not silently send nothing here — though on Windows the useful
|
|
333
|
+
# shortcut is almost always ctrl, which the tool description says.
|
|
334
|
+
'cmd' = 0x5B; 'command' = 0x5B; 'meta' = 0x5B; 'super' = 0x5B; 'win' = 0x5B
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function Get-Monitors {
|
|
338
|
+
$out = @()
|
|
339
|
+
foreach ($m in [PComp]::Monitors()) {
|
|
340
|
+
$out += [ordered]@{
|
|
341
|
+
id = $m.Id; label = $m.Label
|
|
342
|
+
width = $m.W; height = $m.H
|
|
343
|
+
scale = $m.Scale
|
|
344
|
+
# Reported in the SAME physical pixels as width/height, because a
|
|
345
|
+
# per-monitor-aware process is given physical pixels throughout. Still
|
|
346
|
+
# informational only — the agent never converts through it.
|
|
347
|
+
originX = $m.X; originY = $m.Y
|
|
348
|
+
primary = $m.Primary
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return $out
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function Get-Grants {
|
|
355
|
+
# Windows gates neither screen capture nor synthetic input, so both are true. UIPI
|
|
356
|
+
# can still swallow input aimed at an elevated window and cannot be detected
|
|
357
|
+
# reliably — see the header; it is documented, not reported as a grant.
|
|
358
|
+
return [ordered]@{
|
|
359
|
+
screen = $true
|
|
360
|
+
input = $true
|
|
361
|
+
secureInput = $false
|
|
362
|
+
frontmostPid = [PComp]::ForegroundPid()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function Write-Frame($obj) {
|
|
367
|
+
# -Compress keeps it to ONE line (the client reads newline-delimited frames) and
|
|
368
|
+
# -Depth 10 is required because ConvertTo-Json truncates at depth 2 by default,
|
|
369
|
+
# which would silently empty the displays array.
|
|
370
|
+
[Console]::Out.WriteLine(($obj | ConvertTo-Json -Depth 10 -Compress))
|
|
371
|
+
[Console]::Out.Flush()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function Send-Ok($id, $fields) {
|
|
375
|
+
$out = [ordered]@{ id = $id; ok = $true }
|
|
376
|
+
if ($fields) { foreach ($k in $fields.Keys) { $out[$k] = $fields[$k] } }
|
|
377
|
+
Write-Frame $out
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function Send-Fail($id, $message) {
|
|
381
|
+
Write-Frame ([ordered]@{ id = $id; ok = $false; error = $message })
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if ($Version) { Write-Output 'privateer-computer 1'; exit 0 }
|
|
385
|
+
|
|
386
|
+
if ($Grants) {
|
|
387
|
+
Write-Frame ([ordered]@{ ok = $true; grants = (Get-Grants); displays = (Get-Monitors) })
|
|
388
|
+
exit 0
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (-not $Serve) {
|
|
392
|
+
[Console]::Error.WriteLine('usage: PrivateerComputer.ps1 -Serve | -Grants | -Version')
|
|
393
|
+
exit 2
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function Find-Monitor($id) {
|
|
397
|
+
$all = [PComp]::Monitors()
|
|
398
|
+
if ($null -eq $id -or $id -eq '') {
|
|
399
|
+
$p = $all | Where-Object { $_.Primary } | Select-Object -First 1
|
|
400
|
+
if ($p) { return $p }
|
|
401
|
+
return ($all | Select-Object -First 1)
|
|
402
|
+
}
|
|
403
|
+
return ($all | Where-Object { $_.Id -eq $id } | Select-Object -First 1)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
while ($null -ne ($line = [Console]::In.ReadLine())) {
|
|
407
|
+
$line = $line.Trim()
|
|
408
|
+
if ($line -eq '') { continue }
|
|
409
|
+
|
|
410
|
+
$req = $null
|
|
411
|
+
try { $req = $line | ConvertFrom-Json } catch { Send-Fail $null 'malformed request'; continue }
|
|
412
|
+
$id = $req.id
|
|
413
|
+
$op = [string]$req.op
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
switch ($op) {
|
|
417
|
+
'displays' { Send-Ok $id @{ displays = (Get-Monitors) } }
|
|
418
|
+
|
|
419
|
+
'grants' { Send-Ok $id @{ grants = (Get-Grants) } }
|
|
420
|
+
|
|
421
|
+
'capture' {
|
|
422
|
+
$m = Find-Monitor $req.display
|
|
423
|
+
if (-not $m) { Send-Fail $id 'no such display'; continue }
|
|
424
|
+
$tw = if ($req.targetWidth) { [int]$req.targetWidth } else { $m.W }
|
|
425
|
+
$th = if ($req.targetHeight) { [int]$req.targetHeight } else { $m.H }
|
|
426
|
+
$pw = if ($req.previewWidth) { [int]$req.previewWidth } else { 0 }
|
|
427
|
+
$ph = if ($req.previewHeight) { [int]$req.previewHeight } else { 0 }
|
|
428
|
+
$shots = [PComp]::Capture($m.X, $m.Y, $m.W, $m.H, $tw, $th, $pw, $ph)
|
|
429
|
+
$fields = [ordered]@{
|
|
430
|
+
mimeType = 'image/png'
|
|
431
|
+
data = [Convert]::ToBase64String($shots[0])
|
|
432
|
+
width = $tw
|
|
433
|
+
height = $th
|
|
434
|
+
}
|
|
435
|
+
if ($shots[1]) { $fields['preview'] = [Convert]::ToBase64String($shots[1]) }
|
|
436
|
+
Send-Ok $id $fields
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
'pointer' {
|
|
440
|
+
$m = Find-Monitor $req.display
|
|
441
|
+
if (-not $m) { Send-Fail $id 'no such display'; continue }
|
|
442
|
+
# Display-local physical pixels → global physical pixels. The ONLY
|
|
443
|
+
# conversion, mirroring the macOS helper's globalPoint.
|
|
444
|
+
$gx = $m.X + [int]$req.x
|
|
445
|
+
$gy = $m.Y + [int]$req.y
|
|
446
|
+
$button = if ($req.button) { [string]$req.button } else { 'left' }
|
|
447
|
+
switch ([string]$req.action) {
|
|
448
|
+
'move' { [PComp]::Move($gx, $gy) }
|
|
449
|
+
'click' { [PComp]::Click($gx, $gy, $button, 1) }
|
|
450
|
+
'double_click' { [PComp]::Click($gx, $gy, $button, 2) }
|
|
451
|
+
'drag' {
|
|
452
|
+
$tx = $m.X + [int]$req.toX
|
|
453
|
+
$ty = $m.Y + [int]$req.toY
|
|
454
|
+
[PComp]::Drag($gx, $gy, $tx, $ty, $button)
|
|
455
|
+
}
|
|
456
|
+
'scroll' {
|
|
457
|
+
$dx = if ($req.scrollX) { [int]$req.scrollX } else { 0 }
|
|
458
|
+
$dy = if ($req.scrollY) { [int]$req.scrollY } else { 0 }
|
|
459
|
+
[PComp]::Scroll($gx, $gy, $dx, $dy)
|
|
460
|
+
}
|
|
461
|
+
default { Send-Fail $id ("unknown pointer action `"" + $req.action + "`""); continue }
|
|
462
|
+
}
|
|
463
|
+
Send-Ok $id $null
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
'key' {
|
|
467
|
+
switch ([string]$req.action) {
|
|
468
|
+
'type' { [PComp]::TypeText([string]$req.text); Send-Ok $id $null }
|
|
469
|
+
'press' {
|
|
470
|
+
$spec = ([string]$req.keys).ToLower().Trim()
|
|
471
|
+
if ($spec -eq '') { Send-Fail $id 'empty key combination'; continue }
|
|
472
|
+
$parts = $spec.Split('+')
|
|
473
|
+
$last = $parts[-1]
|
|
474
|
+
$mods = @()
|
|
475
|
+
$bad = $null
|
|
476
|
+
for ($i = 0; $i -lt $parts.Length - 1; $i++) {
|
|
477
|
+
$p = $parts[$i]
|
|
478
|
+
if ($MODS.ContainsKey($p)) { $mods += [uint16]$MODS[$p] }
|
|
479
|
+
else { $bad = $p; break }
|
|
480
|
+
}
|
|
481
|
+
if ($bad) { Send-Fail $id ("unknown modifier `"" + $bad + "`" in `"" + $spec + "`""); continue }
|
|
482
|
+
$vk = $null
|
|
483
|
+
if ($VK.ContainsKey($last)) { $vk = $VK[$last] }
|
|
484
|
+
elseif ($last.Length -eq 1 -and $last -match '[a-z0-9]') {
|
|
485
|
+
$vk = [int][char]([string]$last).ToUpper()
|
|
486
|
+
}
|
|
487
|
+
if ($null -eq $vk) { Send-Fail $id ("unknown key `"" + $last + "`" in `"" + $spec + "`""); continue }
|
|
488
|
+
[PComp]::PressKeys([uint16[]]$mods, [uint16]$vk)
|
|
489
|
+
Send-Ok $id $null
|
|
490
|
+
}
|
|
491
|
+
default { Send-Fail $id 'unknown key action' }
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
default { Send-Fail $id ("unknown op `"" + $op + "`"") }
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
# A helper that dies on one bad frame takes the whole session's screen control
|
|
499
|
+
# with it. Report and keep serving.
|
|
500
|
+
Send-Fail $id $_.Exception.Message
|
|
501
|
+
}
|
|
502
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.31",
|
|
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",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"files": [
|
|
50
50
|
"bin",
|
|
51
51
|
"src",
|
|
52
|
+
"native",
|
|
52
53
|
"extensions",
|
|
53
54
|
"skills",
|
|
54
55
|
"patches",
|
package/src/acp/run.ts
CHANGED
|
@@ -92,7 +92,7 @@ export async function runAcp(): Promise<void> {
|
|
|
92
92
|
const { modelRegistryOf, piAuthStore } = await import("../providers/piAuthStore.ts");
|
|
93
93
|
const { hasCredentials, acquireAccountCredential, revokeAccountSession } = await import("../auth/privateer.ts");
|
|
94
94
|
const { webEnabled, mediaEnabled } = await import("../config/hosted.ts");
|
|
95
|
-
const { resolveDefaultModel } = await import("../providers/defaultModel.ts");
|
|
95
|
+
const { resolveDefaultModel, writePiDefaultModel } = await import("../providers/defaultModel.ts");
|
|
96
96
|
const { agentDir, configPath } = await import("../config/paths.ts");
|
|
97
97
|
const { PrivateerAcpAgent, askOverAcp } = await import("./server.ts");
|
|
98
98
|
type AcpSession = import("./server.ts").AcpSession;
|
|
@@ -333,6 +333,11 @@ export async function runAcp(): Promise<void> {
|
|
|
333
333
|
await ensureAccountArmed(next.provider);
|
|
334
334
|
await session.setModel(next);
|
|
335
335
|
currentProvider = next.provider;
|
|
336
|
+
// The host's model dropdown (session/set_model) is the user choosing, so it
|
|
337
|
+
// becomes the default the next launch resolves — the desktop app and a fresh
|
|
338
|
+
// terminal are meant to agree on which model you're on. Pi won't write it for
|
|
339
|
+
// us: nothing here passes `persist`. See writePiDefaultModel.
|
|
340
|
+
writePiDefaultModel(`${next.provider}/${next.id}`);
|
|
336
341
|
},
|
|
337
342
|
async prompt(text, events, signal) {
|
|
338
343
|
holder.events = events;
|
package/src/cli/chat.ts
CHANGED
|
@@ -53,7 +53,8 @@ async function main() {
|
|
|
53
53
|
const { modelRegistryOf } = await import("../providers/piAuthStore.ts");
|
|
54
54
|
const { agentVersion } = await import("../config/version.ts");
|
|
55
55
|
const { pickerCatalog, hiddenAccountNotice, hiddenAccountTitleSuffix } = await import("../providers/modelCatalog.ts");
|
|
56
|
-
const { resolveDefaultModel, resolveSignedInModel, savedPiDefaultSpec } =
|
|
56
|
+
const { resolveDefaultModel, resolveSignedInModel, savedPiDefaultSpec, writePiDefaultModel } =
|
|
57
|
+
await import("../providers/defaultModel.ts");
|
|
57
58
|
const { postOutbox } = await import("../outbox/cloudOutbox.ts");
|
|
58
59
|
const { addPendingCloud } = await import("../routines/store.ts");
|
|
59
60
|
|
|
@@ -715,6 +716,11 @@ async function main() {
|
|
|
715
716
|
try {
|
|
716
717
|
await session.setModel(model);
|
|
717
718
|
currentSpec = sp;
|
|
719
|
+
// Typed /model is a deliberate pick, and Pi persists a switch only for a caller
|
|
720
|
+
// that asks (AgentSession.setModel's `options.persist`, which we don't pass and
|
|
721
|
+
// its own /model doesn't either). Without this the pick died with the process and
|
|
722
|
+
// the next terminal came back on the old model. See writePiDefaultModel.
|
|
723
|
+
writePiDefaultModel(sp);
|
|
718
724
|
const m = `model → ${sp}`;
|
|
719
725
|
console.log(`${DIM}${m}${RESET}`);
|
|
720
726
|
relay?.sendContext({ model: currentSpec, cwd, version: agentVersion() }); // banner follows the switch
|