privateer-agent 0.12.36 β 0.12.38
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.
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { verifyModelPosture, TIERS, type PrivacyTier } from "pi-privacy";
|
|
10
10
|
import { accountPosture } from "../src/providers/account.ts";
|
|
11
11
|
import { type Palette, paletteFor } from "../src/ui/palette.ts";
|
|
12
|
-
import { privacyDisabled } from "../src/config/privacyDisabled.ts";
|
|
12
|
+
import { onPrivacyDisabledChange, privacyDisabled } from "../src/config/privacyDisabled.ts";
|
|
13
13
|
|
|
14
14
|
const DOT: Record<string, string> = { green: "π’", yellow: "π‘", red: "π΄", neutral: "βͺ" };
|
|
15
15
|
|
|
@@ -79,6 +79,21 @@ export default function privateerPosture(pi: any): void {
|
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// Keep the last ctx a real event handed us: the badge has to be repainted when the
|
|
83
|
+
// privacy flag moves, and the flag can now move with no event behind it at all β
|
|
84
|
+
// the app's shield toggle writes it over the relay (RemoteBridge's onPrivacy), so
|
|
85
|
+
// `/privacy` is no longer the only driver and the CLI's own badge would otherwise
|
|
86
|
+
// sit on "β Trusted Execution" over a session whose filter a phone just took down.
|
|
87
|
+
let lastCtx: any = null;
|
|
88
|
+
pi.on("model_select", (event: any, ctx: any) => {
|
|
89
|
+
lastCtx = ctx;
|
|
90
|
+
return update(event?.model?.provider, event?.model?.id, ctx);
|
|
91
|
+
});
|
|
92
|
+
pi.on("session_start", (_event: any, ctx: any) => {
|
|
93
|
+
lastCtx = ctx;
|
|
94
|
+
return update(ctx?.model?.provider, ctx?.model?.id, ctx);
|
|
95
|
+
});
|
|
96
|
+
onPrivacyDisabledChange(() => {
|
|
97
|
+
void update(lastCtx?.model?.provider, lastCtx?.model?.id, lastCtx);
|
|
98
|
+
});
|
|
84
99
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.38",
|
|
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",
|
|
@@ -24,11 +24,27 @@ export function privacyDisabled(): boolean {
|
|
|
24
24
|
return process.env[ENV] === "1" || process.env[ALT_ENV] === "1";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// WHO ELSE NEEDS TO KNOW. The env var is the store, but it is not observable β and the
|
|
28
|
+
// switch now has two drivers on the same session: `/privacy off` typed into a composer,
|
|
29
|
+
// and the app's own shield toggle (RemoteBridge's onPrivacy). Whichever one moves it, the
|
|
30
|
+
// app's switch has to follow, or the desktop shows a shield over a session whose filter
|
|
31
|
+
// its own composer just turned off. Listeners fire on a real CHANGE only, so a re-assert
|
|
32
|
+
// is not an event; failures are swallowed, because a subscriber that throws must not
|
|
33
|
+
// leave the flag half-applied.
|
|
34
|
+
const listeners = new Set<(disabled: boolean) => void>();
|
|
35
|
+
|
|
36
|
+
/** Watch the disabled state. Returns an unsubscribe. Fires only when the value changes. */
|
|
37
|
+
export function onPrivacyDisabledChange(fn: (disabled: boolean) => void): () => void {
|
|
38
|
+
listeners.add(fn);
|
|
39
|
+
return () => listeners.delete(fn);
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
/**
|
|
28
43
|
* Set the disabled state. Modifies process.env so every module copy and child process agrees.
|
|
29
44
|
* Returns the new state.
|
|
30
45
|
*/
|
|
31
46
|
export function setPrivacyDisabled(disabled: boolean): boolean {
|
|
47
|
+
const before = privacyDisabled();
|
|
32
48
|
if (disabled) {
|
|
33
49
|
process.env[ENV] = "1";
|
|
34
50
|
process.env[ALT_ENV] = "1";
|
|
@@ -36,6 +52,11 @@ export function setPrivacyDisabled(disabled: boolean): boolean {
|
|
|
36
52
|
delete process.env[ENV];
|
|
37
53
|
delete process.env[ALT_ENV];
|
|
38
54
|
}
|
|
55
|
+
if (before !== disabled) {
|
|
56
|
+
for (const fn of listeners) {
|
|
57
|
+
try { fn(disabled); } catch { /* a watcher must not break the toggle */ }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
39
60
|
return disabled;
|
|
40
61
|
}
|
|
41
62
|
|
|
@@ -126,6 +126,13 @@ export interface RelayCallbacks {
|
|
|
126
126
|
// bypass mode instead of relaying every action, so the agent runs to completion.
|
|
127
127
|
// Dangerous/destructive actions still relay β they sit above bypass locally too.
|
|
128
128
|
onNoQuarter?: (on: boolean) => void;
|
|
129
|
+
// The app toggled the privacy filter. `off` true = pi-privacy is disabled for this
|
|
130
|
+
// session, exactly as `/privacy off` does it: no outbound PII scan or prompt, no
|
|
131
|
+
// tool-result credential redaction, no exfiltration or downgrade guard. The app is
|
|
132
|
+
// the only surface that can ask for this without a keyboard on this machine, so the
|
|
133
|
+
// frame carries the SAME state the command writes (process.env, shared with
|
|
134
|
+
// subagent children) rather than a second flag beside it.
|
|
135
|
+
onPrivacy?: (off: boolean) => void;
|
|
129
136
|
// A controller attached β push a transcript snapshot so it can catch up.
|
|
130
137
|
onControllerAttached: () => void;
|
|
131
138
|
// The last controller went away (app closed / socket reaped). The terminal keeps
|
|
@@ -696,6 +703,7 @@ export class RelayClient {
|
|
|
696
703
|
seq?: number;
|
|
697
704
|
data?: string;
|
|
698
705
|
on?: boolean;
|
|
706
|
+
off?: boolean;
|
|
699
707
|
value?: string;
|
|
700
708
|
source?: string;
|
|
701
709
|
description?: string;
|
|
@@ -763,6 +771,9 @@ export class RelayClient {
|
|
|
763
771
|
case "no_quarter":
|
|
764
772
|
this.cb.onNoQuarter?.(frame.on === true);
|
|
765
773
|
break;
|
|
774
|
+
case "privacy":
|
|
775
|
+
this.cb.onPrivacy?.(frame.off === true);
|
|
776
|
+
break;
|
|
766
777
|
case "controller_attached":
|
|
767
778
|
this.cb.onControllerAttached();
|
|
768
779
|
break;
|
|
@@ -1158,6 +1169,15 @@ export class RelayClient {
|
|
|
1158
1169
|
this.rawSend({ type: "no_quarter", on });
|
|
1159
1170
|
}
|
|
1160
1171
|
|
|
1172
|
+
// Echo the privacy-filter state up to the controller, same contract as
|
|
1173
|
+
// sendNoQuarter: an ack of the app's toggle, and a resync on controller attach.
|
|
1174
|
+
// Only ever sent while the filter is OFF β the app defaults its switch to "on"
|
|
1175
|
+
// (the safe reading), so a terminal too old to know this frame, or one running
|
|
1176
|
+
// with the filter intact, correctly shows protected.
|
|
1177
|
+
sendPrivacy(off: boolean): void {
|
|
1178
|
+
this.rawSend({ type: "privacy", off });
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1161
1181
|
// Push this terminal's live context (selected model, agent version) to a
|
|
1162
1182
|
// controller so the app's session banner reflects reality instead of a stub.
|
|
1163
1183
|
// Sent on controller attach β like the snapshot/no_quarter resync. NON-PII ONLY
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import { noQuarterActive, setNoQuarter } from "../permissions/noQuarter.ts";
|
|
15
|
+
// Pure env-var accessors (see config/privacyDisabled.ts) β no Pi, no node builtins,
|
|
16
|
+
// so this stays safe to import from the bridge, which boot-ordered code reaches.
|
|
17
|
+
import { onPrivacyDisabledChange, privacyDisabled, setPrivacyDisabled } from "../config/privacyDisabled.ts";
|
|
15
18
|
import type { EngineEvent } from "../engine/events.ts";
|
|
16
19
|
import type { PermissionRequest } from "../permissions/gate.ts";
|
|
17
20
|
import type { AskOutcome } from "../permissions/modeGate.ts";
|
|
@@ -56,6 +59,10 @@ export interface RelayLike {
|
|
|
56
59
|
// turns into a duplicate of something the app already displayed.
|
|
57
60
|
hasController?(): boolean;
|
|
58
61
|
sendNoQuarter(on: boolean): void;
|
|
62
|
+
// Echo the privacy-filter state (off = pi-privacy disabled). Optional so a
|
|
63
|
+
// transport built against an older shape still satisfies the interface; the
|
|
64
|
+
// bridge's callers all treat a missing echo as "filter on", which is the safe read.
|
|
65
|
+
sendPrivacy?(off: boolean): void;
|
|
59
66
|
sendFile(file: { name: string; mediaType: string; base64: string; size: number }): Promise<{ ok: boolean; reason?: string }>;
|
|
60
67
|
sendNotice(text: string): void;
|
|
61
68
|
sendCommands(commands: { name: string; description?: string }[]): void;
|
|
@@ -190,7 +197,25 @@ export class RemoteBridge {
|
|
|
190
197
|
private turnPrompt = "";
|
|
191
198
|
private turnText = "";
|
|
192
199
|
|
|
193
|
-
|
|
200
|
+
// Unsubscribes the privacy watcher below. Held so a bridge that outlives its window
|
|
201
|
+
// (the desktop builds one per window, in one process) stops writing to a transport
|
|
202
|
+
// whose renderer is gone.
|
|
203
|
+
private readonly unwatchPrivacy: () => void;
|
|
204
|
+
|
|
205
|
+
constructor(private readonly cfg: RemoteBridgeConfig) {
|
|
206
|
+
// The privacy switch has a second driver: `/privacy off` typed into the composer
|
|
207
|
+
// (or into the CLI beside a driving app). Watch the flag itself rather than only
|
|
208
|
+
// the frame, so the app's shield follows the terminal however it was moved β and
|
|
209
|
+
// on the desktop, where the flag is process-wide, so does every OTHER window's,
|
|
210
|
+
// which is the truth: they share the env var this writes.
|
|
211
|
+
this.unwatchPrivacy = onPrivacyDisabledChange((off) => this.relay?.sendPrivacy?.(off));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Release what outlives a socket. Safe to call twice. */
|
|
215
|
+
dispose(): void {
|
|
216
|
+
this.unwatchPrivacy();
|
|
217
|
+
this.relay = undefined;
|
|
218
|
+
}
|
|
194
219
|
|
|
195
220
|
// Wire the outbound relay once it's constructed (RelayClient needs `callbacks`
|
|
196
221
|
// at construction, so the relay is attached right after).
|
|
@@ -286,10 +311,29 @@ export class RemoteBridge {
|
|
|
286
311
|
setNoQuarter(on);
|
|
287
312
|
this.relay?.sendNoQuarter(on); // echo the ack back so the app's toggle syncs
|
|
288
313
|
},
|
|
314
|
+
// The app's privacy switch. It writes the SAME state `/privacy off` writes β the
|
|
315
|
+
// env var every module copy and subagent child reads β so a session toggled from
|
|
316
|
+
// the app and one toggled from the keyboard are the same session, and the CLI's
|
|
317
|
+
// own `/privacy status` tells the truth about what the app did. Echoed back so
|
|
318
|
+
// the app's switch reflects what this terminal is actually doing rather than what
|
|
319
|
+
// it was asked to do.
|
|
320
|
+
onPrivacy: (off) => {
|
|
321
|
+
const changed = privacyDisabled() !== off;
|
|
322
|
+
setPrivacyDisabled(off);
|
|
323
|
+
// A real change is echoed by the watcher in the constructor (which also covers
|
|
324
|
+
// `/privacy` typed at a composer). Ack a no-op ask here so an app whose switch
|
|
325
|
+
// was already out of step still gets an answer rather than silence.
|
|
326
|
+
if (!changed) this.relay?.sendPrivacy?.(off);
|
|
327
|
+
},
|
|
289
328
|
onControllerAttached: () => {
|
|
290
329
|
if (this.noQuarter || noQuarterActive()) {
|
|
291
330
|
this.relay?.sendNoQuarter(true);
|
|
292
331
|
}
|
|
332
|
+
// Resync the privacy switch the same way, and for a sharper reason: the filter
|
|
333
|
+
// may have been turned off from the keyboard, or by a launch flag, or by an
|
|
334
|
+
// earlier session of this app β an attaching controller must not paint a shield
|
|
335
|
+
// over a terminal that has none. Sent only when OFF, so silence means protected.
|
|
336
|
+
if (privacyDisabled()) this.relay?.sendPrivacy?.(true);
|
|
293
337
|
this.cfg.onControllerAttached?.();
|
|
294
338
|
},
|
|
295
339
|
// The app left while we're still running. Same posture as a dropped socket: stop
|
|
@@ -319,6 +363,9 @@ export class RemoteBridge {
|
|
|
319
363
|
getRemote = (): boolean => this.remote;
|
|
320
364
|
getNoQuarter = (): boolean => this.noQuarter || noQuarterActive();
|
|
321
365
|
|
|
366
|
+
/** True while the privacy filter is disabled for this session (`/privacy off`). */
|
|
367
|
+
getPrivacyDisabled = (): boolean => privacyDisabled();
|
|
368
|
+
|
|
322
369
|
// The gate's remote approver: relay the request to the app and await its
|
|
323
370
|
// allow/deny. Fail closed if no controller, on abort, or on disconnect. (The gate
|
|
324
371
|
// also wraps this in its own timeout, so a silent app can't wedge the turn.)
|
package/src/tools/media.ts
CHANGED
|
@@ -1143,6 +1143,9 @@ export const generateSpriteToolDefinition = {
|
|
|
1143
1143
|
"right-facing ones rather than rendered, which is why eight animations cost five clips and not " +
|
|
1144
1144
|
"eight. Each clip is charged at the account's video rate, so an eight-way set is genuinely " +
|
|
1145
1145
|
"expensive; say the total to the user before batching characters.\n" +
|
|
1146
|
+
"TWO models are involved, which matters when one of them is down: an IMAGE model turns the " +
|
|
1147
|
+
"picture to face each direction (only for `directions` 'four' and 'eight'), then a VIDEO model " +
|
|
1148
|
+
"renders the motion per facing. `image_model` and `model` override them separately.\n" +
|
|
1146
1149
|
"It takes several minutes (the clips render sequentially) and this tool waits. Frame count, cell " +
|
|
1147
1150
|
"size and frame rate are chosen here and cost nothing extra. AVAILABILITY: this needs a video " +
|
|
1148
1151
|
"decoder on the Privateer API and some deployments do not have one β call media_capabilities and " +
|
|
@@ -1218,13 +1221,25 @@ export const generateSpriteToolDefinition = {
|
|
|
1218
1221
|
model: Type.Optional(
|
|
1219
1222
|
Type.String({ description: "Video model id to render the motion with. Omit for the account default." }),
|
|
1220
1223
|
),
|
|
1224
|
+
image_model: Type.Optional(
|
|
1225
|
+
Type.String({
|
|
1226
|
+
description:
|
|
1227
|
+
"Image model id used to TURN the picture to face each direction, e.g. " +
|
|
1228
|
+
"'google/gemini-3.1-flash-image'. Omit for the account default. Only used when " +
|
|
1229
|
+
"`directions` is 'four' or 'eight' β 'one' renders no turns and never touches an image " +
|
|
1230
|
+
"model. Worth setting when a run fails with SPRITE_IMAGE_MODEL_UNAVAILABLE: that is the " +
|
|
1231
|
+
"image provider being down, not the request being wrong, and naming another model here " +
|
|
1232
|
+
"retries the whole run for nothing (the turns are drawn before any clip is billed). " +
|
|
1233
|
+
"media_capabilities lists the account's current image model.",
|
|
1234
|
+
}),
|
|
1235
|
+
),
|
|
1221
1236
|
}),
|
|
1222
1237
|
async execute(
|
|
1223
1238
|
_toolCallId: string,
|
|
1224
1239
|
params: {
|
|
1225
1240
|
image: string; prompt: string; dir: string; action?: string; directions?: string;
|
|
1226
1241
|
frames?: number; frame_size?: number; fps?: number; loop?: boolean;
|
|
1227
|
-
name?: string; res_path?: string; model?: string;
|
|
1242
|
+
name?: string; res_path?: string; model?: string; image_model?: string;
|
|
1228
1243
|
},
|
|
1229
1244
|
signal?: AbortSignal,
|
|
1230
1245
|
_onUpdate?: unknown,
|
|
@@ -1264,6 +1279,7 @@ export const generateSpriteToolDefinition = {
|
|
|
1264
1279
|
return guessed ? { res_path: guessed } : {};
|
|
1265
1280
|
})()),
|
|
1266
1281
|
...(params.model ? { model: params.model } : {}),
|
|
1282
|
+
...(params.image_model ? { image_model: params.image_model } : {}),
|
|
1267
1283
|
},
|
|
1268
1284
|
});
|
|
1269
1285
|
if (!submitted.ok) return text(`Sprite generation failed: ${submitted.message}`);
|