myagentmemory 0.4.13 → 0.4.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.
@@ -0,0 +1,537 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomBytes, randomUUID } from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
5
+ import * as path from "node:path";
6
+
7
+ import {
8
+ getDefaultPluginInstallRoot,
9
+ type PluginAccessDecisionV1,
10
+ type PluginBootstrapBackendV1,
11
+ PluginBootstrapFailure,
12
+ type PluginNextActionV1,
13
+ type PluginSessionUsageDecisionV1,
14
+ type SignedPluginReleaseV1,
15
+ } from "./plugin-bootstrap.js";
16
+ import { type PluginEntitlementStatusV1, validatePluginEntitlementStatusV1 } from "./plugin-host.js";
17
+
18
+ const API_ORIGIN = "https://api.agentmemory.paperpilot.me";
19
+ const ARTIFACT_ORIGIN = "https://plugins.agentmemory.paperpilot.me";
20
+ const ACTIVATION_FILE = "credentials/temporary-access.json";
21
+ const REQUEST_TIMEOUT_MS = 30_000;
22
+ const EMAIL_MAX_BYTES = 254;
23
+ const FORM_MAX_BYTES = 2_048;
24
+ const SERVICE_JSON_MAX_BYTES = 1024 * 1024;
25
+ const ACTIVATION_CREDENTIAL = /^am_activation_[A-Za-z0-9_-]{32,256}$/;
26
+
27
+ const MISSING_ENTITLEMENT: PluginEntitlementStatusV1 = {
28
+ plan: null,
29
+ state: "missing",
30
+ features: [],
31
+ capabilities: {},
32
+ reason: "Enter an email address to activate the free daily session allowance",
33
+ };
34
+
35
+ interface TemporaryActivationV1 {
36
+ schemaVersion: 2;
37
+ email: string;
38
+ activatedAt: string;
39
+ usageCredential: string;
40
+ dailySessionLimit: number;
41
+ }
42
+
43
+ interface TemporaryPluginBackendOptions {
44
+ root?: string;
45
+ coreVersion?: string;
46
+ apiOrigin?: string;
47
+ artifactOrigin?: string;
48
+ fetch?: typeof globalThis.fetch;
49
+ openUrl?: (url: string) => boolean;
50
+ activate?: () => Promise<string>;
51
+ }
52
+
53
+ function cloneEntitlement(value: PluginEntitlementStatusV1): PluginEntitlementStatusV1 {
54
+ return structuredClone(value);
55
+ }
56
+
57
+ function freeEntitlement(dailySessionLimit: number): PluginEntitlementStatusV1 {
58
+ return {
59
+ plan: "free",
60
+ state: "active",
61
+ features: ["session-intelligence", "web-console"],
62
+ capabilities: {
63
+ "session-index": { enabled: true },
64
+ "session-worker": {
65
+ enabled: true,
66
+ quota: { limit: dailySessionLimit, window: "day", scope: "account" },
67
+ },
68
+ learning: { enabled: true },
69
+ "retrieval-evaluation": { enabled: true },
70
+ "operational-metrics": { enabled: true },
71
+ "web-console": { enabled: true },
72
+ "memory-explorer": { enabled: true },
73
+ },
74
+ reason: `${dailySessionLimit} free agent sessions per UTC day`,
75
+ };
76
+ }
77
+
78
+ function isEmail(value: string): boolean {
79
+ return (
80
+ Buffer.byteLength(value, "utf-8") <= EMAIL_MAX_BYTES &&
81
+ [...value].every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127) &&
82
+ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
83
+ );
84
+ }
85
+
86
+ function securityHeaders(contentType: string): Record<string, string> {
87
+ return {
88
+ "Cache-Control": "no-store",
89
+ "Content-Security-Policy":
90
+ "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
91
+ "Content-Type": contentType,
92
+ "Referrer-Policy": "same-origin",
93
+ "X-Content-Type-Options": "nosniff",
94
+ };
95
+ }
96
+
97
+ function activationPage(action: string, error?: string): string {
98
+ const errorHtml = error ? `<p class="error">${error}</p>` : "";
99
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Activate AgentMemory</title><style>body{font:16px system-ui;max-width:34rem;margin:10vh auto;padding:0 1.5rem;color:#18212b}form{display:grid;gap:1rem}input,button{font:inherit;padding:.8rem;border-radius:.5rem;border:1px solid #aab4bf}button{background:#18212b;color:#fff;cursor:pointer}.muted{color:#586574}.error{color:#a21d24}</style></head><body><h1>Activate AgentMemory</h1><p>Enter an email address to enable the free daily agent-session allowance on this device.</p>${errorHtml}<form method="post" action="${action}"><label>Email <input type="email" name="email" autocomplete="email" maxlength="254" required autofocus></label><button type="submit">Activate and return to terminal</button></form><p class="muted">The AgentMemory CLI sends your email plus core, bundle, platform, architecture, and release-channel metadata to the private activation service. D1 stores a daily count of opaque SessionStart operations for your normalized email. The request never includes memory, session content, queries, repository paths, raw agent session identifiers, IP addresses, or user-agent strings. Activation records expire after 365 days without use.</p></body></html>`;
100
+ }
101
+
102
+ function completionPage(): string {
103
+ return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>AgentMemory activated</title></head><body><h1>Activation complete</h1><p>You can close this tab and return to the terminal.</p></body></html>';
104
+ }
105
+
106
+ function send(response: ServerResponse, status: number, body: string, contentType = "text/html; charset=utf-8"): void {
107
+ response.writeHead(status, securityHeaders(contentType));
108
+ response.end(body);
109
+ }
110
+
111
+ async function readBoundedBody(response: Response, maxBytes: number): Promise<Uint8Array> {
112
+ if (!response.body) return new Uint8Array();
113
+ const reader = response.body.getReader();
114
+ const chunks: Uint8Array[] = [];
115
+ let size = 0;
116
+ while (true) {
117
+ const { done, value } = await reader.read();
118
+ if (done) break;
119
+ size += value.byteLength;
120
+ if (size > maxBytes) {
121
+ await reader.cancel();
122
+ throw new PluginBootstrapFailure("service_response_too_large", "The plugin service response is too large");
123
+ }
124
+ chunks.push(value);
125
+ }
126
+ const output = new Uint8Array(size);
127
+ let offset = 0;
128
+ for (const chunk of chunks) {
129
+ output.set(chunk, offset);
130
+ offset += chunk.byteLength;
131
+ }
132
+ return output;
133
+ }
134
+
135
+ async function readJson(response: Response): Promise<unknown> {
136
+ try {
137
+ return JSON.parse(new TextDecoder().decode(await readBoundedBody(response, SERVICE_JSON_MAX_BYTES)));
138
+ } catch (error) {
139
+ if (error instanceof PluginBootstrapFailure) throw error;
140
+ throw new PluginBootstrapFailure("service_response_invalid", "The plugin service returned invalid JSON");
141
+ }
142
+ }
143
+
144
+ function readForm(request: IncomingMessage): Promise<URLSearchParams> {
145
+ return new Promise((resolve, reject) => {
146
+ let size = 0;
147
+ const chunks: Buffer[] = [];
148
+ request.on("data", (chunk: Buffer) => {
149
+ size += chunk.byteLength;
150
+ if (size > FORM_MAX_BYTES) {
151
+ reject(new Error("form too large"));
152
+ request.destroy();
153
+ return;
154
+ }
155
+ chunks.push(chunk);
156
+ });
157
+ request.on("end", () => resolve(new URLSearchParams(Buffer.concat(chunks).toString("utf-8"))));
158
+ request.on("error", reject);
159
+ });
160
+ }
161
+
162
+ function isSameOriginActivationPost(request: IncomingMessage, expectedHost: string, activationPath: string): boolean {
163
+ const expectedOrigin = `http://${expectedHost}`;
164
+ const origin = request.headers.origin;
165
+ const fetchSite = request.headers["sec-fetch-site"];
166
+ if (origin === "null") {
167
+ return (
168
+ fetchSite === "same-origin" &&
169
+ request.headers["sec-fetch-mode"] === "navigate" &&
170
+ request.headers["sec-fetch-dest"] === "document" &&
171
+ request.headers["sec-fetch-user"] === "?1"
172
+ );
173
+ }
174
+ if (origin !== undefined) return origin === expectedOrigin;
175
+
176
+ if (fetchSite !== undefined && fetchSite !== "same-origin") return false;
177
+
178
+ const referer = request.headers.referer;
179
+ if (referer !== undefined) {
180
+ try {
181
+ const parsed = new URL(referer);
182
+ return parsed.origin === expectedOrigin && parsed.pathname === activationPath;
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
187
+
188
+ // Privacy-focused and older browsers may omit all three headers. The exact
189
+ // loopback Host plus the 192-bit, single-use path remains the CSRF capability.
190
+ return true;
191
+ }
192
+
193
+ export function openLoopbackUrl(url: string): boolean {
194
+ const parsed = new URL(url);
195
+ if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1") return false;
196
+ try {
197
+ const child =
198
+ process.platform === "darwin"
199
+ ? spawn("open", [parsed.toString()], { detached: true, stdio: "ignore" })
200
+ : process.platform === "win32"
201
+ ? spawn("explorer.exe", [parsed.toString()], { detached: true, stdio: "ignore" })
202
+ : spawn("xdg-open", [parsed.toString()], { detached: true, stdio: "ignore" });
203
+ child.unref();
204
+ return true;
205
+ } catch {
206
+ return false;
207
+ }
208
+ }
209
+
210
+ export async function collectTemporaryActivation(openUrl: (url: string) => boolean = openLoopbackUrl): Promise<string> {
211
+ const nonce = randomBytes(24).toString("hex");
212
+ const activationPath = `/activate/${nonce}`;
213
+ let expectedHost = "";
214
+ let settle: ((email: string) => void) | null = null;
215
+ let fail: ((error: Error) => void) | null = null;
216
+ const result = new Promise<string>((resolve, reject) => {
217
+ settle = resolve;
218
+ fail = reject;
219
+ });
220
+ const server = createServer(async (request, response) => {
221
+ if (request.headers.host !== expectedHost || request.url !== activationPath) {
222
+ send(response, 404, "Not found", "text/plain; charset=utf-8");
223
+ return;
224
+ }
225
+ if (request.method === "GET") {
226
+ send(response, 200, activationPage(activationPath));
227
+ return;
228
+ }
229
+ if (request.method !== "POST" || !isSameOriginActivationPost(request, expectedHost, activationPath)) {
230
+ send(response, 403, "Forbidden", "text/plain; charset=utf-8");
231
+ return;
232
+ }
233
+ try {
234
+ const email = (await readForm(request)).get("email")?.trim() ?? "";
235
+ if (!isEmail(email)) {
236
+ send(response, 400, activationPage(activationPath, "Enter a valid email address."));
237
+ return;
238
+ }
239
+ send(response, 200, completionPage());
240
+ settle?.(email);
241
+ settle = null;
242
+ server.close();
243
+ } catch {
244
+ send(response, 400, activationPage(activationPath, "The activation form could not be read."));
245
+ }
246
+ });
247
+ server.requestTimeout = 10_000;
248
+ server.headersTimeout = 10_000;
249
+ server.maxHeadersCount = 40;
250
+ await new Promise<void>((resolve, reject) => {
251
+ server.once("error", reject);
252
+ server.listen(0, "127.0.0.1", resolve);
253
+ });
254
+ const address = server.address();
255
+ if (!address || typeof address === "string")
256
+ throw new PluginBootstrapFailure("activation_failed", "Activation did not bind to loopback");
257
+ expectedHost = `127.0.0.1:${address.port}`;
258
+ const url = `http://${expectedHost}${activationPath}`;
259
+ console.log(`AgentMemory activation: ${url}`);
260
+ let opened = false;
261
+ try {
262
+ opened = openUrl(url);
263
+ } catch {
264
+ server.close();
265
+ throw new PluginBootstrapFailure("browser_unavailable", `Open this URL in a browser: ${url}`);
266
+ }
267
+ if (!opened) {
268
+ server.close();
269
+ throw new PluginBootstrapFailure("browser_unavailable", `Open this URL in a browser: ${url}`);
270
+ }
271
+ const timer = setTimeout(() => {
272
+ fail?.(new PluginBootstrapFailure("activation_timeout", "Temporary activation timed out", true));
273
+ server.close();
274
+ }, 5 * 60_000);
275
+ timer.unref();
276
+ try {
277
+ return await result;
278
+ } finally {
279
+ clearTimeout(timer);
280
+ server.close();
281
+ }
282
+ }
283
+
284
+ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
285
+ private readonly root: string;
286
+ private readonly coreVersion: string;
287
+ private readonly apiOrigin: string;
288
+ private readonly artifactOrigin: string;
289
+ private readonly fetchImplementation: typeof globalThis.fetch;
290
+ private readonly openUrl: (url: string) => boolean;
291
+ private readonly activate: () => Promise<string>;
292
+
293
+ constructor(options: TemporaryPluginBackendOptions = {}) {
294
+ this.root = path.resolve(options.root ?? getDefaultPluginInstallRoot());
295
+ this.coreVersion = options.coreVersion ?? "0.0.0";
296
+ this.apiOrigin = options.apiOrigin ?? API_ORIGIN;
297
+ this.artifactOrigin = options.artifactOrigin ?? ARTIFACT_ORIGIN;
298
+ this.fetchImplementation = options.fetch ?? globalThis.fetch;
299
+ this.openUrl = options.openUrl ?? openLoopbackUrl;
300
+ this.activate = options.activate ?? (() => collectTemporaryActivation(this.openUrl));
301
+ }
302
+
303
+ async getLocalEntitlement(): Promise<PluginEntitlementStatusV1> {
304
+ const activation = this.readActivation();
305
+ return activation ? freeEntitlement(activation.dailySessionLimit) : cloneEntitlement(MISSING_ENTITLEMENT);
306
+ }
307
+
308
+ async resolveAccess(request: {
309
+ bundleId: string;
310
+ installedVersion?: string;
311
+ channel: string;
312
+ allowAuthentication: boolean;
313
+ }): Promise<PluginAccessDecisionV1> {
314
+ const activation = this.readActivation();
315
+ let email = activation?.email;
316
+ if (!email) {
317
+ if (!request.allowAuthentication)
318
+ return {
319
+ kind: "auth_required",
320
+ entitlement: cloneEntitlement(MISSING_ENTITLEMENT),
321
+ nextAction: {
322
+ kind: "authenticate",
323
+ url: "https://jayzeng.github.io/agentmemory/",
324
+ message: "Run plugin install in an interactive terminal to enter an email address",
325
+ },
326
+ };
327
+ email = await this.activate();
328
+ }
329
+ const response = await this.request(`${this.apiOrigin}/v1/plugin/access`, {
330
+ method: "POST",
331
+ headers: { "Content-Type": "application/json" },
332
+ body: JSON.stringify({
333
+ schemaVersion: 1,
334
+ email,
335
+ bundleId: request.bundleId,
336
+ installedVersion: request.installedVersion ?? null,
337
+ coreVersion: this.coreVersion,
338
+ channel: request.channel,
339
+ platform: process.platform,
340
+ architecture: process.arch,
341
+ consentVersion: "activation-v2",
342
+ }),
343
+ });
344
+ const value = (await readJson(response)) as {
345
+ entitlement?: unknown;
346
+ artifactGrant?: unknown;
347
+ usageCredential?: unknown;
348
+ };
349
+ validatePluginEntitlementStatusV1(value.entitlement);
350
+ if (typeof value.artifactGrant !== "string" || !value.artifactGrant)
351
+ throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its artifact grant");
352
+ if (typeof value.usageCredential !== "string" || !ACTIVATION_CREDENTIAL.test(value.usageCredential))
353
+ throw new PluginBootstrapFailure(
354
+ "service_response_invalid",
355
+ "The access response omitted its usage credential",
356
+ );
357
+ const freeQuota = value.entitlement.capabilities["session-worker"]?.quota;
358
+ if (
359
+ value.entitlement.plan !== "free" ||
360
+ value.entitlement.state !== "active" ||
361
+ !freeQuota ||
362
+ freeQuota.scope !== "account" ||
363
+ freeQuota.window !== "day"
364
+ )
365
+ throw new PluginBootstrapFailure("service_response_invalid", "The free session policy is invalid");
366
+ this.writeActivation(email, value.usageCredential, freeQuota.limit);
367
+ return { kind: "granted", entitlement: value.entitlement, artifactGrant: value.artifactGrant };
368
+ }
369
+
370
+ async reserveSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
371
+ return this.sessionUsage("reserve", operationId);
372
+ }
373
+
374
+ async commitSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
375
+ return this.sessionUsage("commit", operationId);
376
+ }
377
+
378
+ async releaseSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
379
+ return this.sessionUsage("release", operationId);
380
+ }
381
+
382
+ async listReleases(request: {
383
+ bundleId: string;
384
+ channel: string;
385
+ artifactGrant: string;
386
+ }): Promise<SignedPluginReleaseV1[]> {
387
+ const response = await this.request(`${this.apiOrigin}/v1/plugin/releases`, {
388
+ headers: { Authorization: `Bearer ${request.artifactGrant}` },
389
+ });
390
+ const value = (await readJson(response)) as { releases?: unknown };
391
+ if (!Array.isArray(value.releases))
392
+ throw new PluginBootstrapFailure("service_response_invalid", "The release response is invalid");
393
+ return value.releases as SignedPluginReleaseV1[];
394
+ }
395
+
396
+ async downloadArtifact(request: { release: SignedPluginReleaseV1; artifactGrant: string }): Promise<Uint8Array> {
397
+ const response = await this.request(`${this.artifactOrigin}/v1/artifacts/download`, {
398
+ headers: { Authorization: `Bearer ${request.artifactGrant}` },
399
+ });
400
+ const declared = Number(response.headers.get("Content-Length"));
401
+ if (Number.isFinite(declared) && declared !== request.release.size)
402
+ throw new PluginBootstrapFailure("artifact_size_mismatch", "The artifact response size is invalid");
403
+ const artifact = await readBoundedBody(response, request.release.size);
404
+ if (artifact.byteLength !== request.release.size)
405
+ throw new PluginBootstrapFailure("artifact_size_mismatch", "The artifact response size is invalid");
406
+ return artifact;
407
+ }
408
+
409
+ async getManagementAction(): Promise<PluginNextActionV1 | null> {
410
+ return null;
411
+ }
412
+
413
+ private activationPath(): string {
414
+ return path.join(this.root, ...ACTIVATION_FILE.split("/"));
415
+ }
416
+
417
+ private readActivation(): TemporaryActivationV1 | null {
418
+ const activationPath = this.activationPath();
419
+ if (!fs.existsSync(activationPath)) return null;
420
+ try {
421
+ const rootStat = fs.lstatSync(this.root);
422
+ const credentialsStat = fs.lstatSync(path.dirname(activationPath));
423
+ if (
424
+ !rootStat.isDirectory() ||
425
+ rootStat.isSymbolicLink() ||
426
+ !credentialsStat.isDirectory() ||
427
+ credentialsStat.isSymbolicLink()
428
+ )
429
+ return null;
430
+ const stat = fs.lstatSync(activationPath);
431
+ if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
432
+ return null;
433
+ const value = JSON.parse(fs.readFileSync(activationPath, "utf-8")) as TemporaryActivationV1;
434
+ if (
435
+ value.schemaVersion !== 2 ||
436
+ !isEmail(value.email) ||
437
+ !Number.isFinite(Date.parse(value.activatedAt)) ||
438
+ !ACTIVATION_CREDENTIAL.test(value.usageCredential) ||
439
+ !Number.isSafeInteger(value.dailySessionLimit) ||
440
+ value.dailySessionLimit <= 0 ||
441
+ value.dailySessionLimit > 10_000
442
+ )
443
+ return null;
444
+ return value;
445
+ } catch {
446
+ return null;
447
+ }
448
+ }
449
+
450
+ private writeActivation(email: string, usageCredential: string, dailySessionLimit: number): void {
451
+ if (!isEmail(email)) throw new PluginBootstrapFailure("email_invalid", "Enter a valid email address");
452
+ if (!ACTIVATION_CREDENTIAL.test(usageCredential))
453
+ throw new PluginBootstrapFailure("activation_failed", "The activation credential is invalid");
454
+ if (!Number.isSafeInteger(dailySessionLimit) || dailySessionLimit <= 0 || dailySessionLimit > 10_000)
455
+ throw new PluginBootstrapFailure("activation_failed", "The free session allowance is invalid");
456
+ const target = this.activationPath();
457
+ fs.mkdirSync(this.root, { recursive: true, mode: 0o700 });
458
+ const rootStat = fs.lstatSync(this.root);
459
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
460
+ throw new PluginBootstrapFailure("activation_path_invalid", "The plugin activation root is unsafe");
461
+ const directory = path.dirname(target);
462
+ if (!fs.existsSync(directory)) fs.mkdirSync(directory, { mode: 0o700 });
463
+ const directoryStat = fs.lstatSync(directory);
464
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink())
465
+ throw new PluginBootstrapFailure("activation_path_invalid", "The plugin activation directory is unsafe");
466
+ const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
467
+ fs.writeFileSync(
468
+ temporary,
469
+ `${JSON.stringify(
470
+ { schemaVersion: 2, email, activatedAt: new Date().toISOString(), usageCredential, dailySessionLimit },
471
+ null,
472
+ 2,
473
+ )}\n`,
474
+ { mode: 0o600, flag: "wx" },
475
+ );
476
+ fs.renameSync(temporary, target);
477
+ }
478
+
479
+ private async sessionUsage(
480
+ action: "reserve" | "commit" | "release",
481
+ operationId: string,
482
+ ): Promise<PluginSessionUsageDecisionV1> {
483
+ const activation = this.readActivation();
484
+ if (!activation) throw new PluginBootstrapFailure("auth_required", "Run plugin install to activate AgentMemory");
485
+ const response = await this.request(`${this.apiOrigin}/v1/plugin/sessions/${action}`, {
486
+ method: "POST",
487
+ headers: {
488
+ Authorization: `Bearer ${activation.usageCredential}`,
489
+ "Content-Type": "application/json",
490
+ },
491
+ body: JSON.stringify({ schemaVersion: 1, operationId }),
492
+ });
493
+ const value = (await readJson(response)) as { decision?: Partial<PluginSessionUsageDecisionV1> };
494
+ const decision = value.decision;
495
+ if (
496
+ !decision ||
497
+ typeof decision.allowed !== "boolean" ||
498
+ !["reserved", "committed", "released", "exhausted", "missing"].includes(String(decision.state)) ||
499
+ !Number.isSafeInteger(decision.limit) ||
500
+ Number(decision.limit) <= 0 ||
501
+ !Number.isSafeInteger(decision.used) ||
502
+ Number(decision.used) < 0 ||
503
+ !Number.isSafeInteger(decision.remaining) ||
504
+ Number(decision.remaining) < 0 ||
505
+ typeof decision.resetAt !== "string" ||
506
+ !Number.isFinite(Date.parse(decision.resetAt)) ||
507
+ typeof decision.idempotent !== "boolean"
508
+ )
509
+ throw new PluginBootstrapFailure("service_response_invalid", "The session usage response is invalid");
510
+ return decision as PluginSessionUsageDecisionV1;
511
+ }
512
+
513
+ private async request(url: string, init: RequestInit = {}): Promise<Response> {
514
+ let response: Response;
515
+ try {
516
+ response = await this.fetchImplementation(url, {
517
+ ...init,
518
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
519
+ headers: { Accept: "application/json", ...init.headers },
520
+ });
521
+ } catch {
522
+ throw new PluginBootstrapFailure("service_unavailable", "The AgentMemory plugin service is unavailable", true);
523
+ }
524
+ if (!response.ok) {
525
+ let message = `The AgentMemory plugin service returned HTTP ${response.status}`;
526
+ try {
527
+ const value = (await readJson(response)) as { error?: { message?: unknown } };
528
+ if (typeof value.error?.message === "string") message = value.error.message;
529
+ } catch (error) {
530
+ if (error instanceof PluginBootstrapFailure && error.code === "service_response_too_large") throw error;
531
+ // Keep the bounded generic response.
532
+ }
533
+ throw new PluginBootstrapFailure("service_request_failed", message, response.status >= 500);
534
+ }
535
+ return response;
536
+ }
537
+ }