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.
- package/README.md +118 -49
- package/dist/cli-spec.d.ts +25 -0
- package/dist/cli-spec.js +211 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +435 -71
- package/dist/completions.d.ts +13 -0
- package/dist/completions.js +429 -0
- package/dist/core.d.ts +22 -1
- package/dist/core.js +299 -62
- package/dist/hooks.d.ts +42 -0
- package/dist/hooks.js +444 -0
- package/dist/plugin-bootstrap.d.ts +190 -0
- package/dist/plugin-bootstrap.js +628 -0
- package/dist/plugin-host.d.ts +136 -0
- package/dist/plugin-host.js +98 -0
- package/dist/plugin-runtime.d.ts +21 -0
- package/dist/plugin-runtime.js +208 -0
- package/dist/plugin-service.d.ts +45 -0
- package/dist/plugin-service.js +395 -0
- package/docs/official-plugin-bootstrap.md +335 -0
- package/package.json +62 -11
- package/scripts/install-skills.sh +4 -1
- package/scripts/postinstall.cjs +23 -4
- package/src/cli-spec.ts +236 -0
- package/src/cli.ts +455 -82
- package/src/completions.ts +501 -0
- package/src/core.ts +314 -62
- package/src/hooks.ts +485 -0
- package/src/plugin-bootstrap.ts +931 -0
- package/src/plugin-host.ts +255 -0
- package/src/plugin-runtime.ts +296 -0
- package/src/plugin-service.ts +451 -0
- package/dist/agent-memory +0 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import { createHash, createPublicKey, randomUUID, verify } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { AGENT_MEMORY_PLUGIN_API_VERSION, isSafeBundlePath, validateBundleManifestV1, } from "./plugin-host.js";
|
|
6
|
+
import { TemporaryPluginBackend } from "./plugin-service.js";
|
|
7
|
+
export const OFFICIAL_BUNDLE_ID = "agentmemory.pro";
|
|
8
|
+
export const OFFICIAL_PLUGIN_IDS = ["agentmemory.session-intelligence", "agentmemory.web-console"];
|
|
9
|
+
const MISSING_ENTITLEMENT = {
|
|
10
|
+
plan: null,
|
|
11
|
+
state: "missing",
|
|
12
|
+
features: [],
|
|
13
|
+
capabilities: {},
|
|
14
|
+
reason: "No signed AgentMemory commercial entitlement is installed",
|
|
15
|
+
};
|
|
16
|
+
const OFFICIAL_PLUGINS = [
|
|
17
|
+
{ id: OFFICIAL_PLUGIN_IDS[0], name: "Session Intelligence" },
|
|
18
|
+
{ id: OFFICIAL_PLUGIN_IDS[1], name: "Web Console" },
|
|
19
|
+
];
|
|
20
|
+
const PACKAGE_MAX_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
const PACKAGE_MAX_EXPANDED_BYTES = 128 * 1024 * 1024;
|
|
22
|
+
const PACKAGE_MAX_FILES = 10_000;
|
|
23
|
+
const TEMPORARY_RELEASE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
24
|
+
MCowBQYDK2VwAyEASefZFUVFy1EmvGbd0ckHZThmPgqQ3u9HCwZRReAZQW8=
|
|
25
|
+
-----END PUBLIC KEY-----`;
|
|
26
|
+
export class PluginBootstrapFailure extends Error {
|
|
27
|
+
code;
|
|
28
|
+
retryable;
|
|
29
|
+
constructor(code, message, retryable = false) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.retryable = retryable;
|
|
33
|
+
this.name = "PluginBootstrapFailure";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class UnavailablePluginBackend {
|
|
37
|
+
async getLocalEntitlement() {
|
|
38
|
+
return structuredClone(MISSING_ENTITLEMENT);
|
|
39
|
+
}
|
|
40
|
+
async resolveAccess() {
|
|
41
|
+
return {
|
|
42
|
+
kind: "unavailable",
|
|
43
|
+
entitlement: structuredClone(MISSING_ENTITLEMENT),
|
|
44
|
+
error: {
|
|
45
|
+
code: "service_not_configured",
|
|
46
|
+
message: "The AgentMemory commercial service is not configured in this build",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async listReleases() {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
async downloadArtifact() {
|
|
54
|
+
throw new PluginBootstrapFailure("service_not_configured", "The AgentMemory commercial service is not configured in this build");
|
|
55
|
+
}
|
|
56
|
+
async getManagementAction() {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class RejectingReleaseVerifier {
|
|
61
|
+
verifyRelease() {
|
|
62
|
+
throw new PluginBootstrapFailure("signing_keys_unavailable", "No commercial release signing keys are configured");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export class Ed25519ReleaseVerifier {
|
|
66
|
+
keys = new Map();
|
|
67
|
+
constructor(keys) {
|
|
68
|
+
for (const [keyId, key] of Object.entries(keys))
|
|
69
|
+
this.keys.set(keyId, createPublicKey(key));
|
|
70
|
+
}
|
|
71
|
+
verifyRelease(release) {
|
|
72
|
+
validateRelease(release);
|
|
73
|
+
const key = this.keys.get(release.signature.keyId);
|
|
74
|
+
if (!key)
|
|
75
|
+
throw new PluginBootstrapFailure("unknown_signing_key", "The release uses an unknown signing key");
|
|
76
|
+
const signature = decodeBase64Strict(release.signature.value, "release signature");
|
|
77
|
+
const valid = verify(null, releaseSigningPayload(release), key, signature);
|
|
78
|
+
if (!valid)
|
|
79
|
+
throw new PluginBootstrapFailure("signature_invalid", "The commercial release signature is invalid");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export class FilePluginInstallStore {
|
|
83
|
+
root;
|
|
84
|
+
constructor(root = getDefaultPluginInstallRoot()) {
|
|
85
|
+
this.root = path.resolve(root);
|
|
86
|
+
}
|
|
87
|
+
readReceipt(bundleId) {
|
|
88
|
+
validateId(bundleId, "bundle");
|
|
89
|
+
const receiptPath = this.receiptPath(bundleId);
|
|
90
|
+
if (!fs.existsSync(receiptPath))
|
|
91
|
+
return null;
|
|
92
|
+
const stat = fs.lstatSync(receiptPath);
|
|
93
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
94
|
+
throw new PluginBootstrapFailure("receipt_invalid", "The plugin install receipt is not a regular file");
|
|
95
|
+
let receipt;
|
|
96
|
+
try {
|
|
97
|
+
receipt = JSON.parse(fs.readFileSync(receiptPath, "utf-8"));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new PluginBootstrapFailure("receipt_invalid", "The plugin install receipt is not valid JSON");
|
|
101
|
+
}
|
|
102
|
+
validateReceipt(receipt, bundleId);
|
|
103
|
+
return receipt;
|
|
104
|
+
}
|
|
105
|
+
hasInstalledBundle(receipt) {
|
|
106
|
+
try {
|
|
107
|
+
const directory = this.versionPath(receipt.bundleId, receipt.version);
|
|
108
|
+
const marker = path.join(directory, ".package-sha256");
|
|
109
|
+
const entrypoint = resolveManagedPath(directory, receipt.entrypoint);
|
|
110
|
+
return (fs.lstatSync(directory).isDirectory() &&
|
|
111
|
+
!fs.lstatSync(directory).isSymbolicLink() &&
|
|
112
|
+
fs.lstatSync(entrypoint).isFile() &&
|
|
113
|
+
!fs.lstatSync(entrypoint).isSymbolicLink() &&
|
|
114
|
+
fs.readFileSync(marker, "utf-8").trim() === receipt.packageSha256);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async install(packageBytes, release, healthCheck = defaultHealthCheck) {
|
|
121
|
+
validateRelease(release);
|
|
122
|
+
this.ensureRoot();
|
|
123
|
+
const releaseDigest = sha256(packageBytes);
|
|
124
|
+
if (release.size !== packageBytes.byteLength)
|
|
125
|
+
throw new PluginBootstrapFailure("artifact_size_mismatch", "The downloaded package size does not match its release");
|
|
126
|
+
if (release.packageSha256 !== releaseDigest)
|
|
127
|
+
throw new PluginBootstrapFailure("artifact_digest_mismatch", "The downloaded package digest does not match its release");
|
|
128
|
+
const lock = this.acquireLock();
|
|
129
|
+
const stagingDirectory = path.join(this.root, "staging", `${release.manifest.id}-${randomUUID()}`);
|
|
130
|
+
try {
|
|
131
|
+
const packageValue = decodePluginPackage(packageBytes);
|
|
132
|
+
if (JSON.stringify(packageValue.manifest) !== JSON.stringify(release.manifest))
|
|
133
|
+
throw new PluginBootstrapFailure("package_manifest_mismatch", "The package manifest does not match its release");
|
|
134
|
+
extractPackage(packageValue, stagingDirectory);
|
|
135
|
+
fs.writeFileSync(path.join(stagingDirectory, ".package-sha256"), `${releaseDigest}\n`, { mode: 0o600 });
|
|
136
|
+
await healthCheck(stagingDirectory, release);
|
|
137
|
+
const target = this.versionPath(release.manifest.id, release.manifest.version);
|
|
138
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
139
|
+
if (fs.existsSync(target)) {
|
|
140
|
+
const markerPath = path.join(target, ".package-sha256");
|
|
141
|
+
const targetStat = fs.lstatSync(target);
|
|
142
|
+
if (targetStat.isSymbolicLink() ||
|
|
143
|
+
!targetStat.isDirectory() ||
|
|
144
|
+
!fs.existsSync(markerPath) ||
|
|
145
|
+
fs.readFileSync(markerPath, "utf-8").trim() !== releaseDigest)
|
|
146
|
+
throw new PluginBootstrapFailure("version_conflict", "The target plugin version already exists with different contents");
|
|
147
|
+
removeManagedDirectory(this.root, stagingDirectory);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
fs.renameSync(stagingDirectory, target);
|
|
151
|
+
}
|
|
152
|
+
const previous = this.readReceipt(release.manifest.id);
|
|
153
|
+
const receipt = {
|
|
154
|
+
schemaVersion: 1,
|
|
155
|
+
bundleId: release.manifest.id,
|
|
156
|
+
version: release.manifest.version,
|
|
157
|
+
channel: release.manifest.channel,
|
|
158
|
+
pluginApi: AGENT_MEMORY_PLUGIN_API_VERSION,
|
|
159
|
+
entrypoint: release.manifest.entrypoint,
|
|
160
|
+
packageSha256: release.packageSha256,
|
|
161
|
+
installedAt: new Date().toISOString(),
|
|
162
|
+
...(previous && previous.version !== release.manifest.version ? { previousVersion: previous.version } : {}),
|
|
163
|
+
};
|
|
164
|
+
this.writeReceipt(receipt);
|
|
165
|
+
return receipt;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
if (fs.existsSync(stagingDirectory)) {
|
|
169
|
+
try {
|
|
170
|
+
removeManagedDirectory(this.root, stagingDirectory);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Preserve the primary failure. Staging remains inert because no receipt references it.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
this.releaseLock(lock);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
uninstall(bundleId) {
|
|
180
|
+
validateId(bundleId, "bundle");
|
|
181
|
+
this.ensureRoot();
|
|
182
|
+
const lock = this.acquireLock();
|
|
183
|
+
try {
|
|
184
|
+
const receipt = this.readReceipt(bundleId);
|
|
185
|
+
if (!receipt)
|
|
186
|
+
return null;
|
|
187
|
+
const versions = path.join(this.root, "bundles", bundleId);
|
|
188
|
+
if (fs.existsSync(versions))
|
|
189
|
+
removeManagedDirectory(this.root, versions);
|
|
190
|
+
fs.unlinkSync(this.receiptPath(bundleId));
|
|
191
|
+
return receipt;
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
this.releaseLock(lock);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
ensureRoot() {
|
|
198
|
+
ensureRegularDirectory(this.root, true);
|
|
199
|
+
ensureRegularDirectory(path.join(this.root, "receipts"));
|
|
200
|
+
ensureRegularDirectory(path.join(this.root, "staging"));
|
|
201
|
+
ensureRegularDirectory(path.join(this.root, "bundles"));
|
|
202
|
+
}
|
|
203
|
+
receiptPath(bundleId) {
|
|
204
|
+
return resolveManagedPath(this.root, `receipts/${bundleId}.json`);
|
|
205
|
+
}
|
|
206
|
+
versionPath(bundleId, version) {
|
|
207
|
+
validateId(bundleId, "bundle");
|
|
208
|
+
validateVersion(version);
|
|
209
|
+
return resolveManagedPath(this.root, `bundles/${bundleId}/${version}`);
|
|
210
|
+
}
|
|
211
|
+
writeReceipt(receipt) {
|
|
212
|
+
const target = this.receiptPath(receipt.bundleId);
|
|
213
|
+
const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
|
|
214
|
+
fs.writeFileSync(temporary, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
|
|
215
|
+
fs.renameSync(temporary, target);
|
|
216
|
+
}
|
|
217
|
+
acquireLock() {
|
|
218
|
+
const lockPath = path.join(this.root, "install.lock");
|
|
219
|
+
try {
|
|
220
|
+
const descriptor = fs.openSync(lockPath, "wx", 0o600);
|
|
221
|
+
fs.writeFileSync(descriptor, `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`);
|
|
222
|
+
return { descriptor, path: lockPath };
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error.code === "EEXIST")
|
|
226
|
+
throw new PluginBootstrapFailure("install_in_progress", "Another plugin installation is already in progress", true);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
releaseLock(lock) {
|
|
231
|
+
try {
|
|
232
|
+
fs.closeSync(lock.descriptor);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Preserve the primary install result; a later command will report a retained lock.
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
fs.unlinkSync(lock.path);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Preserve the primary install result; a later command will report a retained lock.
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export class PluginBootstrapV1 {
|
|
246
|
+
options;
|
|
247
|
+
platform;
|
|
248
|
+
architecture;
|
|
249
|
+
constructor(options) {
|
|
250
|
+
this.options = options;
|
|
251
|
+
this.platform = options.platform ?? process.platform;
|
|
252
|
+
this.architecture = options.architecture ?? process.arch;
|
|
253
|
+
}
|
|
254
|
+
async list() {
|
|
255
|
+
const receipt = this.validReceipt();
|
|
256
|
+
const entitlement = await this.options.backend.getLocalEntitlement();
|
|
257
|
+
const available = Boolean(receipt) && isEntitled(entitlement);
|
|
258
|
+
return this.result("plugin.list", receipt ? "current" : "not_installed", true, receipt, entitlement, {
|
|
259
|
+
plugins: OFFICIAL_PLUGINS.map((plugin) => ({
|
|
260
|
+
...plugin,
|
|
261
|
+
installed: Boolean(receipt),
|
|
262
|
+
available,
|
|
263
|
+
entitlement: entitlement.state,
|
|
264
|
+
})),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async status(channel = "stable") {
|
|
268
|
+
const receipt = this.validReceipt();
|
|
269
|
+
const entitlement = await this.options.backend.getLocalEntitlement();
|
|
270
|
+
if (!receipt)
|
|
271
|
+
return this.result("plugin.status", "not_installed", true, null, entitlement);
|
|
272
|
+
if (!isEntitled(entitlement))
|
|
273
|
+
return this.result("plugin.status", entitlement.state === "expired" ? "renewal_required" : "unavailable", true, receipt, entitlement);
|
|
274
|
+
try {
|
|
275
|
+
const access = await this.options.backend.resolveAccess({
|
|
276
|
+
bundleId: OFFICIAL_BUNDLE_ID,
|
|
277
|
+
installedVersion: receipt.version,
|
|
278
|
+
channel,
|
|
279
|
+
allowAuthentication: false,
|
|
280
|
+
});
|
|
281
|
+
if (access.kind !== "granted")
|
|
282
|
+
return this.result("plugin.status", "current", true, receipt, entitlement);
|
|
283
|
+
const release = await this.selectRelease(channel, access.artifactGrant);
|
|
284
|
+
if (release && compareVersions(release.manifest.version, receipt.version) > 0)
|
|
285
|
+
return this.result("plugin.status", "update_available", true, receipt, entitlement);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// Status stays useful offline. Install/update surfaces the detailed service error.
|
|
289
|
+
}
|
|
290
|
+
return this.result("plugin.status", "current", true, receipt, entitlement);
|
|
291
|
+
}
|
|
292
|
+
async install(options = {}) {
|
|
293
|
+
return this.reconcile("plugin.install", true, options);
|
|
294
|
+
}
|
|
295
|
+
async update(options = {}) {
|
|
296
|
+
if (!this.validReceipt()) {
|
|
297
|
+
return this.failure("plugin.update", "not_installed", await this.options.backend.getLocalEntitlement(), "plugin_not_installed", "AgentMemory Pro is not installed; run `agent-memory plugin install` first");
|
|
298
|
+
}
|
|
299
|
+
return this.reconcile("plugin.update", false, options);
|
|
300
|
+
}
|
|
301
|
+
async uninstall() {
|
|
302
|
+
const entitlement = await this.options.backend.getLocalEntitlement();
|
|
303
|
+
const removed = this.options.store.uninstall(OFFICIAL_BUNDLE_ID);
|
|
304
|
+
if (!removed)
|
|
305
|
+
return this.result("plugin.uninstall", "not_installed", true, null, entitlement);
|
|
306
|
+
return this.result("plugin.uninstall", "uninstalled", true, removed, entitlement, { version: null });
|
|
307
|
+
}
|
|
308
|
+
async manage() {
|
|
309
|
+
const receipt = this.validReceipt();
|
|
310
|
+
const entitlement = await this.options.backend.getLocalEntitlement();
|
|
311
|
+
const nextAction = await this.options.backend.getManagementAction();
|
|
312
|
+
if (!nextAction)
|
|
313
|
+
return this.failure("plugin.manage", "unavailable", entitlement, "service_not_configured", "AgentMemory account management is not configured in this build", receipt);
|
|
314
|
+
return this.result("plugin.manage", receipt ? "current" : "not_installed", true, receipt, entitlement, {
|
|
315
|
+
nextAction,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
async reconcile(command, allowAuthentication, options) {
|
|
319
|
+
const channel = options.channel ?? "stable";
|
|
320
|
+
const previous = this.validReceipt();
|
|
321
|
+
try {
|
|
322
|
+
const access = await this.options.backend.resolveAccess({
|
|
323
|
+
bundleId: OFFICIAL_BUNDLE_ID,
|
|
324
|
+
installedVersion: previous?.version,
|
|
325
|
+
channel,
|
|
326
|
+
allowAuthentication: allowAuthentication && options.allowAuthentication !== false,
|
|
327
|
+
});
|
|
328
|
+
if (access.kind === "auth_required" || access.kind === "renewal_required")
|
|
329
|
+
return this.result(command, access.kind, false, previous, access.entitlement, {
|
|
330
|
+
nextAction: access.nextAction,
|
|
331
|
+
error: {
|
|
332
|
+
code: access.kind,
|
|
333
|
+
message: access.nextAction.message ?? "User action is required before installation can continue",
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
if (access.kind === "unavailable")
|
|
337
|
+
return this.result(command, "unavailable", false, previous, access.entitlement, { error: access.error });
|
|
338
|
+
const release = await this.selectRelease(channel, access.artifactGrant);
|
|
339
|
+
if (!release)
|
|
340
|
+
return this.failure(command, "unavailable", access.entitlement, "compatible_release_not_found", "No signed AgentMemory Pro release is compatible with this core and platform", previous);
|
|
341
|
+
if (previous &&
|
|
342
|
+
previous.version === release.manifest.version &&
|
|
343
|
+
this.options.store.hasInstalledBundle(previous))
|
|
344
|
+
return this.result(command, "current", true, previous, access.entitlement);
|
|
345
|
+
this.options.verifier.verifyRelease(release);
|
|
346
|
+
const packageBytes = await this.options.backend.downloadArtifact({
|
|
347
|
+
release,
|
|
348
|
+
artifactGrant: access.artifactGrant,
|
|
349
|
+
});
|
|
350
|
+
const installed = await this.options.store.install(packageBytes, release, this.options.healthCheck);
|
|
351
|
+
return this.result(command, previous ? "upgraded" : "installed", true, installed, access.entitlement, {
|
|
352
|
+
previousVersion: previous?.version ?? null,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
const failure = normalizeFailure(error);
|
|
357
|
+
return this.failure(command, "unavailable", await this.options.backend.getLocalEntitlement(), failure.code, failure.message, previous, failure.retryable);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async selectRelease(channel, artifactGrant) {
|
|
361
|
+
const releases = await this.options.backend.listReleases({
|
|
362
|
+
bundleId: OFFICIAL_BUNDLE_ID,
|
|
363
|
+
channel,
|
|
364
|
+
artifactGrant,
|
|
365
|
+
});
|
|
366
|
+
const compatible = [];
|
|
367
|
+
let verificationFailure = null;
|
|
368
|
+
for (const release of releases) {
|
|
369
|
+
try {
|
|
370
|
+
validateRelease(release);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (release.manifest.id !== OFFICIAL_BUNDLE_ID ||
|
|
376
|
+
release.manifest.channel !== channel ||
|
|
377
|
+
release.manifest.pluginApi !== AGENT_MEMORY_PLUGIN_API_VERSION ||
|
|
378
|
+
(release.platform !== "any" && release.platform !== this.platform) ||
|
|
379
|
+
(release.architecture !== "any" && release.architecture !== this.architecture) ||
|
|
380
|
+
!supportsVersionRange(release.manifest.core, this.options.coreVersion))
|
|
381
|
+
continue;
|
|
382
|
+
try {
|
|
383
|
+
this.options.verifier.verifyRelease(release);
|
|
384
|
+
compatible.push(release);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
verificationFailure ??= normalizeFailure(error);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!compatible.length && verificationFailure)
|
|
391
|
+
throw verificationFailure;
|
|
392
|
+
return (compatible.sort((left, right) => compareVersions(right.manifest.version, left.manifest.version))[0] ?? null);
|
|
393
|
+
}
|
|
394
|
+
validReceipt() {
|
|
395
|
+
const receipt = this.options.store.readReceipt(OFFICIAL_BUNDLE_ID);
|
|
396
|
+
return receipt && this.options.store.hasInstalledBundle(receipt) ? receipt : null;
|
|
397
|
+
}
|
|
398
|
+
result(command, result, ok, receipt, entitlement, overrides = {}) {
|
|
399
|
+
return {
|
|
400
|
+
schemaVersion: 1,
|
|
401
|
+
command,
|
|
402
|
+
ok,
|
|
403
|
+
result,
|
|
404
|
+
bundle: receipt
|
|
405
|
+
? {
|
|
406
|
+
id: receipt.bundleId,
|
|
407
|
+
previousVersion: overrides.previousVersion ?? receipt.previousVersion ?? null,
|
|
408
|
+
version: overrides.version === undefined ? receipt.version : overrides.version,
|
|
409
|
+
channel: receipt.channel,
|
|
410
|
+
}
|
|
411
|
+
: null,
|
|
412
|
+
entitlement: structuredClone(entitlement),
|
|
413
|
+
...(overrides.plugins ? { plugins: overrides.plugins } : {}),
|
|
414
|
+
nextAction: overrides.nextAction ?? null,
|
|
415
|
+
...(overrides.error ? { error: overrides.error } : {}),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
failure(command, result, entitlement, code, message, receipt = null, retryable = false) {
|
|
419
|
+
return this.result(command, result, false, receipt, entitlement, {
|
|
420
|
+
error: { code, message, ...(retryable ? { retryable } : {}) },
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
export function createDefaultPluginBootstrap(coreVersion) {
|
|
425
|
+
const store = new FilePluginInstallStore();
|
|
426
|
+
const backend = new TemporaryPluginBackend({ root: store.root, coreVersion });
|
|
427
|
+
return new PluginBootstrapV1({
|
|
428
|
+
coreVersion,
|
|
429
|
+
backend,
|
|
430
|
+
verifier: new Ed25519ReleaseVerifier({ "agentmemory-temporary-2026-08": TEMPORARY_RELEASE_PUBLIC_KEY }),
|
|
431
|
+
store,
|
|
432
|
+
healthCheck: async (directory, release) => {
|
|
433
|
+
const { createInstalledBundleHealthCheck } = await import("./plugin-runtime.js");
|
|
434
|
+
await createInstalledBundleHealthCheck(coreVersion, backend, store.root)(directory, release);
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
export function getDefaultPluginInstallRoot() {
|
|
439
|
+
const override = process.env.AGENT_MEMORY_PLUGIN_DIR?.trim();
|
|
440
|
+
return path.resolve(override || path.join(os.homedir(), ".agent-memory", "system", "plugins"));
|
|
441
|
+
}
|
|
442
|
+
export function encodePluginPackage(packageValue) {
|
|
443
|
+
validatePackage(packageValue);
|
|
444
|
+
return Buffer.from(`${JSON.stringify(packageValue)}\n`, "utf-8");
|
|
445
|
+
}
|
|
446
|
+
export function releaseSigningPayload(release) {
|
|
447
|
+
return Buffer.from(JSON.stringify({
|
|
448
|
+
schemaVersion: release.schemaVersion,
|
|
449
|
+
manifest: release.manifest,
|
|
450
|
+
platform: release.platform,
|
|
451
|
+
architecture: release.architecture,
|
|
452
|
+
packageSha256: release.packageSha256,
|
|
453
|
+
size: release.size,
|
|
454
|
+
}), "utf-8");
|
|
455
|
+
}
|
|
456
|
+
export function sha256(value) {
|
|
457
|
+
return createHash("sha256").update(value).digest("hex");
|
|
458
|
+
}
|
|
459
|
+
export function compareVersions(left, right) {
|
|
460
|
+
const leftParts = parseVersion(left);
|
|
461
|
+
const rightParts = parseVersion(right);
|
|
462
|
+
for (let index = 0; index < 3; index++) {
|
|
463
|
+
if (leftParts[index] > rightParts[index])
|
|
464
|
+
return 1;
|
|
465
|
+
if (leftParts[index] < rightParts[index])
|
|
466
|
+
return -1;
|
|
467
|
+
}
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
export function supportsVersionRange(range, version) {
|
|
471
|
+
const clauses = range.trim().split(/\s+/).filter(Boolean);
|
|
472
|
+
if (!clauses.length)
|
|
473
|
+
return false;
|
|
474
|
+
return clauses.every((clause) => {
|
|
475
|
+
const match = /^(>=|>|<=|<|=)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(clause);
|
|
476
|
+
if (!match)
|
|
477
|
+
return false;
|
|
478
|
+
const comparison = compareVersions(version, match[2]);
|
|
479
|
+
switch (match[1] ?? "=") {
|
|
480
|
+
case ">=":
|
|
481
|
+
return comparison >= 0;
|
|
482
|
+
case ">":
|
|
483
|
+
return comparison > 0;
|
|
484
|
+
case "<=":
|
|
485
|
+
return comparison <= 0;
|
|
486
|
+
case "<":
|
|
487
|
+
return comparison < 0;
|
|
488
|
+
default:
|
|
489
|
+
return comparison === 0;
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
function validateRelease(release) {
|
|
494
|
+
if (release.schemaVersion !== 1)
|
|
495
|
+
throw new PluginBootstrapFailure("release_invalid", "Unsupported release schema");
|
|
496
|
+
validateBundleManifestV1(release.manifest);
|
|
497
|
+
if (!release.platform.trim() || !release.architecture.trim())
|
|
498
|
+
throw new PluginBootstrapFailure("release_invalid", "Release platform metadata is incomplete");
|
|
499
|
+
if (!/^[a-f0-9]{64}$/.test(release.packageSha256))
|
|
500
|
+
throw new PluginBootstrapFailure("release_invalid", "Release package digest is invalid");
|
|
501
|
+
if (!Number.isSafeInteger(release.size) || release.size <= 0 || release.size > PACKAGE_MAX_BYTES)
|
|
502
|
+
throw new PluginBootstrapFailure("release_invalid", "Release package size is invalid");
|
|
503
|
+
if (release.signature.algorithm !== "ed25519" || !release.signature.keyId.trim() || !release.signature.value.trim())
|
|
504
|
+
throw new PluginBootstrapFailure("release_invalid", "Release signature metadata is invalid");
|
|
505
|
+
}
|
|
506
|
+
function decodePluginPackage(packageBytes) {
|
|
507
|
+
if (packageBytes.byteLength > PACKAGE_MAX_BYTES)
|
|
508
|
+
throw new PluginBootstrapFailure("package_too_large", "The plugin package exceeds the compressed-size limit");
|
|
509
|
+
let value;
|
|
510
|
+
try {
|
|
511
|
+
value = JSON.parse(Buffer.from(packageBytes).toString("utf-8"));
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
throw new PluginBootstrapFailure("package_invalid", "The plugin package is not valid JSON");
|
|
515
|
+
}
|
|
516
|
+
validatePackage(value);
|
|
517
|
+
return value;
|
|
518
|
+
}
|
|
519
|
+
function validatePackage(value) {
|
|
520
|
+
if (value.schemaVersion !== 1)
|
|
521
|
+
throw new PluginBootstrapFailure("package_invalid", "Unsupported plugin package schema");
|
|
522
|
+
validateBundleManifestV1(value.manifest);
|
|
523
|
+
if (!Array.isArray(value.files) || !value.files.length || value.files.length > PACKAGE_MAX_FILES)
|
|
524
|
+
throw new PluginBootstrapFailure("package_invalid", "The plugin package file count is invalid");
|
|
525
|
+
const paths = new Set();
|
|
526
|
+
let expandedSize = 0;
|
|
527
|
+
for (const file of value.files) {
|
|
528
|
+
if (!isSafeBundlePath(file.path))
|
|
529
|
+
throw new PluginBootstrapFailure("package_path_invalid", "Plugin package path is unsafe");
|
|
530
|
+
if (paths.has(file.path))
|
|
531
|
+
throw new PluginBootstrapFailure("package_path_duplicate", "Plugin package contains duplicate paths");
|
|
532
|
+
paths.add(file.path);
|
|
533
|
+
if (!/^[a-f0-9]{64}$/.test(file.sha256))
|
|
534
|
+
throw new PluginBootstrapFailure("package_invalid", `Plugin package digest is invalid for ${file.path}`);
|
|
535
|
+
const content = decodeBase64Strict(file.contentBase64, `content for ${file.path}`);
|
|
536
|
+
expandedSize += content.byteLength;
|
|
537
|
+
if (expandedSize > PACKAGE_MAX_EXPANDED_BYTES)
|
|
538
|
+
throw new PluginBootstrapFailure("package_too_large", "The plugin package exceeds the expanded-size limit");
|
|
539
|
+
if (sha256(content) !== file.sha256)
|
|
540
|
+
throw new PluginBootstrapFailure("package_file_digest_mismatch", `Plugin package file digest failed for ${file.path}`);
|
|
541
|
+
}
|
|
542
|
+
if (!paths.has(value.manifest.entrypoint))
|
|
543
|
+
throw new PluginBootstrapFailure("package_entrypoint_missing", "The plugin package does not contain its entrypoint");
|
|
544
|
+
}
|
|
545
|
+
function extractPackage(packageValue, stagingDirectory) {
|
|
546
|
+
fs.mkdirSync(stagingDirectory, { recursive: false, mode: 0o700 });
|
|
547
|
+
for (const file of packageValue.files) {
|
|
548
|
+
const target = resolveManagedPath(stagingDirectory, file.path);
|
|
549
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
550
|
+
fs.writeFileSync(target, decodeBase64Strict(file.contentBase64, `content for ${file.path}`), {
|
|
551
|
+
mode: file.executable ? 0o755 : 0o644,
|
|
552
|
+
flag: "wx",
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async function defaultHealthCheck(directory, release) {
|
|
557
|
+
const entrypoint = resolveManagedPath(directory, release.manifest.entrypoint);
|
|
558
|
+
const stat = fs.lstatSync(entrypoint);
|
|
559
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
560
|
+
throw new PluginBootstrapFailure("health_check_failed", "The plugin entrypoint is not a regular file");
|
|
561
|
+
}
|
|
562
|
+
function validateReceipt(receipt, expectedBundleId) {
|
|
563
|
+
if (receipt.schemaVersion !== 1 || receipt.bundleId !== expectedBundleId)
|
|
564
|
+
throw new PluginBootstrapFailure("receipt_invalid", "The plugin install receipt has the wrong identity");
|
|
565
|
+
validateId(receipt.bundleId, "bundle");
|
|
566
|
+
validateVersion(receipt.version);
|
|
567
|
+
if (!receipt.channel.trim() || receipt.pluginApi !== AGENT_MEMORY_PLUGIN_API_VERSION)
|
|
568
|
+
throw new PluginBootstrapFailure("receipt_invalid", "The plugin install receipt is incompatible");
|
|
569
|
+
if (!isSafeBundlePath(receipt.entrypoint) || !/^[a-f0-9]{64}$/.test(receipt.packageSha256))
|
|
570
|
+
throw new PluginBootstrapFailure("receipt_invalid", "The plugin install receipt contains invalid paths or digests");
|
|
571
|
+
}
|
|
572
|
+
function validateId(value, label) {
|
|
573
|
+
if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(value))
|
|
574
|
+
throw new PluginBootstrapFailure(`${label}_id_invalid`, `Invalid ${label} id: ${value}`);
|
|
575
|
+
}
|
|
576
|
+
function validateVersion(value) {
|
|
577
|
+
parseVersion(value);
|
|
578
|
+
}
|
|
579
|
+
function parseVersion(value) {
|
|
580
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(value);
|
|
581
|
+
if (!match)
|
|
582
|
+
throw new PluginBootstrapFailure("version_invalid", `Invalid semantic version: ${value}`);
|
|
583
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
584
|
+
}
|
|
585
|
+
function resolveManagedPath(root, relative) {
|
|
586
|
+
if (!isSafeBundlePath(relative))
|
|
587
|
+
throw new PluginBootstrapFailure("path_invalid", "Managed plugin path is unsafe");
|
|
588
|
+
const resolvedRoot = path.resolve(root);
|
|
589
|
+
const resolved = path.resolve(resolvedRoot, ...relative.split("/"));
|
|
590
|
+
if (resolved === resolvedRoot || !resolved.startsWith(`${resolvedRoot}${path.sep}`))
|
|
591
|
+
throw new PluginBootstrapFailure("path_invalid", "Managed plugin path escapes its root");
|
|
592
|
+
return resolved;
|
|
593
|
+
}
|
|
594
|
+
function ensureRegularDirectory(directory, recursive = false) {
|
|
595
|
+
if (!fs.existsSync(directory))
|
|
596
|
+
fs.mkdirSync(directory, { recursive, mode: 0o700 });
|
|
597
|
+
const stat = fs.lstatSync(directory);
|
|
598
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
599
|
+
throw new PluginBootstrapFailure("install_root_invalid", "The plugin install root and its managed directories must be regular directories");
|
|
600
|
+
}
|
|
601
|
+
function removeManagedDirectory(root, target) {
|
|
602
|
+
const resolvedRoot = path.resolve(root);
|
|
603
|
+
const resolvedTarget = path.resolve(target);
|
|
604
|
+
if (resolvedTarget === resolvedRoot || !resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`))
|
|
605
|
+
throw new PluginBootstrapFailure("path_invalid", "Refusing to remove a path outside the plugin install root");
|
|
606
|
+
const stat = fs.lstatSync(resolvedTarget);
|
|
607
|
+
if (stat.isSymbolicLink())
|
|
608
|
+
throw new PluginBootstrapFailure("path_invalid", "Refusing to remove a symbolic link");
|
|
609
|
+
if (!stat.isDirectory())
|
|
610
|
+
throw new PluginBootstrapFailure("path_invalid", "Managed removal target is not a directory");
|
|
611
|
+
fs.rmSync(resolvedTarget, { recursive: true, force: false });
|
|
612
|
+
}
|
|
613
|
+
function decodeBase64Strict(value, label) {
|
|
614
|
+
if (!value || !/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0)
|
|
615
|
+
throw new PluginBootstrapFailure("base64_invalid", `Invalid base64 ${label}`);
|
|
616
|
+
const decoded = Buffer.from(value, "base64");
|
|
617
|
+
if (decoded.toString("base64") !== value)
|
|
618
|
+
throw new PluginBootstrapFailure("base64_invalid", `Non-canonical base64 ${label}`);
|
|
619
|
+
return decoded;
|
|
620
|
+
}
|
|
621
|
+
function isEntitled(entitlement) {
|
|
622
|
+
return entitlement.state === "active" || entitlement.state === "grace";
|
|
623
|
+
}
|
|
624
|
+
function normalizeFailure(error) {
|
|
625
|
+
if (error instanceof PluginBootstrapFailure)
|
|
626
|
+
return error;
|
|
627
|
+
return new PluginBootstrapFailure("plugin_install_failed", error instanceof Error ? error.message : String(error));
|
|
628
|
+
}
|