privateer-agent 0.12.13 → 0.12.15
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 +7 -0
- package/bin/privateer-subagent.mjs +10 -2
- package/extensions/privateer-desktop.ts +54 -0
- package/extensions/privateer-gate.ts +7 -0
- package/extensions/privateer-hints.ts +33 -2
- package/extensions/privateer-media.ts +17 -4
- package/extensions/privateer-privacy.ts +13 -33
- package/extensions/privateer-tools.ts +8 -3
- package/package.json +1 -1
- package/patches/@earendil-works+pi-coding-agent+0.84.1.patch +102 -5
- package/src/acp/run.ts +29 -2
- package/src/channels/run.ts +9 -0
- package/src/cli/chat.ts +47 -0
- package/src/config/desktopApp.ts +138 -0
- package/src/config/moat.ts +6 -26
- package/src/config/moatManifest.json +1 -0
- package/src/config/moatManifest.ts +5 -0
- package/src/config/privacyPolicy.ts +97 -0
- package/src/engine/errors.ts +97 -1
- package/src/ext/permissionGate.ts +6 -0
- package/src/harbor/index.ts +65 -11
- package/src/outbox/cloudOutbox.ts +72 -4
- package/src/permissions/childSpend.ts +105 -0
- package/src/permissions/classify.ts +61 -3
- package/src/permissions/modeGate.ts +39 -0
- package/src/providers/account.ts +8 -1
- package/src/providers/phala/measurements.ts +170 -0
- package/src/providers/phala/pin.ts +98 -0
- package/src/providers/phalaSeal.ts +131 -9
- package/src/providers/sealedShim.ts +6 -1
- package/src/remote/liveTaskSession.ts +8 -1
- package/src/remote/relayClient.ts +21 -0
- package/src/remote/remoteBridge.ts +9 -0
- package/src/routines/store.ts +11 -0
- package/src/tools/media.ts +516 -11
- package/src/tools/routineResult.ts +146 -0
- package/src/tools/videoCompose.ts +825 -7
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Trust-on-first-use pinning for the Phala enclave's identity.
|
|
2
|
+
//
|
|
3
|
+
// Why TOFU and not a real pin: Phala publishes no registry of known-good measurements.
|
|
4
|
+
// The expected MRTD/RTMR0-2 are *computed* with dstack-mr from the reproducible dstack
|
|
5
|
+
// OS build, and the gateway's compose-hash from its published source — neither of which
|
|
6
|
+
// we do yet. So the honest guarantee is narrow and worth stating plainly: we can tell
|
|
7
|
+
// you the image CHANGED, never that it was right to begin with.
|
|
8
|
+
//
|
|
9
|
+
// That shapes the semantics deliberately:
|
|
10
|
+
// - first sight records and reports `first-seen` — never "verified"
|
|
11
|
+
// - a later mismatch reports `changed` with the exact fields that moved
|
|
12
|
+
// - `changed` is NOT a failure. Phala upgrades the gateway legitimately, and hard-
|
|
13
|
+
// failing on every upgrade would train the user to ignore the one that matters.
|
|
14
|
+
// The verdict stays where the cryptography is; this only ever adds a visible state.
|
|
15
|
+
|
|
16
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { globalDir } from "../../config/paths.ts";
|
|
19
|
+
|
|
20
|
+
// The fields worth pinning: the image identity (MRTD + boot registers, os-image-hash)
|
|
21
|
+
// and the app identity (app-id, compose-hash, key-management root). Deliberately NOT
|
|
22
|
+
// instance-id — it changes on every restart of the same image, so pinning it would
|
|
23
|
+
// cry wolf constantly and bury a real change.
|
|
24
|
+
export interface PhalaPin {
|
|
25
|
+
mrTd?: string;
|
|
26
|
+
rtMr0?: string;
|
|
27
|
+
rtMr1?: string;
|
|
28
|
+
rtMr2?: string;
|
|
29
|
+
appId?: string;
|
|
30
|
+
composeHash?: string;
|
|
31
|
+
osImageHash?: string;
|
|
32
|
+
mrKms?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type PinState = "first-seen" | "unchanged" | "changed";
|
|
36
|
+
|
|
37
|
+
export interface PinResult {
|
|
38
|
+
state: PinState;
|
|
39
|
+
// Field-level drift, e.g. "composeHash: 73fa4608… → 91bd0c2a…". Empty unless changed.
|
|
40
|
+
changed: string[];
|
|
41
|
+
firstSeenAt?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PIN_VERSION = 1;
|
|
45
|
+
|
|
46
|
+
function pinPath(): string {
|
|
47
|
+
return join(globalDir(), "phala-enclave-pin.json");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readPin(): { v?: number; firstSeenAt?: string; pin?: PhalaPin } | null {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(readFileSync(pinPath(), "utf8")) as { v?: number; firstSeenAt?: string; pin?: PhalaPin };
|
|
53
|
+
if (parsed?.v !== PIN_VERSION || !parsed.pin || typeof parsed.pin !== "object") return null;
|
|
54
|
+
return parsed;
|
|
55
|
+
} catch {
|
|
56
|
+
return null; // absent, unreadable, or garbage — treated as first sight
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writePin(pin: PhalaPin, firstSeenAt: string): void {
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(globalDir(), { recursive: true });
|
|
63
|
+
writeFileSync(pinPath(), JSON.stringify({ v: PIN_VERSION, firstSeenAt, pin }, null, 2) + "\n", "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
// Unwritable home: every run then reports first-seen. Degrading to "no memory" is
|
|
66
|
+
// right — the alternative is inventing an "unchanged" we cannot substantiate.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Compare against the stored pin, recording it on first sight.
|
|
71
|
+
//
|
|
72
|
+
// `now` is injected rather than read from the clock so tests are deterministic; the
|
|
73
|
+
// timestamp is display-only (it never participates in the comparison).
|
|
74
|
+
export function checkPin(current: PhalaPin, now: () => string = () => new Date().toISOString()): PinResult {
|
|
75
|
+
const stored = readPin();
|
|
76
|
+
if (!stored?.pin) {
|
|
77
|
+
const firstSeenAt = now();
|
|
78
|
+
writePin(current, firstSeenAt);
|
|
79
|
+
return { state: "first-seen", changed: [], firstSeenAt };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const changed: string[] = [];
|
|
83
|
+
for (const key of Object.keys(current) as (keyof PhalaPin)[]) {
|
|
84
|
+
const was = stored.pin[key];
|
|
85
|
+
const is = current[key];
|
|
86
|
+
// Only compare fields present on BOTH sides. A field the gateway stopped sending
|
|
87
|
+
// is an absence, not a substitution, and calling it drift would be a false alarm.
|
|
88
|
+
if (was && is && was !== is) changed.push(`${key}: ${was.slice(0, 12)}… → ${is.slice(0, 12)}…`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (changed.length === 0) return { state: "unchanged", changed: [], firstSeenAt: stored.firstSeenAt };
|
|
92
|
+
|
|
93
|
+
// Re-pin to the new identity so the change is reported ONCE. Leaving the old pin in
|
|
94
|
+
// place would repeat the same warning every turn until it became wallpaper — and the
|
|
95
|
+
// user has already been shown exactly what moved.
|
|
96
|
+
writePin(current, stored.firstSeenAt ?? now());
|
|
97
|
+
return { state: "changed", changed, firstSeenAt: stored.firstSeenAt };
|
|
98
|
+
}
|
|
@@ -34,6 +34,13 @@ import {
|
|
|
34
34
|
// refuses. This wrapper delegates ed25519 to it unchanged and adds the secp256k1 arm
|
|
35
35
|
// the spec allows (§4.3), leaving aci-verifier/ pristine for re-pulls.
|
|
36
36
|
import { verifyAciReportBinding } from "./phala/reportBinding.ts";
|
|
37
|
+
import {
|
|
38
|
+
parseEventLog,
|
|
39
|
+
appIdentityFrom,
|
|
40
|
+
checkReportConsistency,
|
|
41
|
+
type PhalaAppIdentity,
|
|
42
|
+
} from "./phala/measurements.ts";
|
|
43
|
+
import { checkPin, type PinResult } from "./phala/pin.ts";
|
|
37
44
|
import { serverBaseUrl } from "../auth/privateer.ts";
|
|
38
45
|
|
|
39
46
|
const DEFAULT_ACCEPTABLE_TCB = ["UpToDate"];
|
|
@@ -56,6 +63,51 @@ function acceptableTcb(): Set<string> {
|
|
|
56
63
|
return new Set(list);
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
// The launch measurements carried by a verified TDX quote. MRTD measures the initial
|
|
67
|
+
// TD build (the dstack OS image); RTMR0-2 accumulate firmware/kernel/config and RTMR3
|
|
68
|
+
// carries the app-level extensions (compose hash, app id).
|
|
69
|
+
//
|
|
70
|
+
// READING these is not CHECKING them. Nothing here compares a measurement against a
|
|
71
|
+
// known-good value, so they are evidence to display and pin later — never a verdict.
|
|
72
|
+
// The quote's signature is verified before we get here, so the bytes are authentic:
|
|
73
|
+
// what is unproven is that this particular image is the one we intend to be talking to.
|
|
74
|
+
export interface PhalaMeasurements {
|
|
75
|
+
mrTd: string;
|
|
76
|
+
rtMr0: string;
|
|
77
|
+
rtMr1: string;
|
|
78
|
+
rtMr2: string;
|
|
79
|
+
rtMr3: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Pull the measurements off a verified quote report (TD1.0/1.5 layouts), or undefined
|
|
83
|
+
// when the report carries no TD measurement block (e.g. an SGX quote).
|
|
84
|
+
export function extractQuoteMeasurements(report: Report): PhalaMeasurements | undefined {
|
|
85
|
+
const td = report.asTd10?.() ?? report.asTd15?.()?.base;
|
|
86
|
+
const fields = [td?.mrTd, td?.rtMr0, td?.rtMr1, td?.rtMr2, td?.rtMr3];
|
|
87
|
+
// All five or none. A partial set still reads as evidence while silently omitting
|
|
88
|
+
// the register that would have contradicted it — RTMR3 (the app layer) most of all.
|
|
89
|
+
if (fields.some((f) => !f?.length)) return undefined;
|
|
90
|
+
const [mrTd, rtMr0, rtMr1, rtMr2, rtMr3] = fields.map((f) => toHex(new Uint8Array(f as Uint8Array)));
|
|
91
|
+
return { mrTd, rtMr0, rtMr1, rtMr2, rtMr3 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Everything we can say about the enclave that answered, once its quote verified.
|
|
95
|
+
// `measurements` and `identity` are what the hardware signed; `pin` is our own memory
|
|
96
|
+
// of it; the provenance and downstream fields are the enclave's self-declarations,
|
|
97
|
+
// carried for display precisely because they are NOT proven by the quote.
|
|
98
|
+
export interface PhalaEnclaveIdentity {
|
|
99
|
+
measurements: PhalaMeasurements;
|
|
100
|
+
identity: PhalaAppIdentity;
|
|
101
|
+
pin: PinResult;
|
|
102
|
+
// Self-consistency checks we could not run (absent event log / app_compose). Named
|
|
103
|
+
// so "we didn't check" can never be read as "we checked and it passed".
|
|
104
|
+
skippedChecks: string[];
|
|
105
|
+
repoUrl?: string;
|
|
106
|
+
repoCommit?: string;
|
|
107
|
+
downstreamDomain?: string;
|
|
108
|
+
downstreamSpkiSha256?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
59
111
|
// The 64-byte report_data from a verified TDX quote report (TD1.0/1.5 layouts).
|
|
60
112
|
function extractQuoteReportData(report: Report): Uint8Array {
|
|
61
113
|
const td10 = report.asTd10?.();
|
|
@@ -70,6 +122,9 @@ function extractQuoteReportData(report: Report): Uint8Array {
|
|
|
70
122
|
interface VerifiedAttestation {
|
|
71
123
|
report: AttestationReport;
|
|
72
124
|
verification: ReportVerification;
|
|
125
|
+
// Absent when the hardware quote was skipped (requireQuote off): with no verified
|
|
126
|
+
// quote there is nothing about the enclave we are entitled to show.
|
|
127
|
+
enclave?: PhalaEnclaveIdentity;
|
|
73
128
|
}
|
|
74
129
|
|
|
75
130
|
// Attest once, cache the verified report; drop the memo on failure so a later call
|
|
@@ -103,17 +158,24 @@ async function establishAttestation(): Promise<VerifiedAttestation> {
|
|
|
103
158
|
const failed = verification.checks.filter((c) => !c.ok).map((c) => c.name).join(", ");
|
|
104
159
|
throw new Error(`phala attestation binding failed: ${failed}`);
|
|
105
160
|
}
|
|
106
|
-
await verifyHardwareQuote(report);
|
|
107
|
-
return { report, verification };
|
|
161
|
+
const enclave = await verifyHardwareQuote(report);
|
|
162
|
+
return { report, verification, enclave };
|
|
108
163
|
}
|
|
109
164
|
|
|
110
|
-
async function verifyHardwareQuote(report: AttestationReport): Promise<
|
|
111
|
-
if (!requireQuote()) return;
|
|
165
|
+
async function verifyHardwareQuote(report: AttestationReport): Promise<PhalaEnclaveIdentity | undefined> {
|
|
166
|
+
if (!requireQuote()) return undefined;
|
|
112
167
|
|
|
113
168
|
const attestation = report.attestation as unknown as {
|
|
114
169
|
tee_type?: string;
|
|
115
170
|
report_data?: string;
|
|
116
|
-
|
|
171
|
+
source_provenance?: { repo_url?: string; repo_commit?: string };
|
|
172
|
+
evidence?: {
|
|
173
|
+
quote?: string;
|
|
174
|
+
quote_report_data?: string;
|
|
175
|
+
event_log?: unknown;
|
|
176
|
+
app_compose?: unknown;
|
|
177
|
+
downstream_tls_binding?: { domain?: string; spki_sha256?: string };
|
|
178
|
+
};
|
|
117
179
|
};
|
|
118
180
|
const teeType = String(attestation?.tee_type || "");
|
|
119
181
|
if (teeType !== "tdx") throw new Error(`phala: unsupported/absent tee_type "${teeType}" (only tdx is wired)`);
|
|
@@ -141,14 +203,74 @@ async function verifyHardwareQuote(report: AttestationReport): Promise<void> {
|
|
|
141
203
|
if (typeof declared === "string" && declared && toHex(quoteReportData) !== declared.toLowerCase()) {
|
|
142
204
|
throw new Error("phala: evidence.quote_report_data does not match the verified quote");
|
|
143
205
|
}
|
|
206
|
+
|
|
207
|
+
// 4) The quote is authentic, so its measurement registers are trustworthy bytes.
|
|
208
|
+
const measurements = extractQuoteMeasurements(verified.report);
|
|
209
|
+
if (!measurements) return undefined; // non-TD quote: nothing further to check
|
|
210
|
+
|
|
211
|
+
// 5) SELF-CONSISTENCY GATES. The event log must replay to the registers the hardware
|
|
212
|
+
// signed, and the shipped app_compose must hash to the compose-hash the log
|
|
213
|
+
// attests. Both are checkable from the report alone, so a failure means the report
|
|
214
|
+
// is doctored or malformed — refuse it rather than showing a green shield over it.
|
|
215
|
+
const events = parseEventLog(attestation.evidence?.event_log);
|
|
216
|
+
const identity = appIdentityFrom(events);
|
|
217
|
+
const consistency = checkReportConsistency({
|
|
218
|
+
events,
|
|
219
|
+
quoted: measurements,
|
|
220
|
+
appCompose: attestation.evidence?.app_compose,
|
|
221
|
+
identity,
|
|
222
|
+
});
|
|
223
|
+
if (!consistency.ok) {
|
|
224
|
+
const failed = consistency.checks.filter((c) => !c.ok).map((c) => `${c.name} (${c.detail})`).join("; ");
|
|
225
|
+
throw new Error(`phala: attestation self-consistency failed: ${failed}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 6) IDENTITY, as evidence only. TOFU against the stored pin — this can say the image
|
|
229
|
+
// CHANGED, never that it is the right one (no published registry to check against;
|
|
230
|
+
// see phala/pin.ts). It moves no verdict and never throws.
|
|
231
|
+
const pin = checkPin({
|
|
232
|
+
mrTd: measurements.mrTd,
|
|
233
|
+
rtMr0: measurements.rtMr0,
|
|
234
|
+
rtMr1: measurements.rtMr1,
|
|
235
|
+
rtMr2: measurements.rtMr2,
|
|
236
|
+
appId: identity.appId,
|
|
237
|
+
composeHash: identity.composeHash,
|
|
238
|
+
osImageHash: identity.osImageHash,
|
|
239
|
+
mrKms: identity.mrKms,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const provenance = attestation.source_provenance;
|
|
243
|
+
const downstream = attestation.evidence?.downstream_tls_binding;
|
|
244
|
+
return {
|
|
245
|
+
measurements,
|
|
246
|
+
identity,
|
|
247
|
+
pin,
|
|
248
|
+
skippedChecks: consistency.skipped,
|
|
249
|
+
// Named so a human can go read the source; self-declared by the enclave and NOT
|
|
250
|
+
// proven by the quote (image_digest/image_provenance come back null), so it is a
|
|
251
|
+
// pointer to audit, never evidence that this binary came from that commit.
|
|
252
|
+
repoUrl: provenance?.repo_url,
|
|
253
|
+
repoCommit: provenance?.repo_commit,
|
|
254
|
+
// The attested enclave forwards to this downstream host over TLS. Surfaced because
|
|
255
|
+
// it marks where our attested boundary ENDS — that host is a separate trust domain
|
|
256
|
+
// we do not attest.
|
|
257
|
+
downstreamDomain: downstream?.domain,
|
|
258
|
+
downstreamSpkiSha256: downstream?.spki_sha256,
|
|
259
|
+
};
|
|
144
260
|
}
|
|
145
261
|
|
|
146
262
|
// Posture signal: does the attested keyset verify (crypto binding + hardware quote)?
|
|
147
|
-
// A green result is a quote WE checked, bound to the E2EE key we seal to.
|
|
148
|
-
|
|
263
|
+
// A green result is a quote WE checked, bound to the E2EE key we seal to. The
|
|
264
|
+
// measurements ride along as evidence of WHICH image answered — unpinned, so they
|
|
265
|
+
// inform the display without moving the verdict.
|
|
266
|
+
export async function attestPhala(): Promise<{
|
|
267
|
+
ok: boolean;
|
|
268
|
+
error?: string;
|
|
269
|
+
enclaveIdentity?: PhalaEnclaveIdentity;
|
|
270
|
+
}> {
|
|
149
271
|
try {
|
|
150
|
-
await attest();
|
|
151
|
-
return { ok: true };
|
|
272
|
+
const { enclave } = await attest();
|
|
273
|
+
return { ok: true, enclaveIdentity: enclave };
|
|
152
274
|
} catch (e) {
|
|
153
275
|
return { ok: false, error: (e as Error).message };
|
|
154
276
|
}
|
|
@@ -31,7 +31,7 @@ import http from "node:http";
|
|
|
31
31
|
import { Readable } from "node:stream";
|
|
32
32
|
import { SecureClient } from "tinfoil";
|
|
33
33
|
import { serverBaseUrl } from "../auth/privateer.ts";
|
|
34
|
-
import { attestPhala, phalaSealedFetch } from "./phalaSeal.ts";
|
|
34
|
+
import { attestPhala, phalaSealedFetch, type PhalaEnclaveIdentity } from "./phalaSeal.ts";
|
|
35
35
|
import { iterateSSE } from "./phala/sse.ts";
|
|
36
36
|
|
|
37
37
|
// Providers whose enclave supports application-layer body encryption + client-verified
|
|
@@ -109,6 +109,11 @@ export interface SealedAttestation {
|
|
|
109
109
|
ok: boolean;
|
|
110
110
|
enclave?: string;
|
|
111
111
|
error?: string;
|
|
112
|
+
// Phala only: what the verified quote says about the enclave that answered (see
|
|
113
|
+
// phalaSeal.ts). Distinct from `enclave` above, which is Tinfoil's enclave URL —
|
|
114
|
+
// Tinfoil's SecureClient verifies its enclave internally and exposes no equivalent
|
|
115
|
+
// identity, so a green Tinfoil result carries none of this. Absence is not a failure.
|
|
116
|
+
enclaveIdentity?: PhalaEnclaveIdentity;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
// Drive the provider's client-side attestation (the SAME client/keyset the shim
|
|
@@ -20,7 +20,7 @@ import { agentVersion } from "../config/version.ts";
|
|
|
20
20
|
import { createEngineEventAdapter } from "../bridge/engineAdapter.ts";
|
|
21
21
|
import { isRemoteUnsafeTool, type GateController } from "../ext/permissionGate.ts";
|
|
22
22
|
import { moatResourceOptions } from "../config/moat.ts";
|
|
23
|
-
import { rememberAccountCredential, persistAccountCredential, dropPersistedAccountCredential } from "../providers/account.ts";
|
|
23
|
+
import { rememberAccountCredential, persistAccountCredential, dropPersistedAccountCredential, ensureAccountArmed } from "../providers/account.ts";
|
|
24
24
|
// Pi-free static graph by design — see the header of piAuthStore.ts.
|
|
25
25
|
import { modelRegistryOf } from "../providers/piAuthStore.ts";
|
|
26
26
|
import { RelayClient, type TaskSpec } from "./relayClient.ts";
|
|
@@ -130,6 +130,13 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
|
|
|
130
130
|
lastAnswer = ""; // keep the CLOSING answer, not the whole session
|
|
131
131
|
turnErrored = false;
|
|
132
132
|
try {
|
|
133
|
+
// auth.json's `privateer` entry is machine-global and single-slot, so another
|
|
134
|
+
// terminal arming over ours and then exiting deletes the credential this session
|
|
135
|
+
// is running on — every later turn would fail with "This terminal isn't signed in
|
|
136
|
+
// to Privateer" while the session we hold stays valid. Re-arm here rather than in
|
|
137
|
+
// providers/account.ts's `before_agent_start` net, which pi's prompt() throws
|
|
138
|
+
// past on its own `hasConfiguredAuth` precheck.
|
|
139
|
+
if (modelSpec.startsWith("privateer/")) await ensureAccountArmed(undefined);
|
|
133
140
|
// Fold any files the app sent since the last prompt into a reference note, so the
|
|
134
141
|
// model knows they exist and can save_attachment them to disk.
|
|
135
142
|
const atts = sinceLastPrompt;
|
|
@@ -19,6 +19,7 @@ import WebSocket from "ws";
|
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { apiRequest, serverBaseUrl } from "../auth/privateer.ts";
|
|
22
|
+
import { MOAT_SHIMS, reservedNames } from "../config/moatManifest.ts";
|
|
22
23
|
import type { EngineEvent } from "../engine/events.ts";
|
|
23
24
|
import type { PermissionRequest } from "../permissions/gate.ts";
|
|
24
25
|
|
|
@@ -1089,6 +1090,21 @@ export class RelayClient {
|
|
|
1089
1090
|
// each add/remove. `busy` drives a progress indicator; `needsRestart` tells the app
|
|
1090
1091
|
// the change only takes effect on the next terminal launch. NON-PII: package
|
|
1091
1092
|
// sources + scope only. Installed list bounded like sendCommands.
|
|
1093
|
+
//
|
|
1094
|
+
// `builtIn` + `managed` describe the MOAT, which is deliberately absent from
|
|
1095
|
+
// `installed` (it never enters settings "packages"). Without them the app's Browse
|
|
1096
|
+
// tab — an unfiltered npm `keywords:pi-package` search — offers Add on packages we
|
|
1097
|
+
// already ship: pi-mcp-adapter, pi-subagents and the rpiv packs are on its first
|
|
1098
|
+
// page. Every one of those round-trips to extensionsControl.add() only to come back
|
|
1099
|
+
// refused ("managed by Privateer"), and until it does the row reads as *not*
|
|
1100
|
+
// installed, which is the opposite of the truth. So:
|
|
1101
|
+
// • `builtIn` — what we ship, in load order, for a read-only section.
|
|
1102
|
+
// • `managed` — every name add()/remove() will refuse (builtIn + retired + the
|
|
1103
|
+
// scoped aliases), so the app can drop the Add affordance instead of offering
|
|
1104
|
+
// a button that always fails.
|
|
1105
|
+
// Stamped here rather than passed by the caller: it is a constant of the build, and
|
|
1106
|
+
// six call sites that each had to remember it is the drift moatManifest.json exists
|
|
1107
|
+
// to make unrepresentable. An older app ignores both fields.
|
|
1092
1108
|
sendExtensions(payload: {
|
|
1093
1109
|
installed: { source: string; scope: string; filtered?: boolean; installed?: boolean }[];
|
|
1094
1110
|
busy?: boolean;
|
|
@@ -1103,6 +1119,11 @@ export class RelayClient {
|
|
|
1103
1119
|
filtered: !!e.filtered,
|
|
1104
1120
|
installed: !!e.installed,
|
|
1105
1121
|
})),
|
|
1122
|
+
builtIn: MOAT_SHIMS.slice(0, 100).map((s) => ({
|
|
1123
|
+
name: safe(s.name, 120),
|
|
1124
|
+
note: s.note ? safe(s.note, 200) : undefined,
|
|
1125
|
+
})),
|
|
1126
|
+
managed: reservedNames().slice(0, 200).map((n) => safe(n, 120)),
|
|
1106
1127
|
busy: !!payload.busy,
|
|
1107
1128
|
message: payload.message ? safe(payload.message, 500) : undefined,
|
|
1108
1129
|
needsRestart: !!payload.needsRestart,
|
|
@@ -42,8 +42,17 @@ export interface RelayLike {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// The installed-extensions snapshot relayed to the app's extensions manager.
|
|
45
|
+
//
|
|
46
|
+
// `managed`/`builtIn` describe the moat, which `installed` never contains (it is loaded
|
|
47
|
+
// as launch shims, not settings "packages"). Callers do NOT set them: each transport
|
|
48
|
+
// stamps them on the way out — RelayClient.sendExtensions from the shipping manifest,
|
|
49
|
+
// IpcRelay from reservedNames() — because a constant of the build passed by hand at six
|
|
50
|
+
// call sites is the drift moatManifest.json exists to make unrepresentable. Declared
|
|
51
|
+
// here only so a transport can type what it adds.
|
|
45
52
|
export interface ExtensionsPayload {
|
|
46
53
|
installed: { source: string; scope: string; filtered?: boolean; installed?: boolean }[];
|
|
54
|
+
builtIn?: { name: string; note?: string }[];
|
|
55
|
+
managed?: string[];
|
|
47
56
|
busy?: boolean;
|
|
48
57
|
message?: string;
|
|
49
58
|
needsRestart?: boolean;
|
package/src/routines/store.ts
CHANGED
|
@@ -230,6 +230,17 @@ export interface PendingCloud {
|
|
|
230
230
|
// instead. Copying megabytes into this queue would be the same content twice on
|
|
231
231
|
// the one box that already has it.
|
|
232
232
|
media?: StagedMedia[];
|
|
233
|
+
// What the run was asked to do (outbox/cloudOutbox.ts OutboxSource), so a flush
|
|
234
|
+
// hours later still seals the context a follow-up needs. Structurally typed rather
|
|
235
|
+
// than imported: this module is the queue's shape, and cloudOutbox already imports
|
|
236
|
+
// from here (importing back would make the cycle).
|
|
237
|
+
source?: {
|
|
238
|
+
routineId?: string;
|
|
239
|
+
prompt?: string;
|
|
240
|
+
cwd?: string;
|
|
241
|
+
model?: string;
|
|
242
|
+
schedule?: string;
|
|
243
|
+
};
|
|
233
244
|
}
|
|
234
245
|
|
|
235
246
|
function pendingCloudPath(): string {
|