myagentmemory 0.4.16 → 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.
@@ -1,326 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { pathToFileURL } from "node:url";
5
-
6
- import { getMemoryDir, memoryWrite, redactSecrets, scheduleQmdUpdate } from "./core.js";
7
- import {
8
- FilePluginInstallStore,
9
- OFFICIAL_BUNDLE_ID,
10
- type PluginBootstrapBackendV1,
11
- PluginBootstrapFailure,
12
- type PluginInstallReceiptV1,
13
- type PluginInstallStoreV1,
14
- type PluginSessionUsageDecisionV1,
15
- type SignedPluginReleaseV1,
16
- } from "./plugin-bootstrap.js";
17
- import {
18
- AGENT_MEMORY_PLUGIN_API_VERSION,
19
- type AgentMemoryPluginBundleV1,
20
- type AgentMemoryPluginHostV1,
21
- type AgentMemoryPluginManifestV1,
22
- isPluginCapabilityEnabled,
23
- type PluginCommandContextV1,
24
- type PluginCommandResultV1,
25
- type PluginCommandV1,
26
- type PluginEntitlementStatusV1,
27
- type PluginMemoryCorrectionV1,
28
- type PluginMemoryWriteV1,
29
- type PluginSessionStartHookV1,
30
- validateBundleManifestV1,
31
- validatePluginEntitlementStatusV1,
32
- validatePluginManifestV1,
33
- } from "./plugin-host.js";
34
- import { TemporaryPluginBackend } from "./plugin-service.js";
35
-
36
- interface RegisteredCommand {
37
- command: PluginCommandV1;
38
- pluginId: string;
39
- }
40
-
41
- export interface PluginRuntimeOptionsV1 {
42
- coreVersion: string;
43
- store?: PluginInstallStoreV1;
44
- backend?: PluginBootstrapBackendV1;
45
- }
46
-
47
- function assertPermission(manifest: AgentMemoryPluginManifestV1, permission: string): void {
48
- if (!manifest.permissions.includes(permission as never))
49
- throw new PluginBootstrapFailure(
50
- "plugin_permission_denied",
51
- `Plugin ${manifest.id} did not declare permission ${permission}`,
52
- );
53
- }
54
-
55
- function memoryResult(result: Awaited<ReturnType<typeof memoryWrite>>): {
56
- ok: boolean;
57
- path: string;
58
- redacted: boolean;
59
- } {
60
- if (result.isError)
61
- throw new PluginBootstrapFailure("plugin_memory_write_failed", result.text.replace(/^Error:\s*/, ""));
62
- return {
63
- ok: true,
64
- path: typeof result.details.path === "string" ? result.details.path : getMemoryDir(),
65
- redacted: result.details.redacted === true,
66
- };
67
- }
68
-
69
- async function importBundle(
70
- directory: string,
71
- receipt: Pick<PluginInstallReceiptV1, "entrypoint" | "bundleId" | "version">,
72
- ): Promise<AgentMemoryPluginBundleV1> {
73
- let component = path.resolve(directory);
74
- const rootStat = fs.lstatSync(component);
75
- if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
76
- throw new PluginBootstrapFailure("plugin_entrypoint_invalid", "The installed plugin directory is unsafe");
77
- for (const part of receipt.entrypoint.split("/")) {
78
- component = path.join(component, part);
79
- const componentStat = fs.lstatSync(component);
80
- if (componentStat.isSymbolicLink())
81
- throw new PluginBootstrapFailure(
82
- "plugin_entrypoint_invalid",
83
- "The installed plugin path contains a symbolic link",
84
- );
85
- }
86
- const entrypoint = path.resolve(directory, ...receipt.entrypoint.split("/"));
87
- if (!entrypoint.startsWith(`${path.resolve(directory)}${path.sep}`))
88
- throw new PluginBootstrapFailure(
89
- "plugin_entrypoint_invalid",
90
- "The installed plugin entrypoint escapes its bundle",
91
- );
92
- const stat = fs.lstatSync(entrypoint);
93
- if (!stat.isFile() || stat.isSymbolicLink())
94
- throw new PluginBootstrapFailure(
95
- "plugin_entrypoint_invalid",
96
- "The installed plugin entrypoint is not a regular file",
97
- );
98
- const imported = (await import(`${pathToFileURL(entrypoint).href}?v=${encodeURIComponent(receipt.version)}`)) as {
99
- default?: unknown;
100
- };
101
- const bundle = imported.default as AgentMemoryPluginBundleV1 | undefined;
102
- if (!bundle || bundle.apiVersion !== AGENT_MEMORY_PLUGIN_API_VERSION || !Array.isArray(bundle.plugins))
103
- throw new PluginBootstrapFailure(
104
- "plugin_bundle_invalid",
105
- "The plugin entrypoint did not export a compatible bundle",
106
- );
107
- validateBundleManifestV1(bundle.manifest);
108
- if (bundle.manifest.id !== receipt.bundleId || bundle.manifest.version !== receipt.version)
109
- throw new PluginBootstrapFailure(
110
- "plugin_bundle_invalid",
111
- "The loaded plugin bundle identity does not match its receipt",
112
- );
113
- const pluginIds = new Set<string>();
114
- for (const plugin of bundle.plugins) {
115
- validatePluginManifestV1(plugin.manifest);
116
- if (pluginIds.has(plugin.manifest.id))
117
- throw new PluginBootstrapFailure("plugin_bundle_invalid", "The plugin bundle contains duplicate plugin ids");
118
- pluginIds.add(plugin.manifest.id);
119
- }
120
- if (
121
- pluginIds.size !== bundle.manifest.plugins.length ||
122
- bundle.manifest.plugins.some((pluginId) => !pluginIds.has(pluginId))
123
- )
124
- throw new PluginBootstrapFailure("plugin_bundle_invalid", "The plugin bundle contents do not match its manifest");
125
- return bundle;
126
- }
127
-
128
- export class InstalledPluginRuntimeV1 {
129
- private readonly store: PluginInstallStoreV1;
130
- private readonly backend: PluginBootstrapBackendV1;
131
- private readonly commands = new Map<string, RegisteredCommand>();
132
- private readonly hooks: PluginSessionStartHookV1[] = [];
133
- private loaded = false;
134
-
135
- constructor(private readonly options: PluginRuntimeOptionsV1) {
136
- this.store = options.store ?? new FilePluginInstallStore();
137
- this.backend = options.backend ?? new TemporaryPluginBackend({ root: this.store.root });
138
- }
139
-
140
- async load(): Promise<boolean> {
141
- if (this.loaded) return true;
142
- const receipt = this.store.readReceipt(OFFICIAL_BUNDLE_ID);
143
- if (!receipt || !this.store.hasInstalledBundle(receipt)) return false;
144
- await this.refreshEntitlement();
145
- const directory = path.join(this.store.root, "bundles", receipt.bundleId, receipt.version);
146
- const bundle = await importBundle(directory, receipt);
147
- for (const plugin of bundle.plugins) {
148
- const host = this.createHost(plugin.manifest);
149
- await plugin.activate(host);
150
- const health = await plugin.healthCheck(host);
151
- if (!health.ok)
152
- throw new PluginBootstrapFailure(
153
- "plugin_health_check_failed",
154
- health.message ?? `Plugin ${plugin.manifest.id} failed its health check`,
155
- );
156
- }
157
- this.loaded = true;
158
- return true;
159
- }
160
-
161
- async run(name: string, context: PluginCommandContextV1): Promise<PluginCommandResultV1 | null> {
162
- if (!(await this.load())) return null;
163
- const registered = this.commands.get(name);
164
- if (!registered) return null;
165
- const entitlement = await this.refreshEntitlement();
166
- if (!isPluginCapabilityEnabled(entitlement, registered.command.requiredCapability))
167
- return {
168
- ok: false,
169
- error: {
170
- code: "plugin_capability_denied",
171
- message: `Capability ${registered.command.requiredCapability} is not enabled for ${registered.pluginId}`,
172
- },
173
- };
174
- return registered.command.run(context);
175
- }
176
-
177
- async runSessionStart(
178
- context: Parameters<PluginSessionStartHookV1["run"]>[0],
179
- ): Promise<PluginSessionUsageDecisionV1 | null> {
180
- if (!(await this.load()) || this.hooks.length === 0) return null;
181
- const entitlement = await this.refreshEntitlement();
182
- const eligible = this.hooks.filter((hook) => isPluginCapabilityEnabled(entitlement, hook.requiredCapability));
183
- if (eligible.length === 0) return null;
184
- const metered = eligible.some(
185
- (hook) => entitlement.capabilities[hook.requiredCapability]?.quota?.scope === "account",
186
- );
187
- if (!metered) {
188
- for (const hook of eligible) await hook.run(context);
189
- return null;
190
- }
191
- if (!this.backend.reserveSession || !this.backend.commitSession || !this.backend.releaseSession)
192
- throw new PluginBootstrapFailure("session_usage_unavailable", "Account session metering is unavailable");
193
- const operationId = randomUUID();
194
- const reservation = await this.backend.reserveSession(operationId);
195
- if (!reservation.allowed) return reservation;
196
- try {
197
- for (const hook of eligible) await hook.run(context);
198
- return await this.backend.commitSession(operationId);
199
- } catch (error) {
200
- await this.backend.releaseSession(operationId);
201
- throw error;
202
- }
203
- }
204
-
205
- private createHost(manifest: AgentMemoryPluginManifestV1): AgentMemoryPluginHostV1 {
206
- const descriptors = new Map(manifest.commands.map((command) => [command.name, command]));
207
- const stateRoot = path.join(this.store.root, "state");
208
- fs.mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
209
- const stateRootStat = fs.lstatSync(stateRoot);
210
- if (!stateRootStat.isDirectory() || stateRootStat.isSymbolicLink())
211
- throw new PluginBootstrapFailure("plugin_state_invalid", "The plugin state root is unsafe");
212
- const stateDirectory = path.join(stateRoot, OFFICIAL_BUNDLE_ID);
213
- if (!fs.existsSync(stateDirectory)) fs.mkdirSync(stateDirectory, { mode: 0o700 });
214
- const stateDirectoryStat = fs.lstatSync(stateDirectory);
215
- if (!stateDirectoryStat.isDirectory() || stateDirectoryStat.isSymbolicLink())
216
- throw new PluginBootstrapFailure("plugin_state_invalid", "The plugin state directory is unsafe");
217
- return {
218
- apiVersion: AGENT_MEMORY_PLUGIN_API_VERSION,
219
- coreVersion: this.options.coreVersion,
220
- registerCommand: (command) => {
221
- const descriptor = descriptors.get(command.name);
222
- if (!descriptor || descriptor.requiredCapability !== command.requiredCapability)
223
- throw new PluginBootstrapFailure(
224
- "plugin_command_invalid",
225
- `Plugin ${manifest.id} registered an undeclared command`,
226
- );
227
- const names = [command.name, ...(command.aliases ?? [])];
228
- for (const name of names) {
229
- if (this.commands.has(name))
230
- throw new PluginBootstrapFailure(
231
- "plugin_command_conflict",
232
- `Plugin command ${name} is already registered`,
233
- );
234
- }
235
- for (const name of names) this.commands.set(name, { command, pluginId: manifest.id });
236
- },
237
- registerSessionStartHook: (hook) => {
238
- if (!(manifest.capabilities ?? []).includes(hook.requiredCapability))
239
- throw new PluginBootstrapFailure(
240
- "plugin_hook_invalid",
241
- `Plugin ${manifest.id} registered a hook with an undeclared capability`,
242
- );
243
- this.hooks.push(hook);
244
- },
245
- getStateDirectory: () => stateDirectory,
246
- getMemoryDirectory: () => {
247
- assertPermission(manifest, "memory:read");
248
- return getMemoryDir();
249
- },
250
- getEntitlement: async () => structuredClone(await this.refreshEntitlement()),
251
- redactSecrets: (value) => redactSecrets(value).content,
252
- writeMemory: async (request: PluginMemoryWriteV1) => {
253
- assertPermission(manifest, "memory:write");
254
- return memoryResult(await memoryWrite({ ...request, sessionId: `plugin-${manifest.id}` }));
255
- },
256
- correctMemory: async (request: PluginMemoryCorrectionV1) => {
257
- assertPermission(manifest, "memory:correct");
258
- const content = `Correction for ${request.artifactId}: ${request.content}${request.reason ? `\nReason: ${request.reason}` : ""}`;
259
- return memoryResult(
260
- await memoryWrite({
261
- target: request.scope === "durable" ? "long_term" : "daily",
262
- content,
263
- sessionId: `plugin-${manifest.id}`,
264
- sourceUri: request.sourceUri,
265
- }),
266
- );
267
- },
268
- scheduleSearchRefresh: () => {
269
- assertPermission(manifest, "jobs:run");
270
- scheduleQmdUpdate();
271
- },
272
- };
273
- }
274
-
275
- private async refreshEntitlement(): Promise<PluginEntitlementStatusV1> {
276
- const entitlement = await this.backend.getLocalEntitlement();
277
- validatePluginEntitlementStatusV1(entitlement);
278
- return entitlement;
279
- }
280
- }
281
-
282
- export function createInstalledBundleHealthCheck(
283
- coreVersion: string,
284
- backend: PluginBootstrapBackendV1,
285
- storeRoot: string,
286
- ): (directory: string, release: SignedPluginReleaseV1) => Promise<void> {
287
- return async (directory, release) => {
288
- const entitlement = await backend.getLocalEntitlement();
289
- validatePluginEntitlementStatusV1(entitlement);
290
- const receipt = {
291
- bundleId: release.manifest.id,
292
- version: release.manifest.version,
293
- entrypoint: release.manifest.entrypoint,
294
- };
295
- const bundle = await importBundle(directory, receipt);
296
- for (const plugin of bundle.plugins) {
297
- const stateDirectory = path.join(storeRoot, "health", OFFICIAL_BUNDLE_ID);
298
- const host: AgentMemoryPluginHostV1 = {
299
- apiVersion: 1,
300
- coreVersion,
301
- registerCommand() {},
302
- registerSessionStartHook() {},
303
- getStateDirectory: () => stateDirectory,
304
- getMemoryDirectory: () => {
305
- assertPermission(plugin.manifest, "memory:read");
306
- return getMemoryDir();
307
- },
308
- getEntitlement: async () => structuredClone(entitlement),
309
- redactSecrets: (value) => redactSecrets(value).content,
310
- async writeMemory() {
311
- throw new PluginBootstrapFailure("plugin_health_check_invalid", "Health checks cannot write memory");
312
- },
313
- async correctMemory() {
314
- throw new PluginBootstrapFailure("plugin_health_check_invalid", "Health checks cannot correct memory");
315
- },
316
- scheduleSearchRefresh() {},
317
- };
318
- const health = await plugin.healthCheck(host);
319
- if (!health.ok)
320
- throw new PluginBootstrapFailure(
321
- "plugin_health_check_failed",
322
- health.message ?? `Plugin ${plugin.manifest.id} failed its health check`,
323
- );
324
- }
325
- };
326
- }