myagentmemory 0.4.12 → 0.4.14

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,395 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomBytes, randomUUID } from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import { createServer } from "node:http";
5
+ import * as path from "node:path";
6
+ import { getDefaultPluginInstallRoot, PluginBootstrapFailure, } from "./plugin-bootstrap.js";
7
+ import { validatePluginEntitlementStatusV1 } from "./plugin-host.js";
8
+ const API_ORIGIN = "https://api.agentmemory.paperpilot.me";
9
+ const ARTIFACT_ORIGIN = "https://plugins.agentmemory.paperpilot.me";
10
+ const ACTIVATION_FILE = "credentials/temporary-access.json";
11
+ const REQUEST_TIMEOUT_MS = 30_000;
12
+ const EMAIL_MAX_BYTES = 254;
13
+ const FORM_MAX_BYTES = 2_048;
14
+ const SERVICE_JSON_MAX_BYTES = 1024 * 1024;
15
+ const MISSING_ENTITLEMENT = {
16
+ plan: null,
17
+ state: "missing",
18
+ features: [],
19
+ capabilities: {},
20
+ reason: "Enter an email address to activate temporary unlimited local use",
21
+ };
22
+ const TEMPORARY_ENTITLEMENT = {
23
+ plan: "pro",
24
+ state: "active",
25
+ features: ["session-intelligence", "web-console"],
26
+ capabilities: Object.fromEntries([
27
+ "session-index",
28
+ "session-worker",
29
+ "learning",
30
+ "retrieval-evaluation",
31
+ "operational-metrics",
32
+ "web-console",
33
+ "memory-explorer",
34
+ ].map((capability) => [capability, { enabled: true }])),
35
+ reason: "Temporary email activation grants unlimited local use",
36
+ };
37
+ function cloneEntitlement(value) {
38
+ return structuredClone(value);
39
+ }
40
+ function isEmail(value) {
41
+ return (Buffer.byteLength(value, "utf-8") <= EMAIL_MAX_BYTES &&
42
+ [...value].every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127) &&
43
+ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value));
44
+ }
45
+ function securityHeaders(contentType) {
46
+ return {
47
+ "Cache-Control": "no-store",
48
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
49
+ "Content-Type": contentType,
50
+ "Referrer-Policy": "same-origin",
51
+ "X-Content-Type-Options": "nosniff",
52
+ };
53
+ }
54
+ function activationPage(action, error) {
55
+ const errorHtml = error ? `<p class="error">${error}</p>` : "";
56
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Activate AgentMemory Pro</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 Pro</h1><p>Enter an email address to enable temporary unlimited use 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. The activation record never includes memory, session content, queries, repository paths, IP addresses, or user-agent strings, and expires after 365 days without activation.</p></body></html>`;
57
+ }
58
+ function completionPage() {
59
+ 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>';
60
+ }
61
+ function send(response, status, body, contentType = "text/html; charset=utf-8") {
62
+ response.writeHead(status, securityHeaders(contentType));
63
+ response.end(body);
64
+ }
65
+ async function readBoundedBody(response, maxBytes) {
66
+ if (!response.body)
67
+ return new Uint8Array();
68
+ const reader = response.body.getReader();
69
+ const chunks = [];
70
+ let size = 0;
71
+ while (true) {
72
+ const { done, value } = await reader.read();
73
+ if (done)
74
+ break;
75
+ size += value.byteLength;
76
+ if (size > maxBytes) {
77
+ await reader.cancel();
78
+ throw new PluginBootstrapFailure("service_response_too_large", "The plugin service response is too large");
79
+ }
80
+ chunks.push(value);
81
+ }
82
+ const output = new Uint8Array(size);
83
+ let offset = 0;
84
+ for (const chunk of chunks) {
85
+ output.set(chunk, offset);
86
+ offset += chunk.byteLength;
87
+ }
88
+ return output;
89
+ }
90
+ async function readJson(response) {
91
+ try {
92
+ return JSON.parse(new TextDecoder().decode(await readBoundedBody(response, SERVICE_JSON_MAX_BYTES)));
93
+ }
94
+ catch (error) {
95
+ if (error instanceof PluginBootstrapFailure)
96
+ throw error;
97
+ throw new PluginBootstrapFailure("service_response_invalid", "The plugin service returned invalid JSON");
98
+ }
99
+ }
100
+ function readForm(request) {
101
+ return new Promise((resolve, reject) => {
102
+ let size = 0;
103
+ const chunks = [];
104
+ request.on("data", (chunk) => {
105
+ size += chunk.byteLength;
106
+ if (size > FORM_MAX_BYTES) {
107
+ reject(new Error("form too large"));
108
+ request.destroy();
109
+ return;
110
+ }
111
+ chunks.push(chunk);
112
+ });
113
+ request.on("end", () => resolve(new URLSearchParams(Buffer.concat(chunks).toString("utf-8"))));
114
+ request.on("error", reject);
115
+ });
116
+ }
117
+ function isSameOriginActivationPost(request, expectedHost, activationPath) {
118
+ const expectedOrigin = `http://${expectedHost}`;
119
+ const origin = request.headers.origin;
120
+ const fetchSite = request.headers["sec-fetch-site"];
121
+ if (origin === "null") {
122
+ return (fetchSite === "same-origin" &&
123
+ request.headers["sec-fetch-mode"] === "navigate" &&
124
+ request.headers["sec-fetch-dest"] === "document" &&
125
+ request.headers["sec-fetch-user"] === "?1");
126
+ }
127
+ if (origin !== undefined)
128
+ return origin === expectedOrigin;
129
+ if (fetchSite !== undefined && fetchSite !== "same-origin")
130
+ return false;
131
+ const referer = request.headers.referer;
132
+ if (referer !== undefined) {
133
+ try {
134
+ const parsed = new URL(referer);
135
+ return parsed.origin === expectedOrigin && parsed.pathname === activationPath;
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
141
+ // Privacy-focused and older browsers may omit all three headers. The exact
142
+ // loopback Host plus the 192-bit, single-use path remains the CSRF capability.
143
+ return true;
144
+ }
145
+ export function openLoopbackUrl(url) {
146
+ const parsed = new URL(url);
147
+ if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1")
148
+ return false;
149
+ try {
150
+ const child = process.platform === "darwin"
151
+ ? spawn("open", [parsed.toString()], { detached: true, stdio: "ignore" })
152
+ : process.platform === "win32"
153
+ ? spawn("explorer.exe", [parsed.toString()], { detached: true, stdio: "ignore" })
154
+ : spawn("xdg-open", [parsed.toString()], { detached: true, stdio: "ignore" });
155
+ child.unref();
156
+ return true;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ }
162
+ export async function collectTemporaryActivation(openUrl = openLoopbackUrl) {
163
+ const nonce = randomBytes(24).toString("hex");
164
+ const activationPath = `/activate/${nonce}`;
165
+ let expectedHost = "";
166
+ let settle = null;
167
+ let fail = null;
168
+ const result = new Promise((resolve, reject) => {
169
+ settle = resolve;
170
+ fail = reject;
171
+ });
172
+ const server = createServer(async (request, response) => {
173
+ if (request.headers.host !== expectedHost || request.url !== activationPath) {
174
+ send(response, 404, "Not found", "text/plain; charset=utf-8");
175
+ return;
176
+ }
177
+ if (request.method === "GET") {
178
+ send(response, 200, activationPage(activationPath));
179
+ return;
180
+ }
181
+ if (request.method !== "POST" || !isSameOriginActivationPost(request, expectedHost, activationPath)) {
182
+ send(response, 403, "Forbidden", "text/plain; charset=utf-8");
183
+ return;
184
+ }
185
+ try {
186
+ const email = (await readForm(request)).get("email")?.trim() ?? "";
187
+ if (!isEmail(email)) {
188
+ send(response, 400, activationPage(activationPath, "Enter a valid email address."));
189
+ return;
190
+ }
191
+ send(response, 200, completionPage());
192
+ settle?.(email);
193
+ settle = null;
194
+ server.close();
195
+ }
196
+ catch {
197
+ send(response, 400, activationPage(activationPath, "The activation form could not be read."));
198
+ }
199
+ });
200
+ server.requestTimeout = 10_000;
201
+ server.headersTimeout = 10_000;
202
+ server.maxHeadersCount = 40;
203
+ await new Promise((resolve, reject) => {
204
+ server.once("error", reject);
205
+ server.listen(0, "127.0.0.1", resolve);
206
+ });
207
+ const address = server.address();
208
+ if (!address || typeof address === "string")
209
+ throw new PluginBootstrapFailure("activation_failed", "Activation did not bind to loopback");
210
+ expectedHost = `127.0.0.1:${address.port}`;
211
+ const url = `http://${expectedHost}${activationPath}`;
212
+ console.log(`AgentMemory activation: ${url}`);
213
+ let opened = false;
214
+ try {
215
+ opened = openUrl(url);
216
+ }
217
+ catch {
218
+ server.close();
219
+ throw new PluginBootstrapFailure("browser_unavailable", `Open this URL in a browser: ${url}`);
220
+ }
221
+ if (!opened) {
222
+ server.close();
223
+ throw new PluginBootstrapFailure("browser_unavailable", `Open this URL in a browser: ${url}`);
224
+ }
225
+ const timer = setTimeout(() => {
226
+ fail?.(new PluginBootstrapFailure("activation_timeout", "Temporary activation timed out", true));
227
+ server.close();
228
+ }, 5 * 60_000);
229
+ timer.unref();
230
+ try {
231
+ return await result;
232
+ }
233
+ finally {
234
+ clearTimeout(timer);
235
+ server.close();
236
+ }
237
+ }
238
+ export class TemporaryPluginBackend {
239
+ root;
240
+ coreVersion;
241
+ apiOrigin;
242
+ artifactOrigin;
243
+ fetchImplementation;
244
+ openUrl;
245
+ activate;
246
+ constructor(options = {}) {
247
+ this.root = path.resolve(options.root ?? getDefaultPluginInstallRoot());
248
+ this.coreVersion = options.coreVersion ?? "0.0.0";
249
+ this.apiOrigin = options.apiOrigin ?? API_ORIGIN;
250
+ this.artifactOrigin = options.artifactOrigin ?? ARTIFACT_ORIGIN;
251
+ this.fetchImplementation = options.fetch ?? globalThis.fetch;
252
+ this.openUrl = options.openUrl ?? openLoopbackUrl;
253
+ this.activate = options.activate ?? (() => collectTemporaryActivation(this.openUrl));
254
+ }
255
+ async getLocalEntitlement() {
256
+ return this.readActivation() ? cloneEntitlement(TEMPORARY_ENTITLEMENT) : cloneEntitlement(MISSING_ENTITLEMENT);
257
+ }
258
+ async resolveAccess(request) {
259
+ let activation = this.readActivation();
260
+ if (!activation) {
261
+ if (!request.allowAuthentication)
262
+ return {
263
+ kind: "auth_required",
264
+ entitlement: cloneEntitlement(MISSING_ENTITLEMENT),
265
+ nextAction: {
266
+ kind: "authenticate",
267
+ url: "https://jayzeng.github.io/agentmemory/",
268
+ message: "Run plugin install in an interactive terminal to enter an email address",
269
+ },
270
+ };
271
+ const email = await this.activate();
272
+ this.writeActivation(email);
273
+ activation = this.readActivation();
274
+ }
275
+ if (!activation)
276
+ throw new PluginBootstrapFailure("activation_failed", "The local activation record could not be loaded");
277
+ const response = await this.request(`${this.apiOrigin}/v1/plugin/access`, {
278
+ method: "POST",
279
+ headers: { "Content-Type": "application/json" },
280
+ body: JSON.stringify({
281
+ schemaVersion: 1,
282
+ email: activation.email,
283
+ bundleId: request.bundleId,
284
+ installedVersion: request.installedVersion ?? null,
285
+ coreVersion: this.coreVersion,
286
+ channel: request.channel,
287
+ platform: process.platform,
288
+ architecture: process.arch,
289
+ consentVersion: "activation-v1",
290
+ }),
291
+ });
292
+ const value = (await readJson(response));
293
+ validatePluginEntitlementStatusV1(value.entitlement);
294
+ if (typeof value.artifactGrant !== "string" || !value.artifactGrant)
295
+ throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its artifact grant");
296
+ return { kind: "granted", entitlement: value.entitlement, artifactGrant: value.artifactGrant };
297
+ }
298
+ async listReleases(request) {
299
+ const response = await this.request(`${this.apiOrigin}/v1/plugin/releases`, {
300
+ headers: { Authorization: `Bearer ${request.artifactGrant}` },
301
+ });
302
+ const value = (await readJson(response));
303
+ if (!Array.isArray(value.releases))
304
+ throw new PluginBootstrapFailure("service_response_invalid", "The release response is invalid");
305
+ return value.releases;
306
+ }
307
+ async downloadArtifact(request) {
308
+ const response = await this.request(`${this.artifactOrigin}/v1/artifacts/download`, {
309
+ headers: { Authorization: `Bearer ${request.artifactGrant}` },
310
+ });
311
+ const declared = Number(response.headers.get("Content-Length"));
312
+ if (Number.isFinite(declared) && declared !== request.release.size)
313
+ throw new PluginBootstrapFailure("artifact_size_mismatch", "The artifact response size is invalid");
314
+ const artifact = await readBoundedBody(response, request.release.size);
315
+ if (artifact.byteLength !== request.release.size)
316
+ throw new PluginBootstrapFailure("artifact_size_mismatch", "The artifact response size is invalid");
317
+ return artifact;
318
+ }
319
+ async getManagementAction() {
320
+ return null;
321
+ }
322
+ activationPath() {
323
+ return path.join(this.root, ...ACTIVATION_FILE.split("/"));
324
+ }
325
+ readActivation() {
326
+ const activationPath = this.activationPath();
327
+ if (!fs.existsSync(activationPath))
328
+ return null;
329
+ try {
330
+ const rootStat = fs.lstatSync(this.root);
331
+ const credentialsStat = fs.lstatSync(path.dirname(activationPath));
332
+ if (!rootStat.isDirectory() ||
333
+ rootStat.isSymbolicLink() ||
334
+ !credentialsStat.isDirectory() ||
335
+ credentialsStat.isSymbolicLink())
336
+ return null;
337
+ const stat = fs.lstatSync(activationPath);
338
+ if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
339
+ return null;
340
+ const value = JSON.parse(fs.readFileSync(activationPath, "utf-8"));
341
+ return value.schemaVersion === 1 && isEmail(value.email) && Number.isFinite(Date.parse(value.activatedAt))
342
+ ? value
343
+ : null;
344
+ }
345
+ catch {
346
+ return null;
347
+ }
348
+ }
349
+ writeActivation(email) {
350
+ if (!isEmail(email))
351
+ throw new PluginBootstrapFailure("email_invalid", "Enter a valid email address");
352
+ const target = this.activationPath();
353
+ fs.mkdirSync(this.root, { recursive: true, mode: 0o700 });
354
+ const rootStat = fs.lstatSync(this.root);
355
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
356
+ throw new PluginBootstrapFailure("activation_path_invalid", "The plugin activation root is unsafe");
357
+ const directory = path.dirname(target);
358
+ if (!fs.existsSync(directory))
359
+ fs.mkdirSync(directory, { mode: 0o700 });
360
+ const directoryStat = fs.lstatSync(directory);
361
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink())
362
+ throw new PluginBootstrapFailure("activation_path_invalid", "The plugin activation directory is unsafe");
363
+ const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
364
+ fs.writeFileSync(temporary, `${JSON.stringify({ schemaVersion: 1, email, activatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600, flag: "wx" });
365
+ fs.renameSync(temporary, target);
366
+ }
367
+ async request(url, init = {}) {
368
+ let response;
369
+ try {
370
+ response = await this.fetchImplementation(url, {
371
+ ...init,
372
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
373
+ headers: { Accept: "application/json", ...init.headers },
374
+ });
375
+ }
376
+ catch {
377
+ throw new PluginBootstrapFailure("service_unavailable", "The AgentMemory plugin service is unavailable", true);
378
+ }
379
+ if (!response.ok) {
380
+ let message = `The AgentMemory plugin service returned HTTP ${response.status}`;
381
+ try {
382
+ const value = (await readJson(response));
383
+ if (typeof value.error?.message === "string")
384
+ message = value.error.message;
385
+ }
386
+ catch (error) {
387
+ if (error instanceof PluginBootstrapFailure && error.code === "service_response_too_large")
388
+ throw error;
389
+ // Keep the bounded generic response.
390
+ }
391
+ throw new PluginBootstrapFailure("service_request_failed", message, response.status >= 500);
392
+ }
393
+ return response;
394
+ }
395
+ }