privateer-agent 0.12.36 β 0.12.37
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.37",
|
|
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.)
|