privateer-agent 0.12.8 → 0.12.11
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/README.md +29 -10
- package/SECURITY.md +41 -5
- package/bin/privateer-launch.mjs +10 -0
- package/bin/privateer-verify.d.mts +10 -0
- package/bin/privateer-verify.mjs +289 -0
- package/extensions/privateer-brand.ts +8 -2
- package/extensions/privateer-hints.ts +8 -2
- package/extensions/privateer-speak.ts +1 -1
- package/package.json +25 -25
- package/patches/{@earendil-works+pi-coding-agent+0.80.3.patch → @earendil-works+pi-coding-agent+0.84.1.patch} +138 -70
- package/src/acp/run.ts +5 -4
- package/src/channels/run.ts +3 -1
- package/src/cli/chat.ts +7 -5
- package/src/config/moat.ts +13 -0
- package/src/harbor/index.ts +72 -21
- package/src/main.ts +1 -1
- package/src/nostr/event.ts +1 -1
- package/src/outbox/cloudOutbox.ts +179 -3
- package/src/providers/account.ts +38 -30
- package/src/providers/defaultModel.ts +1 -1
- package/src/providers/piAuthStore.ts +129 -0
- package/src/providers/sealedShim.ts +1 -1
- package/src/remote/liveTaskSession.ts +6 -6
- package/src/routines/delivery.ts +15 -2
- package/src/routines/resultBrief.ts +60 -0
- package/src/routines/resultMedia.ts +131 -0
- package/src/routines/schema.ts +1 -1
- package/src/routines/store.ts +7 -0
- package/src/session.ts +26 -9
- package/src/tools/attachResult.ts +66 -0
- package/src/tools/media.ts +2 -2
- package/src/tools/sendFile.ts +4 -1
- package/src/tools/videoCompose.ts +1 -1
- package/src/tools/web.ts +1 -1
package/src/session.ts
CHANGED
|
@@ -16,8 +16,18 @@ import {
|
|
|
16
16
|
SessionManager,
|
|
17
17
|
SettingsManager,
|
|
18
18
|
ModelRegistry,
|
|
19
|
-
|
|
19
|
+
ModelRuntime,
|
|
20
20
|
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
// From pi-ai, deliberately, not pi-coding-agent's `AuthStorage.inMemory()`. 0.84
|
|
22
|
+
// stopped exporting AuthStorage and our pi patch puts the export back — which is
|
|
23
|
+
// fine for the one caller that genuinely needs the file-backed store
|
|
24
|
+
// (providers/piAuthStore.ts) and wrong here, because patches are best-effort by
|
|
25
|
+
// design: `bin/apply-patches.mjs` degrades to stock pi when it cannot write to
|
|
26
|
+
// node_modules (the `sudo npm i -g` case it calls out). A static import of a
|
|
27
|
+
// patched-in export turns that graceful degradation into a hard ERR_IMPORT crash
|
|
28
|
+
// on every session start. pi-ai exports this store unpatched and it implements the
|
|
29
|
+
// same CredentialStore interface, so nothing about the session changes.
|
|
30
|
+
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
21
31
|
|
|
22
32
|
import { agentDir as defaultAgentDir } from "./config/paths.ts";
|
|
23
33
|
import { createEngineEventAdapter } from "./bridge/engineAdapter.ts";
|
|
@@ -46,15 +56,23 @@ export interface PrivateerSession {
|
|
|
46
56
|
export async function createSession(opts: CreateSessionOptions): Promise<PrivateerSession> {
|
|
47
57
|
const AGENT_DIR = opts.agentDir ?? process.env.PI_CODING_AGENT_DIR ?? defaultAgentDir();
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
// pi 0.84: ModelRegistry.create(authStorage, modelsPath) is gone. The registry is now
|
|
60
|
+
// a synchronous facade over a ModelRuntime, and the runtime is what owns credentials —
|
|
61
|
+
// so the store this session used to hand to the registry is injected into the runtime
|
|
62
|
+
// instead. Still in-memory: a headless session must not touch the machine's auth.json.
|
|
63
|
+
const authStorage = new InMemoryCredentialStore();
|
|
64
|
+
const modelRuntime = await ModelRuntime.create({
|
|
65
|
+
credentials: authStorage,
|
|
66
|
+
modelsPath: `${AGENT_DIR}/models.json`,
|
|
67
|
+
});
|
|
68
|
+
const modelRegistry = new ModelRegistry(modelRuntime);
|
|
69
|
+
// Explicit, and awaited: find() below is synchronous, so models.json has to be loaded
|
|
70
|
+
// before it runs or the lookup fails on a cold cache.
|
|
71
|
+
await modelRegistry.refresh();
|
|
54
72
|
|
|
55
73
|
const model = modelRegistry.find(opts.provider, opts.modelId);
|
|
56
74
|
if (!model) {
|
|
57
|
-
const ids = (
|
|
75
|
+
const ids = (modelRegistry.getAll() ?? []).map(
|
|
58
76
|
(m: any) => `${m.provider}/${m.id}`,
|
|
59
77
|
);
|
|
60
78
|
throw new Error(
|
|
@@ -75,8 +93,7 @@ export async function createSession(opts: CreateSessionOptions): Promise<Private
|
|
|
75
93
|
cwd: opts.cwd,
|
|
76
94
|
agentDir: AGENT_DIR,
|
|
77
95
|
model,
|
|
78
|
-
|
|
79
|
-
modelRegistry,
|
|
96
|
+
modelRuntime,
|
|
80
97
|
settingsManager,
|
|
81
98
|
sessionManager: SessionManager.inMemory(opts.cwd),
|
|
82
99
|
resourceLoader: loader,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The `attach_to_result` tool — hand a file from disk to the Privateer Inbox
|
|
2
|
+
// alongside the answer this unattended run is about to deliver.
|
|
3
|
+
//
|
|
4
|
+
// The sibling of `send_file_to_client`, for the case that tool cannot serve: there
|
|
5
|
+
// is no controller attached and there won't be one. A routine that fires at 7am,
|
|
6
|
+
// or a task submitted from a phone that then went in a pocket, finishes into the
|
|
7
|
+
// cloud outbox — so its media has to travel the same way, sealed to the account
|
|
8
|
+
// key and collected when the app next syncs (src/routines/resultMedia.ts explains
|
|
9
|
+
// the transport; the ceilings live there too).
|
|
10
|
+
//
|
|
11
|
+
// Staging only. Nothing is read, sealed or uploaded here — the run may still fail
|
|
12
|
+
// after this call, and delivery decides what actually goes. That also keeps the
|
|
13
|
+
// tool cheap enough for a model to use freely: it is a promise about the answer,
|
|
14
|
+
// not an upload.
|
|
15
|
+
|
|
16
|
+
import { Type } from "typebox";
|
|
17
|
+
import type { ResultMedia } from "../routines/resultMedia.ts";
|
|
18
|
+
import { MAX_ATTACHMENTS, MAX_ATTACHMENT_BYTES } from "../routines/resultMedia.ts";
|
|
19
|
+
|
|
20
|
+
function text(t: string) {
|
|
21
|
+
return { content: [{ type: "text", text: t }], details: {} };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ATTACH_RESULT_TOOL = "attach_to_result";
|
|
25
|
+
|
|
26
|
+
export function makeAttachResultTool(media: ResultMedia) {
|
|
27
|
+
return {
|
|
28
|
+
name: ATTACH_RESULT_TOOL,
|
|
29
|
+
label: "Attach to Result",
|
|
30
|
+
description:
|
|
31
|
+
"Attach a file from disk — an image, video, audio clip or document — to the result this run " +
|
|
32
|
+
"delivers to the user's Privateer Inbox, where they can view, save and share it on their device. " +
|
|
33
|
+
"Use it for anything you generated or found that is better seen than described; the user is NOT " +
|
|
34
|
+
"at this machine, so a file path in the answer is not something they can open. Attach the file " +
|
|
35
|
+
`itself rather than pasting base64 into the answer. Up to ${MAX_ATTACHMENTS} files per result, ` +
|
|
36
|
+
`${Math.round(MAX_ATTACHMENT_BYTES / 1048576)} MB each. Attaching the same path twice replaces the earlier entry.`,
|
|
37
|
+
parameters: Type.Object({
|
|
38
|
+
path: Type.String({ description: "Path of the file to attach, relative to cwd or absolute." }),
|
|
39
|
+
caption: Type.Optional(
|
|
40
|
+
Type.String({ description: "One short line shown under the attachment in the app." }),
|
|
41
|
+
),
|
|
42
|
+
}),
|
|
43
|
+
async execute(
|
|
44
|
+
_toolCallId: string,
|
|
45
|
+
params: { path: string; caption?: string },
|
|
46
|
+
_signal?: AbortSignal,
|
|
47
|
+
_onUpdate?: unknown,
|
|
48
|
+
ctx?: { cwd?: string },
|
|
49
|
+
) {
|
|
50
|
+
const res = media.stage(params.path, ctx?.cwd ?? process.cwd(), params.caption);
|
|
51
|
+
if (!res.ok) return text(`Not attached — ${res.reason}`);
|
|
52
|
+
const kb = Math.max(1, Math.round(res.item.size / 1024));
|
|
53
|
+
return text(
|
|
54
|
+
`Attached ${res.item.name} (${kb} KB, ${res.item.mediaType}). It will be delivered with this result. ` +
|
|
55
|
+
`Write the answer as if the user can see it — don't paste the file path as the way to open it.`,
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Registers the tool for a session that has a staging area. */
|
|
62
|
+
export function makeAttachResultTools(media: ResultMedia) {
|
|
63
|
+
return function attachResultTools(pi: any): void {
|
|
64
|
+
pi.registerTool?.(makeAttachResultTool(media));
|
|
65
|
+
};
|
|
66
|
+
}
|
package/src/tools/media.ts
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
// user has explicitly opted into non-ZDR media (`ZDR_MEDIA_BLOCKED`), and these tools
|
|
10
10
|
// surface that refusal verbatim rather than papering over it.
|
|
11
11
|
//
|
|
12
|
-
// WHAT THIS COSTS, HONESTLY. Generation is NOT
|
|
12
|
+
// WHAT THIS COSTS, HONESTLY. Generation is NOT encrypted and cannot be. The
|
|
13
13
|
// prompt, any input image, and the finished bytes pass through Privateer's servers in
|
|
14
14
|
// plaintext on the way to and from the model provider — that is what generation IS.
|
|
15
15
|
// What we do control: nothing is persisted server-side. The bytes come back inline and
|
|
16
16
|
// land only in the file you name. Never describe a routine that generates media as
|
|
17
|
-
// private
|
|
17
|
+
// fully private; do say the output isn't stored in our cloud.
|
|
18
18
|
//
|
|
19
19
|
// MUSIC IS THE LOOSEST OF THESE. Neither Lyria SKU has a zero-retention endpoint and no
|
|
20
20
|
// confidential music model exists, so music is deliberately exempt from the ZDR gate
|
package/src/tools/sendFile.ts
CHANGED
|
@@ -16,7 +16,10 @@ const MEDIA: Record<string, string> = {
|
|
|
16
16
|
json: "application/json", csv: "text/csv", html: "text/html", mp4: "video/mp4",
|
|
17
17
|
mov: "video/quicktime", mp3: "audio/mpeg", wav: "audio/wav", zip: "application/zip",
|
|
18
18
|
};
|
|
19
|
-
|
|
19
|
+
// Exported because the inbox-attachment path (routines/resultMedia.ts) types files
|
|
20
|
+
// by the same table: two tables would drift, and the type decides how the app
|
|
21
|
+
// presents an attachment (image inline, video in a player, everything else a chip).
|
|
22
|
+
export function mediaTypeForPath(p: string): string {
|
|
20
23
|
const ext = p.split(".").pop()?.toLowerCase() ?? "";
|
|
21
24
|
return MEDIA[ext] ?? "application/octet-stream";
|
|
22
25
|
}
|
|
@@ -255,7 +255,7 @@ async function opProbe(cwd: string, p: Params, signal?: AbortSignal): Promise<st
|
|
|
255
255
|
.join("\n");
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
// Stitch clips
|
|
258
|
+
// Stitch clips back-to-back, optionally with a crossfade between them.
|
|
259
259
|
async function opConcat(cwd: string, p: Params, signal?: AbortSignal): Promise<string> {
|
|
260
260
|
const list = p.inputs ?? [];
|
|
261
261
|
if (list.length < 2) throw new ToolError("`concat` needs at least two paths in `inputs`.");
|
package/src/tools/web.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// not, but the derived query does. That is the same residual metadata leak already
|
|
17
17
|
// documented for Sealed mode in the server's routes/rag.js header, and it is why web
|
|
18
18
|
// access is a switch on the agent rather than an unconditional capability. Never
|
|
19
|
-
// describe a routine that searches as private
|
|
19
|
+
// describe a routine that searches as fully private.
|
|
20
20
|
//
|
|
21
21
|
// SCOPE. These are the UNATTENDED paths' web tools: the harbor (routines, tasks,
|
|
22
22
|
// workflow agent steps) and channels, both of which build a session from an explicit
|