myagentmemory 0.4.17 → 0.5.1
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 +68 -71
- package/dist/cli-spec.d.ts +7 -1
- package/dist/cli-spec.js +214 -12
- package/dist/cli.js +2049 -156
- package/dist/completions.js +24 -18
- package/dist/core.d.ts +42 -4
- package/dist/core.js +242 -68
- package/dist/hooks.d.ts +21 -1
- package/dist/hooks.js +382 -87
- package/dist/mcp-server.d.ts +27 -0
- package/dist/mcp-server.js +106 -0
- package/dist/plugin-bootstrap.js +4 -4
- package/dist/plugin-host.d.ts +33 -0
- package/dist/plugin-runtime.d.ts +13 -1
- package/dist/plugin-runtime.js +44 -2
- package/dist/plugin-service.d.ts +10 -4
- package/dist/plugin-service.js +52 -8
- package/dist/upgrade.d.ts +80 -0
- package/dist/upgrade.js +243 -0
- package/docs/official-plugin-bootstrap.md +3 -3
- package/package.json +24 -4
- package/scripts/install-skills.sh +1 -1
- package/skills/agent/SKILL.md +12 -2
- package/skills/claude-code/SKILL.md +17 -2
- package/skills/codex/SKILL.md +14 -2
- package/skills/cursor/SKILL.md +14 -2
- package/src/cli-spec.ts +218 -12
- package/src/completions.ts +26 -18
- package/src/core.ts +312 -123
- package/src/hooks.ts +395 -85
- package/src/plugin-bootstrap.ts +4 -4
- package/src/plugin-host.ts +33 -0
- package/src/cli.ts +0 -1332
- package/src/plugin-runtime.ts +0 -390
- package/src/plugin-service.ts +0 -627
package/src/plugin-service.ts
DELETED
|
@@ -1,627 +0,0 @@
|
|
|
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: "Install the no-account Pro preview to activate local recall and learning",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
interface TemporaryActivationV1 {
|
|
36
|
-
schemaVersion: 3;
|
|
37
|
-
installationId: 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(): PluginEntitlementStatusV1 {
|
|
58
|
-
return {
|
|
59
|
-
plan: "free",
|
|
60
|
-
state: "active",
|
|
61
|
-
features: ["session-intelligence", "web-console"],
|
|
62
|
-
capabilities: {
|
|
63
|
-
"session-index": { enabled: true },
|
|
64
|
-
recall: { enabled: true, quota: { limit: 10, window: "day", scope: "device" } },
|
|
65
|
-
"session-worker": { enabled: false },
|
|
66
|
-
learning: { enabled: true, quota: { limit: 1, window: "day", scope: "device" } },
|
|
67
|
-
"retrieval-evaluation": { enabled: true },
|
|
68
|
-
"operational-metrics": { enabled: true },
|
|
69
|
-
"web-console": { enabled: true },
|
|
70
|
-
"memory-explorer": { enabled: true },
|
|
71
|
-
},
|
|
72
|
-
reason:
|
|
73
|
-
"Free preview: 10 recalls and 1 learning scan per local day; indexing and dashboard access remain available",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function isEmail(value: string): boolean {
|
|
78
|
-
return (
|
|
79
|
-
Buffer.byteLength(value, "utf-8") <= EMAIL_MAX_BYTES &&
|
|
80
|
-
[...value].every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127) &&
|
|
81
|
-
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function securityHeaders(contentType: string): Record<string, string> {
|
|
86
|
-
return {
|
|
87
|
-
"Cache-Control": "no-store",
|
|
88
|
-
"Content-Security-Policy":
|
|
89
|
-
"default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
|
|
90
|
-
"Content-Type": contentType,
|
|
91
|
-
"Referrer-Policy": "same-origin",
|
|
92
|
-
"X-Content-Type-Options": "nosniff",
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const ACTIVATION_PAGE_STYLES = `
|
|
97
|
-
:root{color-scheme:light;font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#172033;background:#f4f7fb;font-synthesis:none}
|
|
98
|
-
*{box-sizing:border-box}
|
|
99
|
-
body{min-height:100vh;margin:0;padding:2rem 1.25rem;display:grid;place-items:center;background:radial-gradient(circle at 12% 0%,#e8f0ff 0,transparent 38rem),radial-gradient(circle at 100% 100%,#eef2ff 0,transparent 32rem),#f7f9fc}
|
|
100
|
-
main{width:min(100%,34rem)}
|
|
101
|
-
.skip-link{position:fixed;z-index:1;top:1rem;left:1rem;padding:.65rem .8rem;border-radius:.55rem;background:#172033;color:#fff;font-weight:700;transform:translateY(-200%)}
|
|
102
|
-
.skip-link:focus{transform:translateY(0)}
|
|
103
|
-
.brand{display:flex;align-items:center;gap:.75rem;margin:0 0 1.25rem .25rem;color:#33415c;font-size:.875rem;font-weight:700;letter-spacing:.01em}
|
|
104
|
-
.mark{width:2.25rem;height:2.25rem;display:grid;place-items:center;border-radius:.7rem;background:linear-gradient(145deg,#172033,#3159b8);color:#fff;box-shadow:0 .5rem 1.25rem rgba(35,65,130,.2);font-size:1.05rem;letter-spacing:-.08em}
|
|
105
|
-
.card{padding:clamp(1.5rem,5vw,2.5rem);border:1px solid rgba(205,216,231,.9);border-radius:1.5rem;background:rgba(255,255,255,.94);box-shadow:0 1.5rem 4rem rgba(32,51,84,.12),0 .125rem .375rem rgba(32,51,84,.06);backdrop-filter:blur(1rem)}
|
|
106
|
-
.eyebrow{display:inline-flex;align-items:center;gap:.45rem;margin:0 0 1rem;padding:.42rem .7rem;border:1px solid #d9e4f7;border-radius:999px;background:#f3f7ff;color:#315798;font-size:.78rem;font-weight:750;letter-spacing:.03em;text-transform:uppercase}
|
|
107
|
-
.eyebrow::before{content:"";width:.45rem;height:.45rem;border-radius:50%;background:#2b67d1;box-shadow:0 0 0 .22rem #dbe8ff}
|
|
108
|
-
h1{margin:0;color:#141d2c;font-size:clamp(2rem,7vw,2.75rem);line-height:1.06;letter-spacing:-.045em;text-wrap:balance}
|
|
109
|
-
.intro{margin:.9rem 0 1.75rem;color:#526176;font-size:1.05rem;line-height:1.55}
|
|
110
|
-
form{display:grid;gap:.75rem}
|
|
111
|
-
label{color:#29364a;font-size:.9rem;font-weight:700}
|
|
112
|
-
input{width:100%;min-height:3.25rem;padding:.8rem 3.5rem .8rem 1rem;border:1px solid #bcc8d8;border-radius:.8rem;background:#fff;color:#172033;font:inherit;box-shadow:inset 0 1px 2px rgba(24,39,65,.04)}
|
|
113
|
-
input::placeholder{color:#8a96a7}
|
|
114
|
-
input:hover{border-color:#92a2b8}
|
|
115
|
-
input:focus-visible{outline:0;border-color:#2b67d1;box-shadow:0 0 0 .25rem rgba(43,103,209,.15)}
|
|
116
|
-
button{min-height:3.35rem;margin-top:.25rem;padding:.85rem 1rem;border:1px solid #172033;border-radius:.8rem;background:linear-gradient(180deg,#24334a,#172033);color:#fff;font:inherit;font-weight:750;cursor:pointer;box-shadow:0 .55rem 1.2rem rgba(23,32,51,.18);touch-action:manipulation;transition:transform .15s}
|
|
117
|
-
button:hover{background:linear-gradient(180deg,#2d405c,#1d2a40);box-shadow:0 .7rem 1.4rem rgba(23,32,51,.23);transform:translateY(-1px)}
|
|
118
|
-
button:active{transform:translateY(0)}
|
|
119
|
-
button:focus-visible,summary:focus-visible{outline:.2rem solid rgba(43,103,209,.32);outline-offset:.18rem}
|
|
120
|
-
.terminal-note{margin:.8rem 0 0;color:#6a7789;font-size:.85rem;text-align:center}
|
|
121
|
-
.error{margin:0 0 1rem;padding:.8rem 1rem;border:1px solid #efb8bd;border-radius:.75rem;background:#fff3f4;color:#982631;font-size:.9rem;line-height:1.45}
|
|
122
|
-
details{margin-top:1.5rem;padding-top:1.25rem;border-top:1px solid #e2e8f0;color:#5b687a;font-size:.85rem;line-height:1.55}
|
|
123
|
-
summary{color:#46566d;font-weight:700;cursor:pointer;list-style-position:outside;touch-action:manipulation}
|
|
124
|
-
details p{margin:.8rem 0 0}
|
|
125
|
-
.never-sent{padding:.75rem .85rem;border-radius:.65rem;background:#f5f7fa;color:#536176}
|
|
126
|
-
.success{display:grid;place-items:center;width:3.5rem;height:3.5rem;margin-bottom:1.4rem;border-radius:1rem;background:#eaf7ef;color:#197542;font-size:1.65rem;font-weight:800;box-shadow:inset 0 0 0 1px #c9ead6}
|
|
127
|
-
.completion .intro{margin-bottom:0}
|
|
128
|
-
@media (max-width:30rem){body{padding:1rem}.brand{margin-left:.1rem}.card{border-radius:1.15rem}h1{font-size:2rem}}
|
|
129
|
-
@media (prefers-reduced-motion:reduce){input,button{transition:none}}
|
|
130
|
-
`;
|
|
131
|
-
|
|
132
|
-
function activationPage(action: string, error?: string): string {
|
|
133
|
-
const errorHtml = error ? `<p class="error" id="activation-error" role="alert">${error}</p>` : "";
|
|
134
|
-
const describedBy = error ? "activation-error terminal-note" : "terminal-note";
|
|
135
|
-
const autofocus = error ? " autofocus" : "";
|
|
136
|
-
return `<!doctype html>
|
|
137
|
-
<html lang="en">
|
|
138
|
-
<head>
|
|
139
|
-
<meta charset="utf-8">
|
|
140
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
141
|
-
<meta name="theme-color" content="#f4f7fb">
|
|
142
|
-
<title>Activate AgentMemory</title>
|
|
143
|
-
<style>${ACTIVATION_PAGE_STYLES}</style>
|
|
144
|
-
</head>
|
|
145
|
-
<body>
|
|
146
|
-
<a class="skip-link" href="#activation">Skip to Activation</a>
|
|
147
|
-
<main id="activation">
|
|
148
|
-
<div class="brand" translate="no"><span class="mark" aria-hidden="true">AM</span><span>AgentMemory</span></div>
|
|
149
|
-
<section class="card" aria-labelledby="activation-title">
|
|
150
|
-
<p class="eyebrow">Free daily allowance</p>
|
|
151
|
-
<h1 id="activation-title">Activate This Device</h1>
|
|
152
|
-
<p class="intro">Use your email to enable AgentMemory Pro for your local agent sessions.</p>
|
|
153
|
-
<form method="post" action="${action}">
|
|
154
|
-
<label for="email">Email address</label>
|
|
155
|
-
${errorHtml}
|
|
156
|
-
<input id="email" type="email" name="email" autocomplete="email" inputmode="email" spellcheck="false" placeholder="you@example.com…" maxlength="254" required${autofocus} aria-describedby="${describedBy}">
|
|
157
|
-
<button type="submit">Activate AgentMemory</button>
|
|
158
|
-
</form>
|
|
159
|
-
<p class="terminal-note" id="terminal-note">Your terminal will finish setup after activation.</p>
|
|
160
|
-
<details>
|
|
161
|
-
<summary>What’s shared during activation</summary>
|
|
162
|
-
<p>Your email identifies your free daily allowance. The CLI also sends core and bundle versions, platform, architecture, and release channel. The service stores a daily count of opaque session-start operations. Activation records expire after 365 days without use.</p>
|
|
163
|
-
<p class="never-sent"><strong>Never sent:</strong> The request never includes memory, session content, queries, repository paths, raw agent session identifiers, IP addresses, or user-agent strings.</p>
|
|
164
|
-
</details>
|
|
165
|
-
</section>
|
|
166
|
-
</main>
|
|
167
|
-
</body>
|
|
168
|
-
</html>`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function completionPage(): string {
|
|
172
|
-
return `<!doctype html>
|
|
173
|
-
<html lang="en">
|
|
174
|
-
<head>
|
|
175
|
-
<meta charset="utf-8">
|
|
176
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
177
|
-
<meta name="theme-color" content="#f4f7fb">
|
|
178
|
-
<title>AgentMemory activated</title>
|
|
179
|
-
<style>${ACTIVATION_PAGE_STYLES}</style>
|
|
180
|
-
</head>
|
|
181
|
-
<body>
|
|
182
|
-
<a class="skip-link" href="#completion">Skip to Activation Status</a>
|
|
183
|
-
<main id="completion">
|
|
184
|
-
<div class="brand" translate="no"><span class="mark" aria-hidden="true">AM</span><span>AgentMemory</span></div>
|
|
185
|
-
<section class="card completion" aria-labelledby="completion-title">
|
|
186
|
-
<div class="success" aria-hidden="true">✓</div>
|
|
187
|
-
<p class="eyebrow">Device activated</p>
|
|
188
|
-
<h1 id="completion-title">You’re All Set</h1>
|
|
189
|
-
<p class="intro">Return to your terminal to finish installing AgentMemory Pro. You can close this tab.</p>
|
|
190
|
-
</section>
|
|
191
|
-
</main>
|
|
192
|
-
</body>
|
|
193
|
-
</html>`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function send(response: ServerResponse, status: number, body: string, contentType = "text/html; charset=utf-8"): void {
|
|
197
|
-
response.writeHead(status, securityHeaders(contentType));
|
|
198
|
-
response.end(body);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function readBoundedBody(response: Response, maxBytes: number): Promise<Uint8Array> {
|
|
202
|
-
if (!response.body) return new Uint8Array();
|
|
203
|
-
const reader = response.body.getReader();
|
|
204
|
-
const chunks: Uint8Array[] = [];
|
|
205
|
-
let size = 0;
|
|
206
|
-
while (true) {
|
|
207
|
-
const { done, value } = await reader.read();
|
|
208
|
-
if (done) break;
|
|
209
|
-
size += value.byteLength;
|
|
210
|
-
if (size > maxBytes) {
|
|
211
|
-
await reader.cancel();
|
|
212
|
-
throw new PluginBootstrapFailure("service_response_too_large", "The plugin service response is too large");
|
|
213
|
-
}
|
|
214
|
-
chunks.push(value);
|
|
215
|
-
}
|
|
216
|
-
const output = new Uint8Array(size);
|
|
217
|
-
let offset = 0;
|
|
218
|
-
for (const chunk of chunks) {
|
|
219
|
-
output.set(chunk, offset);
|
|
220
|
-
offset += chunk.byteLength;
|
|
221
|
-
}
|
|
222
|
-
return output;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async function readJson(response: Response): Promise<unknown> {
|
|
226
|
-
try {
|
|
227
|
-
return JSON.parse(new TextDecoder().decode(await readBoundedBody(response, SERVICE_JSON_MAX_BYTES)));
|
|
228
|
-
} catch (error) {
|
|
229
|
-
if (error instanceof PluginBootstrapFailure) throw error;
|
|
230
|
-
throw new PluginBootstrapFailure("service_response_invalid", "The plugin service returned invalid JSON");
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function readForm(request: IncomingMessage): Promise<URLSearchParams> {
|
|
235
|
-
return new Promise((resolve, reject) => {
|
|
236
|
-
let size = 0;
|
|
237
|
-
const chunks: Buffer[] = [];
|
|
238
|
-
request.on("data", (chunk: Buffer) => {
|
|
239
|
-
size += chunk.byteLength;
|
|
240
|
-
if (size > FORM_MAX_BYTES) {
|
|
241
|
-
reject(new Error("form too large"));
|
|
242
|
-
request.destroy();
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
chunks.push(chunk);
|
|
246
|
-
});
|
|
247
|
-
request.on("end", () => resolve(new URLSearchParams(Buffer.concat(chunks).toString("utf-8"))));
|
|
248
|
-
request.on("error", reject);
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function isSameOriginActivationPost(request: IncomingMessage, expectedHost: string, activationPath: string): boolean {
|
|
253
|
-
const expectedOrigin = `http://${expectedHost}`;
|
|
254
|
-
const origin = request.headers.origin;
|
|
255
|
-
const fetchSite = request.headers["sec-fetch-site"];
|
|
256
|
-
if (origin === "null") {
|
|
257
|
-
return (
|
|
258
|
-
fetchSite === "same-origin" &&
|
|
259
|
-
request.headers["sec-fetch-mode"] === "navigate" &&
|
|
260
|
-
request.headers["sec-fetch-dest"] === "document" &&
|
|
261
|
-
request.headers["sec-fetch-user"] === "?1"
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
if (origin !== undefined) return origin === expectedOrigin;
|
|
265
|
-
|
|
266
|
-
if (fetchSite !== undefined && fetchSite !== "same-origin") return false;
|
|
267
|
-
|
|
268
|
-
const referer = request.headers.referer;
|
|
269
|
-
if (referer !== undefined) {
|
|
270
|
-
try {
|
|
271
|
-
const parsed = new URL(referer);
|
|
272
|
-
return parsed.origin === expectedOrigin && parsed.pathname === activationPath;
|
|
273
|
-
} catch {
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Privacy-focused and older browsers may omit all three headers. The exact
|
|
279
|
-
// loopback Host plus the 192-bit, single-use path remains the CSRF capability.
|
|
280
|
-
return true;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function openLoopbackUrl(url: string): boolean {
|
|
284
|
-
const parsed = new URL(url);
|
|
285
|
-
if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1") return false;
|
|
286
|
-
try {
|
|
287
|
-
const child =
|
|
288
|
-
process.platform === "darwin"
|
|
289
|
-
? spawn("open", [parsed.toString()], { detached: true, stdio: "ignore" })
|
|
290
|
-
: process.platform === "win32"
|
|
291
|
-
? spawn("explorer.exe", [parsed.toString()], { detached: true, stdio: "ignore" })
|
|
292
|
-
: spawn("xdg-open", [parsed.toString()], { detached: true, stdio: "ignore" });
|
|
293
|
-
child.unref();
|
|
294
|
-
return true;
|
|
295
|
-
} catch {
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export async function collectTemporaryActivation(openUrl: (url: string) => boolean = openLoopbackUrl): Promise<string> {
|
|
301
|
-
const nonce = randomBytes(24).toString("hex");
|
|
302
|
-
const activationPath = `/activate/${nonce}`;
|
|
303
|
-
let expectedHost = "";
|
|
304
|
-
let settle: ((email: string) => void) | null = null;
|
|
305
|
-
let fail: ((error: Error) => void) | null = null;
|
|
306
|
-
const result = new Promise<string>((resolve, reject) => {
|
|
307
|
-
settle = resolve;
|
|
308
|
-
fail = reject;
|
|
309
|
-
});
|
|
310
|
-
const server = createServer(async (request, response) => {
|
|
311
|
-
if (request.headers.host !== expectedHost || request.url !== activationPath) {
|
|
312
|
-
send(response, 404, "Not found", "text/plain; charset=utf-8");
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if (request.method === "GET") {
|
|
316
|
-
send(response, 200, activationPage(activationPath));
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
if (request.method !== "POST" || !isSameOriginActivationPost(request, expectedHost, activationPath)) {
|
|
320
|
-
send(response, 403, "Forbidden", "text/plain; charset=utf-8");
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
try {
|
|
324
|
-
const email = (await readForm(request)).get("email")?.trim() ?? "";
|
|
325
|
-
if (!isEmail(email)) {
|
|
326
|
-
send(response, 400, activationPage(activationPath, "Enter a valid email address."));
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
send(response, 200, completionPage());
|
|
330
|
-
settle?.(email);
|
|
331
|
-
settle = null;
|
|
332
|
-
server.close();
|
|
333
|
-
} catch {
|
|
334
|
-
send(response, 400, activationPage(activationPath, "The activation form could not be read."));
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
server.requestTimeout = 10_000;
|
|
338
|
-
server.headersTimeout = 10_000;
|
|
339
|
-
server.maxHeadersCount = 40;
|
|
340
|
-
await new Promise<void>((resolve, reject) => {
|
|
341
|
-
server.once("error", reject);
|
|
342
|
-
server.listen(0, "127.0.0.1", resolve);
|
|
343
|
-
});
|
|
344
|
-
const address = server.address();
|
|
345
|
-
if (!address || typeof address === "string")
|
|
346
|
-
throw new PluginBootstrapFailure("activation_failed", "Activation did not bind to loopback");
|
|
347
|
-
expectedHost = `127.0.0.1:${address.port}`;
|
|
348
|
-
const url = `http://${expectedHost}${activationPath}`;
|
|
349
|
-
console.log(`AgentMemory activation: ${url}`);
|
|
350
|
-
let opened = false;
|
|
351
|
-
try {
|
|
352
|
-
opened = openUrl(url);
|
|
353
|
-
} catch {
|
|
354
|
-
server.close();
|
|
355
|
-
throw new PluginBootstrapFailure("browser_unavailable", `Open this URL in a browser: ${url}`);
|
|
356
|
-
}
|
|
357
|
-
if (!opened) {
|
|
358
|
-
server.close();
|
|
359
|
-
throw new PluginBootstrapFailure("browser_unavailable", `Open this URL in a browser: ${url}`);
|
|
360
|
-
}
|
|
361
|
-
const timer = setTimeout(() => {
|
|
362
|
-
fail?.(new PluginBootstrapFailure("activation_timeout", "Temporary activation timed out", true));
|
|
363
|
-
server.close();
|
|
364
|
-
}, 5 * 60_000);
|
|
365
|
-
timer.unref();
|
|
366
|
-
try {
|
|
367
|
-
return await result;
|
|
368
|
-
} finally {
|
|
369
|
-
clearTimeout(timer);
|
|
370
|
-
server.close();
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
|
|
375
|
-
private readonly root: string;
|
|
376
|
-
private readonly coreVersion: string;
|
|
377
|
-
private readonly apiOrigin: string;
|
|
378
|
-
private readonly artifactOrigin: string;
|
|
379
|
-
private readonly fetchImplementation: typeof globalThis.fetch;
|
|
380
|
-
private readonly openUrl: (url: string) => boolean;
|
|
381
|
-
private readonly activate: () => Promise<string>;
|
|
382
|
-
|
|
383
|
-
constructor(options: TemporaryPluginBackendOptions = {}) {
|
|
384
|
-
this.root = path.resolve(options.root ?? getDefaultPluginInstallRoot());
|
|
385
|
-
this.coreVersion = options.coreVersion ?? "0.0.0";
|
|
386
|
-
this.apiOrigin = options.apiOrigin ?? API_ORIGIN;
|
|
387
|
-
this.artifactOrigin = options.artifactOrigin ?? ARTIFACT_ORIGIN;
|
|
388
|
-
this.fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
389
|
-
this.openUrl = options.openUrl ?? openLoopbackUrl;
|
|
390
|
-
this.activate = options.activate ?? (async () => `am_install_${randomBytes(24).toString("base64url")}`);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async getLocalEntitlement(): Promise<PluginEntitlementStatusV1> {
|
|
394
|
-
const activation = this.readActivation();
|
|
395
|
-
return activation ? freeEntitlement() : cloneEntitlement(MISSING_ENTITLEMENT);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
async resolveAccess(request: {
|
|
399
|
-
bundleId: string;
|
|
400
|
-
installedVersion?: string;
|
|
401
|
-
channel: string;
|
|
402
|
-
allowAuthentication: boolean;
|
|
403
|
-
}): Promise<PluginAccessDecisionV1> {
|
|
404
|
-
const activation = this.readActivation();
|
|
405
|
-
const installationId = activation?.installationId ?? (await this.activate());
|
|
406
|
-
const response = await this.request(`${this.apiOrigin}/v1/plugin/access`, {
|
|
407
|
-
method: "POST",
|
|
408
|
-
headers: { "Content-Type": "application/json" },
|
|
409
|
-
body: JSON.stringify({
|
|
410
|
-
schemaVersion: 2,
|
|
411
|
-
installationId,
|
|
412
|
-
bundleId: request.bundleId,
|
|
413
|
-
installedVersion: request.installedVersion ?? null,
|
|
414
|
-
coreVersion: this.coreVersion,
|
|
415
|
-
channel: request.channel,
|
|
416
|
-
platform: process.platform,
|
|
417
|
-
architecture: process.arch,
|
|
418
|
-
}),
|
|
419
|
-
});
|
|
420
|
-
const value = (await readJson(response)) as {
|
|
421
|
-
entitlement?: unknown;
|
|
422
|
-
artifactGrant?: unknown;
|
|
423
|
-
usageCredential?: unknown;
|
|
424
|
-
};
|
|
425
|
-
validatePluginEntitlementStatusV1(value.entitlement);
|
|
426
|
-
if (typeof value.artifactGrant !== "string" || !value.artifactGrant)
|
|
427
|
-
throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its artifact grant");
|
|
428
|
-
if (typeof value.usageCredential !== "string" || !ACTIVATION_CREDENTIAL.test(value.usageCredential))
|
|
429
|
-
throw new PluginBootstrapFailure(
|
|
430
|
-
"service_response_invalid",
|
|
431
|
-
"The access response omitted its usage credential",
|
|
432
|
-
);
|
|
433
|
-
const recallQuota = value.entitlement.capabilities.recall?.quota;
|
|
434
|
-
const learningQuota = value.entitlement.capabilities.learning?.quota;
|
|
435
|
-
if (
|
|
436
|
-
value.entitlement.plan !== "free" ||
|
|
437
|
-
value.entitlement.state !== "active" ||
|
|
438
|
-
!recallQuota ||
|
|
439
|
-
recallQuota.scope !== "device" ||
|
|
440
|
-
recallQuota.window !== "day" ||
|
|
441
|
-
!learningQuota ||
|
|
442
|
-
learningQuota.scope !== "device" ||
|
|
443
|
-
learningQuota.window !== "day" ||
|
|
444
|
-
value.entitlement.capabilities["session-index"]?.enabled !== true ||
|
|
445
|
-
value.entitlement.capabilities["web-console"]?.enabled !== true
|
|
446
|
-
)
|
|
447
|
-
throw new PluginBootstrapFailure("service_response_invalid", "The free preview policy is invalid");
|
|
448
|
-
this.writeActivation(installationId, value.usageCredential, 1);
|
|
449
|
-
return { kind: "granted", entitlement: value.entitlement, artifactGrant: value.artifactGrant };
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async reserveSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
|
|
453
|
-
return this.sessionUsage("reserve", operationId);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async commitSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
|
|
457
|
-
return this.sessionUsage("commit", operationId);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
async releaseSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
|
|
461
|
-
return this.sessionUsage("release", operationId);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
async listReleases(request: {
|
|
465
|
-
bundleId: string;
|
|
466
|
-
channel: string;
|
|
467
|
-
artifactGrant: string;
|
|
468
|
-
}): Promise<SignedPluginReleaseV1[]> {
|
|
469
|
-
const response = await this.request(`${this.apiOrigin}/v1/plugin/releases`, {
|
|
470
|
-
headers: { Authorization: `Bearer ${request.artifactGrant}` },
|
|
471
|
-
});
|
|
472
|
-
const value = (await readJson(response)) as { releases?: unknown };
|
|
473
|
-
if (!Array.isArray(value.releases))
|
|
474
|
-
throw new PluginBootstrapFailure("service_response_invalid", "The release response is invalid");
|
|
475
|
-
return value.releases as SignedPluginReleaseV1[];
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
async downloadArtifact(request: { release: SignedPluginReleaseV1; artifactGrant: string }): Promise<Uint8Array> {
|
|
479
|
-
const response = await this.request(`${this.artifactOrigin}/v1/artifacts/download`, {
|
|
480
|
-
headers: { Authorization: `Bearer ${request.artifactGrant}` },
|
|
481
|
-
});
|
|
482
|
-
const declared = Number(response.headers.get("Content-Length"));
|
|
483
|
-
if (Number.isFinite(declared) && declared !== request.release.size)
|
|
484
|
-
throw new PluginBootstrapFailure("artifact_size_mismatch", "The artifact response size is invalid");
|
|
485
|
-
const artifact = await readBoundedBody(response, request.release.size);
|
|
486
|
-
if (artifact.byteLength !== request.release.size)
|
|
487
|
-
throw new PluginBootstrapFailure("artifact_size_mismatch", "The artifact response size is invalid");
|
|
488
|
-
return artifact;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async getManagementAction(): Promise<PluginNextActionV1 | null> {
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
private activationPath(): string {
|
|
496
|
-
return path.join(this.root, ...ACTIVATION_FILE.split("/"));
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
private readActivation(): TemporaryActivationV1 | null {
|
|
500
|
-
const activationPath = this.activationPath();
|
|
501
|
-
if (!fs.existsSync(activationPath)) return null;
|
|
502
|
-
try {
|
|
503
|
-
const rootStat = fs.lstatSync(this.root);
|
|
504
|
-
const credentialsStat = fs.lstatSync(path.dirname(activationPath));
|
|
505
|
-
if (
|
|
506
|
-
!rootStat.isDirectory() ||
|
|
507
|
-
rootStat.isSymbolicLink() ||
|
|
508
|
-
!credentialsStat.isDirectory() ||
|
|
509
|
-
credentialsStat.isSymbolicLink()
|
|
510
|
-
)
|
|
511
|
-
return null;
|
|
512
|
-
const stat = fs.lstatSync(activationPath);
|
|
513
|
-
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
|
|
514
|
-
return null;
|
|
515
|
-
const value = JSON.parse(fs.readFileSync(activationPath, "utf-8")) as TemporaryActivationV1;
|
|
516
|
-
if (
|
|
517
|
-
value.schemaVersion !== 3 ||
|
|
518
|
-
typeof value.installationId !== "string" ||
|
|
519
|
-
!/^am_install_[A-Za-z0-9_-]{32}$/.test(value.installationId) ||
|
|
520
|
-
!Number.isFinite(Date.parse(value.activatedAt)) ||
|
|
521
|
-
!ACTIVATION_CREDENTIAL.test(value.usageCredential) ||
|
|
522
|
-
!Number.isSafeInteger(value.dailySessionLimit) ||
|
|
523
|
-
value.dailySessionLimit <= 0 ||
|
|
524
|
-
value.dailySessionLimit > 10_000
|
|
525
|
-
)
|
|
526
|
-
return null;
|
|
527
|
-
return value;
|
|
528
|
-
} catch {
|
|
529
|
-
return null;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
private writeActivation(installationId: string, usageCredential: string, dailySessionLimit: number): void {
|
|
534
|
-
if (!/^am_install_[A-Za-z0-9_-]{32}$/.test(installationId))
|
|
535
|
-
throw new PluginBootstrapFailure("activation_failed", "The installation identifier is invalid");
|
|
536
|
-
if (!ACTIVATION_CREDENTIAL.test(usageCredential))
|
|
537
|
-
throw new PluginBootstrapFailure("activation_failed", "The activation credential is invalid");
|
|
538
|
-
if (!Number.isSafeInteger(dailySessionLimit) || dailySessionLimit <= 0 || dailySessionLimit > 10_000)
|
|
539
|
-
throw new PluginBootstrapFailure("activation_failed", "The free session allowance is invalid");
|
|
540
|
-
const target = this.activationPath();
|
|
541
|
-
fs.mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
542
|
-
const rootStat = fs.lstatSync(this.root);
|
|
543
|
-
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
544
|
-
throw new PluginBootstrapFailure("activation_path_invalid", "The plugin activation root is unsafe");
|
|
545
|
-
const directory = path.dirname(target);
|
|
546
|
-
if (!fs.existsSync(directory)) fs.mkdirSync(directory, { mode: 0o700 });
|
|
547
|
-
const directoryStat = fs.lstatSync(directory);
|
|
548
|
-
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink())
|
|
549
|
-
throw new PluginBootstrapFailure("activation_path_invalid", "The plugin activation directory is unsafe");
|
|
550
|
-
const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
|
|
551
|
-
fs.writeFileSync(
|
|
552
|
-
temporary,
|
|
553
|
-
`${JSON.stringify(
|
|
554
|
-
{
|
|
555
|
-
schemaVersion: 3,
|
|
556
|
-
installationId,
|
|
557
|
-
activatedAt: new Date().toISOString(),
|
|
558
|
-
usageCredential,
|
|
559
|
-
dailySessionLimit,
|
|
560
|
-
},
|
|
561
|
-
null,
|
|
562
|
-
2,
|
|
563
|
-
)}\n`,
|
|
564
|
-
{ mode: 0o600, flag: "wx" },
|
|
565
|
-
);
|
|
566
|
-
fs.renameSync(temporary, target);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
private async sessionUsage(
|
|
570
|
-
action: "reserve" | "commit" | "release",
|
|
571
|
-
operationId: string,
|
|
572
|
-
): Promise<PluginSessionUsageDecisionV1> {
|
|
573
|
-
const activation = this.readActivation();
|
|
574
|
-
if (!activation) throw new PluginBootstrapFailure("auth_required", "Run plugin install to activate AgentMemory");
|
|
575
|
-
const response = await this.request(`${this.apiOrigin}/v1/plugin/sessions/${action}`, {
|
|
576
|
-
method: "POST",
|
|
577
|
-
headers: {
|
|
578
|
-
Authorization: `Bearer ${activation.usageCredential}`,
|
|
579
|
-
"Content-Type": "application/json",
|
|
580
|
-
},
|
|
581
|
-
body: JSON.stringify({ schemaVersion: 1, operationId }),
|
|
582
|
-
});
|
|
583
|
-
const value = (await readJson(response)) as { decision?: Partial<PluginSessionUsageDecisionV1> };
|
|
584
|
-
const decision = value.decision;
|
|
585
|
-
if (
|
|
586
|
-
!decision ||
|
|
587
|
-
typeof decision.allowed !== "boolean" ||
|
|
588
|
-
!["reserved", "committed", "released", "exhausted", "missing"].includes(String(decision.state)) ||
|
|
589
|
-
!Number.isSafeInteger(decision.limit) ||
|
|
590
|
-
Number(decision.limit) <= 0 ||
|
|
591
|
-
!Number.isSafeInteger(decision.used) ||
|
|
592
|
-
Number(decision.used) < 0 ||
|
|
593
|
-
!Number.isSafeInteger(decision.remaining) ||
|
|
594
|
-
Number(decision.remaining) < 0 ||
|
|
595
|
-
typeof decision.resetAt !== "string" ||
|
|
596
|
-
!Number.isFinite(Date.parse(decision.resetAt)) ||
|
|
597
|
-
typeof decision.idempotent !== "boolean"
|
|
598
|
-
)
|
|
599
|
-
throw new PluginBootstrapFailure("service_response_invalid", "The session usage response is invalid");
|
|
600
|
-
return decision as PluginSessionUsageDecisionV1;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
private async request(url: string, init: RequestInit = {}): Promise<Response> {
|
|
604
|
-
let response: Response;
|
|
605
|
-
try {
|
|
606
|
-
response = await this.fetchImplementation(url, {
|
|
607
|
-
...init,
|
|
608
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
609
|
-
headers: { Accept: "application/json", ...init.headers },
|
|
610
|
-
});
|
|
611
|
-
} catch {
|
|
612
|
-
throw new PluginBootstrapFailure("service_unavailable", "The AgentMemory plugin service is unavailable", true);
|
|
613
|
-
}
|
|
614
|
-
if (!response.ok) {
|
|
615
|
-
let message = `The AgentMemory plugin service returned HTTP ${response.status}`;
|
|
616
|
-
try {
|
|
617
|
-
const value = (await readJson(response)) as { error?: { message?: unknown } };
|
|
618
|
-
if (typeof value.error?.message === "string") message = value.error.message;
|
|
619
|
-
} catch (error) {
|
|
620
|
-
if (error instanceof PluginBootstrapFailure && error.code === "service_response_too_large") throw error;
|
|
621
|
-
// Keep the bounded generic response.
|
|
622
|
-
}
|
|
623
|
-
throw new PluginBootstrapFailure("service_request_failed", message, response.status >= 500);
|
|
624
|
-
}
|
|
625
|
-
return response;
|
|
626
|
-
}
|
|
627
|
-
}
|