privateer-agent 0.6.3 → 0.6.6
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 +35 -0
- package/SECURITY.md +54 -0
- package/bin/apply-patches.d.mts +18 -0
- package/bin/apply-patches.mjs +143 -0
- package/bin/privateer-launch.mjs +151 -10
- package/extensions/privateer-gate.ts +4 -0
- package/package.json +8 -3
- package/patches/@earendil-works+pi-coding-agent+0.80.3.patch +20 -0
- package/skills/resolve-dependencies/SKILL.md +103 -0
- package/src/cli/chat.ts +2 -0
- package/src/config/hosted.ts +36 -0
- package/src/daemon/index.ts +75 -7
- package/src/ext/permissionGate.ts +13 -3
- package/src/permissions/modeGate.ts +14 -0
- package/src/remote/mcpControl.ts +268 -0
- package/src/remote/relayClient.ts +78 -0
- package/src/remote/remoteBridge.ts +7 -0
- package/src/routines/schema.ts +9 -1
|
@@ -182,6 +182,22 @@ export interface RelayCallbacks {
|
|
|
182
182
|
// The app asked to delete a platform's channel config, by platform name. Signed
|
|
183
183
|
// (H2) — a forged removal is a DoS (the bot stops until re-added).
|
|
184
184
|
onChannelsRemove?: (platform: string, sig?: string, ts?: number) => void;
|
|
185
|
+
// The app opened the MCP connectors manager — reply with the current MCP config
|
|
186
|
+
// (a sendMcp frame). Owned by the daemon (the host that runs the adapter), so these
|
|
187
|
+
// only fire on its relay. Read-only, so unsigned.
|
|
188
|
+
onMcpList?: () => void;
|
|
189
|
+
// The app asked to create/edit an MCP connector. `draft` carries only NON-secret
|
|
190
|
+
// fields (name/transport/command/args/url/oauth). `sealedSecrets`, when present, is
|
|
191
|
+
// a base64 sealed-box the app sealed to THIS terminal's pinned pubkey, opening to
|
|
192
|
+
// `{ termId, env: {NAME: value} }` — the connector's credential env. `sig`+`ts`
|
|
193
|
+
// authenticate the WHOLE save with the pinned account key (same shape as
|
|
194
|
+
// channels_save), so a hostile relay can neither forge a token nor inject a command.
|
|
195
|
+
onMcpSave?: (draft: Record<string, unknown>, sealedSecrets?: string, sig?: string, ts?: number) => void;
|
|
196
|
+
// The app asked to enable/disable a connector by name. Signed (H2) — a forged toggle
|
|
197
|
+
// silently arms/disarms a tool surface. Idempotent (non-strict ts).
|
|
198
|
+
onMcpSetEnabled?: (name: string, enabled: boolean, sig?: string, ts?: number) => void;
|
|
199
|
+
// The app asked to delete a connector by name. Signed (H2) — a forged removal is a DoS.
|
|
200
|
+
onMcpRemove?: (name: string, sig?: string, ts?: number) => void;
|
|
185
201
|
// The app opened the workflows manager — reply with the current workflow summaries
|
|
186
202
|
// (a sendWorkflows frame). Owned by the daemon, so these only fire on its relay.
|
|
187
203
|
onWorkflowsList?: () => void;
|
|
@@ -519,6 +535,27 @@ export class RelayClient {
|
|
|
519
535
|
case "channels_remove":
|
|
520
536
|
if (typeof frame.platform === "string") this.cb.onChannelsRemove?.(frame.platform, sig(frame), tsOf(frame));
|
|
521
537
|
break;
|
|
538
|
+
case "mcp_list":
|
|
539
|
+
this.cb.onMcpList?.();
|
|
540
|
+
break;
|
|
541
|
+
case "mcp_save":
|
|
542
|
+
// The connector rides in `draft` (same slot as channels_save), untyped — the
|
|
543
|
+
// daemon strict-validates it via mcpControl.save after the signature check.
|
|
544
|
+
if (frame.draft && typeof frame.draft === "object") {
|
|
545
|
+
this.cb.onMcpSave?.(
|
|
546
|
+
frame.draft,
|
|
547
|
+
typeof frame.sealedSecrets === "string" ? frame.sealedSecrets : undefined,
|
|
548
|
+
sig(frame),
|
|
549
|
+
tsOf(frame),
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
case "mcp_set_enabled":
|
|
554
|
+
if (typeof frame.name === "string") this.cb.onMcpSetEnabled?.(frame.name, frame.enabled === true, sig(frame), tsOf(frame));
|
|
555
|
+
break;
|
|
556
|
+
case "mcp_remove":
|
|
557
|
+
if (typeof frame.name === "string") this.cb.onMcpRemove?.(frame.name, sig(frame), tsOf(frame));
|
|
558
|
+
break;
|
|
522
559
|
case "workflows_list":
|
|
523
560
|
this.cb.onWorkflowsList?.();
|
|
524
561
|
break;
|
|
@@ -875,6 +912,47 @@ export class RelayClient {
|
|
|
875
912
|
});
|
|
876
913
|
}
|
|
877
914
|
|
|
915
|
+
// Push the host's MCP connectors to the app's MCP manager. Sent on request and after
|
|
916
|
+
// each save/set_enabled/remove. Like sendChannels this is the user's OWN config echoed
|
|
917
|
+
// to their OWN app — but an env VALUE (a token) NEVER crosses this wire: only `envKeys`
|
|
918
|
+
// (names) and `secretsSet` (which of those are non-empty, by name) are sent, so a relay
|
|
919
|
+
// / server compromise can't lift a credential from this frame. List bounded like the
|
|
920
|
+
// other managers.
|
|
921
|
+
sendMcp(payload: {
|
|
922
|
+
items: {
|
|
923
|
+
name: string;
|
|
924
|
+
transport: string;
|
|
925
|
+
enabled: boolean;
|
|
926
|
+
command?: string;
|
|
927
|
+
argsPreview?: string;
|
|
928
|
+
url?: string;
|
|
929
|
+
host?: string;
|
|
930
|
+
oauth: boolean;
|
|
931
|
+
envKeys: string[];
|
|
932
|
+
secretsSet: string[];
|
|
933
|
+
}[];
|
|
934
|
+
busy?: boolean;
|
|
935
|
+
message?: string;
|
|
936
|
+
}): void {
|
|
937
|
+
this.rawSend({
|
|
938
|
+
type: "mcp",
|
|
939
|
+
items: payload.items.slice(0, 50).map((m) => ({
|
|
940
|
+
name: clip(String(m.name), 128),
|
|
941
|
+
transport: m.transport === "http" ? "http" : "stdio",
|
|
942
|
+
enabled: !!m.enabled,
|
|
943
|
+
command: m.command ? clip(m.command, 200) : undefined,
|
|
944
|
+
argsPreview: m.argsPreview ? clip(m.argsPreview, 500) : undefined,
|
|
945
|
+
url: m.url ? clip(m.url, 500) : undefined,
|
|
946
|
+
host: m.host ? clip(m.host, 200) : undefined,
|
|
947
|
+
oauth: !!m.oauth,
|
|
948
|
+
envKeys: (m.envKeys ?? []).slice(0, 30).map((k) => clip(String(k), 128)),
|
|
949
|
+
secretsSet: (m.secretsSet ?? []).slice(0, 30).map((k) => clip(String(k), 128)),
|
|
950
|
+
})),
|
|
951
|
+
busy: !!payload.busy,
|
|
952
|
+
message: payload.message ? clip(payload.message, 500) : undefined,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
878
956
|
// Push the daemon's saved workflows to the app's workflows manager as SUMMARIES (not
|
|
879
957
|
// the full graphs — the editor fetches one at a time via sendWorkflow). Sent on request
|
|
880
958
|
// and after each save/remove/run. The user's OWN config echoed to their OWN app, so
|
|
@@ -158,6 +158,13 @@ export class RemoteBridge {
|
|
|
158
158
|
onChannelsList: () => {},
|
|
159
159
|
onChannelsSave: () => {},
|
|
160
160
|
onChannelsRemove: () => {},
|
|
161
|
+
// MCP connectors, like channels, are managed on the daemon (the host that runs the
|
|
162
|
+
// adapter) — the daemon's own relay handles mcp_*. These no-ops just satisfy Required;
|
|
163
|
+
// an interactive terminal manages MCP over IPC (desktop), never over this relay.
|
|
164
|
+
onMcpList: () => {},
|
|
165
|
+
onMcpSave: () => {},
|
|
166
|
+
onMcpSetEnabled: () => {},
|
|
167
|
+
onMcpRemove: () => {},
|
|
161
168
|
// Workflows, like routines/channels, are daemon-owned — the daemon's own relay handles
|
|
162
169
|
// workflows_*. These no-ops just satisfy Required; an interactive terminal never
|
|
163
170
|
// surfaces workflows.
|
package/src/routines/schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
// Where a routine's result is delivered after it runs. `file`/`relay`/`notice` stay
|
|
@@ -79,6 +80,13 @@ export const RoutineFile = z.object({
|
|
|
79
80
|
export type RoutineFile = z.infer<typeof RoutineFile>;
|
|
80
81
|
|
|
81
82
|
// A time-ordered routine id minted once at creation.
|
|
83
|
+
//
|
|
84
|
+
// The random suffix is load-bearing, not decoration. This was `r-${Date.now()}` alone,
|
|
85
|
+
// so two routines created in the SAME MILLISECOND got the same id — and upsertRoutine
|
|
86
|
+
// keys on id, so the second silently overwrote the first. Creating routines in quick
|
|
87
|
+
// succession (an import, a scripted setup, a fast tap-tap in the app) could therefore
|
|
88
|
+
// lose one with no error anywhere. The timestamp prefix still sorts by creation order;
|
|
89
|
+
// the suffix just makes collisions vanishingly unlikely.
|
|
82
90
|
export function newRoutineId(): string {
|
|
83
|
-
return `r-${Date.now()}`;
|
|
91
|
+
return `r-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
84
92
|
}
|