privateer-agent 0.12.24 → 0.12.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.12.24",
3
+ "version": "0.12.25",
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",
@@ -254,8 +254,9 @@ export interface RelayCallbacks {
254
254
  // STRICT mode (it executes a graph — non-idempotent, like task_spawn).
255
255
  onWorkflowsRun?: (idOrName: string, sig?: string, ts?: number) => void;
256
256
  // A file finished transferring from the app (reassembled from chunks). Held to
257
- // ride along with the next remote prompt.
258
- onAttachment: (file: { name: string; mediaType: string; base64: string }) => void;
257
+ // ride along with the next remote prompt. `path` instead of `base64` is the desktop
258
+ // loopback's shape only see RemoteAttachment; nothing on this WebSocket sets it.
259
+ onAttachment: (file: { name: string; mediaType: string; base64?: string; path?: string }) => void;
259
260
  // Surface a one-line status/notice in the TUI.
260
261
  onStatus?: (text: string) => void;
261
262
  // The relay socket closed (controller no longer reachable until reconnect).
@@ -112,7 +112,12 @@ export interface InputRequest {
112
112
  export interface RemoteAttachment {
113
113
  name: string;
114
114
  mediaType: string;
115
- base64: string;
115
+ // Exactly one of these. `base64` is the cloud relay's shape: the app is on another
116
+ // machine, so the bytes are chunked across. `path` is the desktop's: app and agent
117
+ // share a disk (IpcRelay), so the file is named rather than moved — which is why a
118
+ // desktop attachment has no size cap. Both are consumed by AttachmentStore.register.
119
+ base64?: string;
120
+ path?: string;
116
121
  }
117
122
 
118
123
  export interface RemoteBridgeConfig {
@@ -4,7 +4,7 @@
4
4
  // extension gates the tool_call, classified as a write against the destination path).
5
5
 
6
6
  import { Type } from "typebox";
7
- import { writeFileSync, mkdirSync, readFileSync } from "node:fs";
7
+ import { mkdirSync, copyFileSync, statSync } from "node:fs";
8
8
  import { dirname, isAbsolute, resolve } from "node:path";
9
9
  import type { AttachmentStore } from "../util/attachmentStore.ts";
10
10
 
@@ -42,9 +42,13 @@ export function makeSaveAttachmentTool(store: AttachmentStore) {
42
42
  const cwd = ctx?.cwd ?? process.cwd();
43
43
  const abs = isAbsolute(params.path) ? params.path : resolve(cwd, params.path);
44
44
  mkdirSync(dirname(abs), { recursive: true });
45
- const bytes = readFileSync(entry.path);
46
- writeFileSync(abs, bytes);
47
- return text(`Saved attachment #${params.ref} (${entry.name}, ${entry.mediaType}) to ${params.path} (${bytes.length} bytes).`);
45
+ // copy rather than read-then-write: a desktop attachment is the user's own file
46
+ // at its own path (AttachmentStore adopts it instead of staging a copy), and
47
+ // those have no size cap — reading a 4 GB video into the heap to write it back
48
+ // out would be the one place the uncapped path could still fall over.
49
+ copyFileSync(entry.path, abs);
50
+ const size = statSync(abs).size;
51
+ return text(`Saved attachment #${params.ref} (${entry.name}, ${entry.mediaType}) to ${params.path} (${size} bytes).`);
48
52
  },
49
53
  };
50
54
  }
@@ -6,12 +6,19 @@ import { join, extname } from "node:path";
6
6
  // scratch dir keyed by the "#n" reference the model sees, so the save_attachment tool
7
7
  // can write one back out to a real path on demand. Ported/adapted from tree-cli
8
8
  // (which persisted paste/drop bytes); here the source is inbound relay attachments.
9
+ //
10
+ // A DESKTOP attachment arrives as a path instead of bytes — the app and this agent
11
+ // share a disk, so there is nothing to transfer and nothing to stage. Such an entry
12
+ // simply points at the file where it already lives; `owned` is what tells the two
13
+ // apart, so cleanup() removes only what we wrote. That is also why the desktop
14
+ // composer has no attachment size cap: a 4 GB video costs one string here.
9
15
 
10
16
  export interface StoredAttachment {
11
17
  n: number;
12
- path: string; // absolute scratch-file path holding the decoded bytes
18
+ path: string; // absolute path holding the bytes — our scratch file, or the user's own file
13
19
  mediaType: string;
14
20
  name: string; // original filename from the app
21
+ owned: boolean; // did WE write this file? false for a desktop path hand-off
15
22
  }
16
23
 
17
24
  export class AttachmentStore {
@@ -26,13 +33,24 @@ export class AttachmentStore {
26
33
  return this.dir;
27
34
  }
28
35
 
29
- // Persist an inbound attachment's bytes, assign it the next ref number, and return
30
- // the stored record (its ref + scratch path).
31
- register(file: { name: string; mediaType: string; base64: string }): StoredAttachment {
36
+ // Register an inbound attachment under the next ref number and return the stored
37
+ // record. Bytes are staged into the scratch dir; a `path` (desktop) is adopted
38
+ // as-is — copying a file the agent can already open would be pure waste, and for
39
+ // the large files this path exists to carry, waste measured in gigabytes.
40
+ register(file: { name: string; mediaType: string; base64?: string; path?: string }): StoredAttachment {
32
41
  const n = this.nextN++;
42
+ if (file.path) {
43
+ const stored: StoredAttachment = {
44
+ n, path: file.path, mediaType: file.mediaType, name: file.name, owned: false,
45
+ };
46
+ this.byN.set(n, stored);
47
+ return stored;
48
+ }
33
49
  const path = join(this.ensureDir(), `att-${n}${extname(file.name) || ""}`);
34
- writeFileSync(path, Buffer.from(file.base64, "base64"));
35
- const stored: StoredAttachment = { n, path, mediaType: file.mediaType, name: file.name };
50
+ writeFileSync(path, Buffer.from(file.base64 ?? "", "base64"));
51
+ const stored: StoredAttachment = {
52
+ n, path, mediaType: file.mediaType, name: file.name, owned: true,
53
+ };
36
54
  this.byN.set(n, stored);
37
55
  return stored;
38
56
  }