myagentmemory 0.4.13 → 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,255 @@
1
+ export const AGENT_MEMORY_PLUGIN_API_VERSION = 1 as const;
2
+
3
+ export type PluginPermissionV1 =
4
+ | "memory:read"
5
+ | "memory:write"
6
+ | "memory:correct"
7
+ | "config:read"
8
+ | "config:write"
9
+ | "jobs:run"
10
+ | "sessions:read"
11
+ | "state:write";
12
+
13
+ export type PluginEntitlementStateV1 = "active" | "grace" | "missing" | "expired";
14
+
15
+ export type PluginPlanV1 = "free" | "trial" | "pro" | "team" | "enterprise";
16
+
17
+ export interface PluginCapabilityQuotaV1 {
18
+ limit: number;
19
+ window: "day";
20
+ scope: "device";
21
+ }
22
+
23
+ export interface PluginCapabilityGrantV1 {
24
+ enabled: boolean;
25
+ quota?: PluginCapabilityQuotaV1;
26
+ }
27
+
28
+ export interface PluginEntitlementStatusV1 {
29
+ plan: PluginPlanV1 | null;
30
+ state: PluginEntitlementStateV1;
31
+ features: string[];
32
+ capabilities: Record<string, PluginCapabilityGrantV1>;
33
+ expiresAt?: string;
34
+ offlineUntil?: string;
35
+ reason?: string;
36
+ }
37
+
38
+ export interface PluginCommandDescriptorV1 {
39
+ name: string;
40
+ description: string;
41
+ aliases?: string[];
42
+ requiredCapability: string;
43
+ }
44
+
45
+ export interface AgentMemoryPluginManifestV1 {
46
+ schemaVersion: 1;
47
+ id: string;
48
+ name: string;
49
+ version: string;
50
+ description: string;
51
+ engine: string;
52
+ entitlement: "commercial";
53
+ commands: PluginCommandDescriptorV1[];
54
+ permissions: PluginPermissionV1[];
55
+ requires?: string[];
56
+ capabilities?: string[];
57
+ optionalCapabilities?: string[];
58
+ }
59
+
60
+ export interface AgentMemoryBundleManifestV1 {
61
+ schemaVersion: 1;
62
+ id: string;
63
+ version: string;
64
+ channel: string;
65
+ core: string;
66
+ pluginApi: 1;
67
+ entrypoint: string;
68
+ plugins: string[];
69
+ }
70
+
71
+ export interface PluginCommandContextV1 {
72
+ args: string[];
73
+ flags: Record<string, string | boolean>;
74
+ signal: AbortSignal;
75
+ }
76
+
77
+ export interface PluginCommandResultV1 {
78
+ ok: boolean;
79
+ data?: unknown;
80
+ error?: PluginStructuredErrorV1;
81
+ }
82
+
83
+ export interface PluginCommandV1 extends PluginCommandDescriptorV1 {
84
+ run(context: PluginCommandContextV1): Promise<PluginCommandResultV1>;
85
+ }
86
+
87
+ export interface PluginSessionStartContextV1 {
88
+ host: string;
89
+ cwd?: string;
90
+ signal: AbortSignal;
91
+ }
92
+
93
+ export interface PluginSessionStartHookV1 {
94
+ name: string;
95
+ requiredCapability: string;
96
+ run(context: PluginSessionStartContextV1): Promise<void>;
97
+ }
98
+
99
+ export interface PluginMemoryWriteV1 {
100
+ target: "long_term" | "daily" | "topic";
101
+ content: string;
102
+ mode?: "append" | "overwrite";
103
+ topic?: string;
104
+ date?: string;
105
+ sourceUri?: string;
106
+ }
107
+
108
+ export interface PluginMemoryWriteResultV1 {
109
+ ok: boolean;
110
+ path: string;
111
+ redacted: boolean;
112
+ }
113
+
114
+ export interface PluginMemoryCorrectionV1 {
115
+ artifactId: string;
116
+ scope: "daily" | "durable";
117
+ content: string;
118
+ reason?: string;
119
+ sourceUri?: string;
120
+ }
121
+
122
+ export interface PluginMemoryCorrectionResultV1 {
123
+ ok: boolean;
124
+ path: string;
125
+ redacted: boolean;
126
+ }
127
+
128
+ export interface PluginStructuredErrorV1 {
129
+ code: string;
130
+ message: string;
131
+ retryable?: boolean;
132
+ }
133
+
134
+ export interface AgentMemoryPluginHostV1 {
135
+ apiVersion: 1;
136
+ coreVersion: string;
137
+ registerCommand(command: PluginCommandV1): void;
138
+ registerSessionStartHook(hook: PluginSessionStartHookV1): void;
139
+ getStateDirectory(): string;
140
+ getMemoryDirectory(): string;
141
+ getEntitlement(): Promise<PluginEntitlementStatusV1>;
142
+ redactSecrets(value: string): string;
143
+ writeMemory(request: PluginMemoryWriteV1): Promise<PluginMemoryWriteResultV1>;
144
+ correctMemory(request: PluginMemoryCorrectionV1): Promise<PluginMemoryCorrectionResultV1>;
145
+ scheduleSearchRefresh(reason: string): void;
146
+ }
147
+
148
+ export interface AgentMemoryPluginV1 {
149
+ manifest: AgentMemoryPluginManifestV1;
150
+ activate(host: AgentMemoryPluginHostV1): Promise<void>;
151
+ healthCheck(host: AgentMemoryPluginHostV1): Promise<{ ok: boolean; message?: string }>;
152
+ }
153
+
154
+ export interface AgentMemoryPluginBundleV1 {
155
+ apiVersion: 1;
156
+ manifest: AgentMemoryBundleManifestV1;
157
+ plugins: readonly AgentMemoryPluginV1[];
158
+ }
159
+
160
+ const PLUGIN_ID = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
161
+ const COMMAND_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
162
+ const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
163
+ const PLANS = new Set<PluginPlanV1>(["free", "trial", "pro", "team", "enterprise"]);
164
+ const ENTITLEMENT_STATES = new Set<PluginEntitlementStateV1>(["active", "grace", "missing", "expired"]);
165
+
166
+ export function validatePluginManifestV1(manifest: AgentMemoryPluginManifestV1): void {
167
+ if (manifest.schemaVersion !== 1) throw new Error(`Unsupported plugin manifest schema for ${manifest.id}`);
168
+ if (!PLUGIN_ID.test(manifest.id)) throw new Error(`Invalid plugin id: ${manifest.id}`);
169
+ if (!manifest.name.trim() || !manifest.description.trim())
170
+ throw new Error(`Plugin manifest ${manifest.id} is incomplete`);
171
+ if (!SEMVER.test(manifest.version))
172
+ throw new Error(`Invalid plugin version for ${manifest.id}: ${manifest.version}`);
173
+ if (!manifest.engine.trim()) throw new Error(`Plugin manifest ${manifest.id} has no engine range`);
174
+
175
+ const commands = new Set<string>();
176
+ const capabilities = new Set(manifest.capabilities ?? []);
177
+ if (capabilities.size !== (manifest.capabilities?.length ?? 0))
178
+ throw new Error(`Plugin manifest ${manifest.id} must declare unique capabilities`);
179
+ for (const capability of capabilities) {
180
+ if (!PLUGIN_ID.test(capability)) throw new Error(`Invalid capability '${capability}' in ${manifest.id}`);
181
+ }
182
+ for (const command of manifest.commands) {
183
+ if (!COMMAND_NAME.test(command.name)) throw new Error(`Invalid command '${command.name}' in ${manifest.id}`);
184
+ if (!command.description.trim())
185
+ throw new Error(`Command '${command.name}' in ${manifest.id} has no description`);
186
+ if (commands.has(command.name)) throw new Error(`Duplicate command '${command.name}' in ${manifest.id}`);
187
+ if (!capabilities.has(command.requiredCapability))
188
+ throw new Error(
189
+ `Command '${command.name}' in ${manifest.id} requires undeclared capability '${command.requiredCapability}'`,
190
+ );
191
+ commands.add(command.name);
192
+ }
193
+
194
+ for (const dependency of manifest.requires ?? []) {
195
+ if (!PLUGIN_ID.test(dependency)) throw new Error(`Invalid dependency '${dependency}' in ${manifest.id}`);
196
+ if (dependency === manifest.id) throw new Error(`Plugin ${manifest.id} cannot require itself`);
197
+ }
198
+ }
199
+
200
+ export function validatePluginEntitlementStatusV1(
201
+ entitlement: unknown,
202
+ ): asserts entitlement is PluginEntitlementStatusV1 {
203
+ if (!entitlement || typeof entitlement !== "object" || Array.isArray(entitlement))
204
+ throw new Error("Plugin entitlement must be an object");
205
+ const candidate = entitlement as Partial<PluginEntitlementStatusV1>;
206
+ if (candidate.plan !== null && !PLANS.has(candidate.plan as PluginPlanV1))
207
+ throw new Error("Plugin entitlement has an invalid plan");
208
+ if (!ENTITLEMENT_STATES.has(candidate.state as PluginEntitlementStateV1))
209
+ throw new Error("Plugin entitlement has an invalid state");
210
+ if (!Array.isArray(candidate.features) || candidate.features.some((feature) => typeof feature !== "string"))
211
+ throw new Error("Plugin entitlement features must be strings");
212
+ if (!candidate.capabilities || typeof candidate.capabilities !== "object" || Array.isArray(candidate.capabilities))
213
+ throw new Error("Plugin entitlement capabilities must be an object");
214
+ for (const [capability, grant] of Object.entries(candidate.capabilities)) {
215
+ if (!PLUGIN_ID.test(capability)) throw new Error(`Invalid entitlement capability: ${capability}`);
216
+ if (!grant || typeof grant !== "object" || Array.isArray(grant) || typeof grant.enabled !== "boolean")
217
+ throw new Error(`Capability ${capability} has an invalid grant`);
218
+ if (grant.quota) {
219
+ if (!Number.isInteger(grant.quota.limit) || grant.quota.limit <= 0)
220
+ throw new Error(`Capability ${capability} has an invalid quota limit`);
221
+ if (grant.quota.window !== "day" || grant.quota.scope !== "device")
222
+ throw new Error(`Capability ${capability} has an invalid quota policy`);
223
+ }
224
+ }
225
+ }
226
+
227
+ export function isPluginCapabilityEnabled(entitlement: PluginEntitlementStatusV1, capability: string): boolean {
228
+ return (
229
+ (entitlement.state === "active" || entitlement.state === "grace") &&
230
+ entitlement.capabilities?.[capability]?.enabled === true
231
+ );
232
+ }
233
+
234
+ export function validateBundleManifestV1(manifest: AgentMemoryBundleManifestV1): void {
235
+ if (manifest.schemaVersion !== 1) throw new Error(`Unsupported bundle manifest schema for ${manifest.id}`);
236
+ if (!PLUGIN_ID.test(manifest.id)) throw new Error(`Invalid bundle id: ${manifest.id}`);
237
+ if (!SEMVER.test(manifest.version))
238
+ throw new Error(`Invalid bundle version for ${manifest.id}: ${manifest.version}`);
239
+ if (manifest.pluginApi !== AGENT_MEMORY_PLUGIN_API_VERSION)
240
+ throw new Error(`Unsupported plugin API ${manifest.pluginApi} for ${manifest.id}`);
241
+ if (!manifest.channel.trim() || !manifest.core.trim())
242
+ throw new Error(`Bundle manifest ${manifest.id} is incomplete`);
243
+ if (!isSafeBundlePath(manifest.entrypoint)) throw new Error(`Invalid bundle entrypoint: ${manifest.entrypoint}`);
244
+ if (!manifest.plugins.length || new Set(manifest.plugins).size !== manifest.plugins.length)
245
+ throw new Error(`Bundle manifest ${manifest.id} must list unique plugins`);
246
+ for (const pluginId of manifest.plugins) {
247
+ if (!PLUGIN_ID.test(pluginId)) throw new Error(`Invalid plugin id in bundle ${manifest.id}: ${pluginId}`);
248
+ }
249
+ }
250
+
251
+ export function isSafeBundlePath(value: string): boolean {
252
+ if (!value || value.includes("\\") || value.includes("\0") || value.startsWith("/")) return false;
253
+ const parts = value.split("/");
254
+ return parts.every((part) => /^[0-9A-Za-z@+._-]+$/.test(part) && part !== "." && part !== "..");
255
+ }
@@ -0,0 +1,296 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ import { getMemoryDir, memoryWrite, redactSecrets, scheduleQmdUpdate } from "./core.js";
6
+ import {
7
+ FilePluginInstallStore,
8
+ OFFICIAL_BUNDLE_ID,
9
+ type PluginBootstrapBackendV1,
10
+ PluginBootstrapFailure,
11
+ type PluginInstallReceiptV1,
12
+ type PluginInstallStoreV1,
13
+ type SignedPluginReleaseV1,
14
+ } from "./plugin-bootstrap.js";
15
+ import {
16
+ AGENT_MEMORY_PLUGIN_API_VERSION,
17
+ type AgentMemoryPluginBundleV1,
18
+ type AgentMemoryPluginHostV1,
19
+ type AgentMemoryPluginManifestV1,
20
+ isPluginCapabilityEnabled,
21
+ type PluginCommandContextV1,
22
+ type PluginCommandResultV1,
23
+ type PluginCommandV1,
24
+ type PluginEntitlementStatusV1,
25
+ type PluginMemoryCorrectionV1,
26
+ type PluginMemoryWriteV1,
27
+ type PluginSessionStartHookV1,
28
+ validateBundleManifestV1,
29
+ validatePluginEntitlementStatusV1,
30
+ validatePluginManifestV1,
31
+ } from "./plugin-host.js";
32
+ import { TemporaryPluginBackend } from "./plugin-service.js";
33
+
34
+ interface RegisteredCommand {
35
+ command: PluginCommandV1;
36
+ pluginId: string;
37
+ }
38
+
39
+ export interface PluginRuntimeOptionsV1 {
40
+ coreVersion: string;
41
+ store?: PluginInstallStoreV1;
42
+ backend?: PluginBootstrapBackendV1;
43
+ }
44
+
45
+ function assertPermission(manifest: AgentMemoryPluginManifestV1, permission: string): void {
46
+ if (!manifest.permissions.includes(permission as never))
47
+ throw new PluginBootstrapFailure(
48
+ "plugin_permission_denied",
49
+ `Plugin ${manifest.id} did not declare permission ${permission}`,
50
+ );
51
+ }
52
+
53
+ function memoryResult(result: Awaited<ReturnType<typeof memoryWrite>>): {
54
+ ok: boolean;
55
+ path: string;
56
+ redacted: boolean;
57
+ } {
58
+ if (result.isError)
59
+ throw new PluginBootstrapFailure("plugin_memory_write_failed", result.text.replace(/^Error:\s*/, ""));
60
+ return {
61
+ ok: true,
62
+ path: typeof result.details.path === "string" ? result.details.path : getMemoryDir(),
63
+ redacted: result.details.redacted === true,
64
+ };
65
+ }
66
+
67
+ async function importBundle(
68
+ directory: string,
69
+ receipt: Pick<PluginInstallReceiptV1, "entrypoint" | "bundleId" | "version">,
70
+ ): Promise<AgentMemoryPluginBundleV1> {
71
+ let component = path.resolve(directory);
72
+ const rootStat = fs.lstatSync(component);
73
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
74
+ throw new PluginBootstrapFailure("plugin_entrypoint_invalid", "The installed plugin directory is unsafe");
75
+ for (const part of receipt.entrypoint.split("/")) {
76
+ component = path.join(component, part);
77
+ const componentStat = fs.lstatSync(component);
78
+ if (componentStat.isSymbolicLink())
79
+ throw new PluginBootstrapFailure(
80
+ "plugin_entrypoint_invalid",
81
+ "The installed plugin path contains a symbolic link",
82
+ );
83
+ }
84
+ const entrypoint = path.resolve(directory, ...receipt.entrypoint.split("/"));
85
+ if (!entrypoint.startsWith(`${path.resolve(directory)}${path.sep}`))
86
+ throw new PluginBootstrapFailure(
87
+ "plugin_entrypoint_invalid",
88
+ "The installed plugin entrypoint escapes its bundle",
89
+ );
90
+ const stat = fs.lstatSync(entrypoint);
91
+ if (!stat.isFile() || stat.isSymbolicLink())
92
+ throw new PluginBootstrapFailure(
93
+ "plugin_entrypoint_invalid",
94
+ "The installed plugin entrypoint is not a regular file",
95
+ );
96
+ const imported = (await import(`${pathToFileURL(entrypoint).href}?v=${encodeURIComponent(receipt.version)}`)) as {
97
+ default?: unknown;
98
+ };
99
+ const bundle = imported.default as AgentMemoryPluginBundleV1 | undefined;
100
+ if (!bundle || bundle.apiVersion !== AGENT_MEMORY_PLUGIN_API_VERSION || !Array.isArray(bundle.plugins))
101
+ throw new PluginBootstrapFailure(
102
+ "plugin_bundle_invalid",
103
+ "The plugin entrypoint did not export a compatible bundle",
104
+ );
105
+ validateBundleManifestV1(bundle.manifest);
106
+ if (bundle.manifest.id !== receipt.bundleId || bundle.manifest.version !== receipt.version)
107
+ throw new PluginBootstrapFailure(
108
+ "plugin_bundle_invalid",
109
+ "The loaded plugin bundle identity does not match its receipt",
110
+ );
111
+ const pluginIds = new Set<string>();
112
+ for (const plugin of bundle.plugins) {
113
+ validatePluginManifestV1(plugin.manifest);
114
+ if (pluginIds.has(plugin.manifest.id))
115
+ throw new PluginBootstrapFailure("plugin_bundle_invalid", "The plugin bundle contains duplicate plugin ids");
116
+ pluginIds.add(plugin.manifest.id);
117
+ }
118
+ if (
119
+ pluginIds.size !== bundle.manifest.plugins.length ||
120
+ bundle.manifest.plugins.some((pluginId) => !pluginIds.has(pluginId))
121
+ )
122
+ throw new PluginBootstrapFailure("plugin_bundle_invalid", "The plugin bundle contents do not match its manifest");
123
+ return bundle;
124
+ }
125
+
126
+ export class InstalledPluginRuntimeV1 {
127
+ private readonly store: PluginInstallStoreV1;
128
+ private readonly backend: PluginBootstrapBackendV1;
129
+ private readonly commands = new Map<string, RegisteredCommand>();
130
+ private readonly hooks: PluginSessionStartHookV1[] = [];
131
+ private loaded = false;
132
+
133
+ constructor(private readonly options: PluginRuntimeOptionsV1) {
134
+ this.store = options.store ?? new FilePluginInstallStore();
135
+ this.backend = options.backend ?? new TemporaryPluginBackend({ root: this.store.root });
136
+ }
137
+
138
+ async load(): Promise<boolean> {
139
+ if (this.loaded) return true;
140
+ const receipt = this.store.readReceipt(OFFICIAL_BUNDLE_ID);
141
+ if (!receipt || !this.store.hasInstalledBundle(receipt)) return false;
142
+ await this.refreshEntitlement();
143
+ const directory = path.join(this.store.root, "bundles", receipt.bundleId, receipt.version);
144
+ const bundle = await importBundle(directory, receipt);
145
+ for (const plugin of bundle.plugins) {
146
+ const host = this.createHost(plugin.manifest);
147
+ await plugin.activate(host);
148
+ const health = await plugin.healthCheck(host);
149
+ if (!health.ok)
150
+ throw new PluginBootstrapFailure(
151
+ "plugin_health_check_failed",
152
+ health.message ?? `Plugin ${plugin.manifest.id} failed its health check`,
153
+ );
154
+ }
155
+ this.loaded = true;
156
+ return true;
157
+ }
158
+
159
+ async run(name: string, context: PluginCommandContextV1): Promise<PluginCommandResultV1 | null> {
160
+ if (!(await this.load())) return null;
161
+ const registered = this.commands.get(name);
162
+ if (!registered) return null;
163
+ const entitlement = await this.refreshEntitlement();
164
+ if (!isPluginCapabilityEnabled(entitlement, registered.command.requiredCapability))
165
+ return {
166
+ ok: false,
167
+ error: {
168
+ code: "plugin_capability_denied",
169
+ message: `Capability ${registered.command.requiredCapability} is not enabled for ${registered.pluginId}`,
170
+ },
171
+ };
172
+ return registered.command.run(context);
173
+ }
174
+
175
+ private createHost(manifest: AgentMemoryPluginManifestV1): AgentMemoryPluginHostV1 {
176
+ const descriptors = new Map(manifest.commands.map((command) => [command.name, command]));
177
+ const stateRoot = path.join(this.store.root, "state");
178
+ fs.mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
179
+ const stateRootStat = fs.lstatSync(stateRoot);
180
+ if (!stateRootStat.isDirectory() || stateRootStat.isSymbolicLink())
181
+ throw new PluginBootstrapFailure("plugin_state_invalid", "The plugin state root is unsafe");
182
+ const stateDirectory = path.join(stateRoot, OFFICIAL_BUNDLE_ID);
183
+ if (!fs.existsSync(stateDirectory)) fs.mkdirSync(stateDirectory, { mode: 0o700 });
184
+ const stateDirectoryStat = fs.lstatSync(stateDirectory);
185
+ if (!stateDirectoryStat.isDirectory() || stateDirectoryStat.isSymbolicLink())
186
+ throw new PluginBootstrapFailure("plugin_state_invalid", "The plugin state directory is unsafe");
187
+ return {
188
+ apiVersion: AGENT_MEMORY_PLUGIN_API_VERSION,
189
+ coreVersion: this.options.coreVersion,
190
+ registerCommand: (command) => {
191
+ const descriptor = descriptors.get(command.name);
192
+ if (!descriptor || descriptor.requiredCapability !== command.requiredCapability)
193
+ throw new PluginBootstrapFailure(
194
+ "plugin_command_invalid",
195
+ `Plugin ${manifest.id} registered an undeclared command`,
196
+ );
197
+ const names = [command.name, ...(command.aliases ?? [])];
198
+ for (const name of names) {
199
+ if (this.commands.has(name))
200
+ throw new PluginBootstrapFailure(
201
+ "plugin_command_conflict",
202
+ `Plugin command ${name} is already registered`,
203
+ );
204
+ }
205
+ for (const name of names) this.commands.set(name, { command, pluginId: manifest.id });
206
+ },
207
+ registerSessionStartHook: (hook) => {
208
+ if (!(manifest.capabilities ?? []).includes(hook.requiredCapability))
209
+ throw new PluginBootstrapFailure(
210
+ "plugin_hook_invalid",
211
+ `Plugin ${manifest.id} registered a hook with an undeclared capability`,
212
+ );
213
+ this.hooks.push(hook);
214
+ },
215
+ getStateDirectory: () => stateDirectory,
216
+ getMemoryDirectory: () => {
217
+ assertPermission(manifest, "memory:read");
218
+ return getMemoryDir();
219
+ },
220
+ getEntitlement: async () => structuredClone(await this.refreshEntitlement()),
221
+ redactSecrets: (value) => redactSecrets(value).content,
222
+ writeMemory: async (request: PluginMemoryWriteV1) => {
223
+ assertPermission(manifest, "memory:write");
224
+ return memoryResult(await memoryWrite({ ...request, sessionId: `plugin-${manifest.id}` }));
225
+ },
226
+ correctMemory: async (request: PluginMemoryCorrectionV1) => {
227
+ assertPermission(manifest, "memory:correct");
228
+ const content = `Correction for ${request.artifactId}: ${request.content}${request.reason ? `\nReason: ${request.reason}` : ""}`;
229
+ return memoryResult(
230
+ await memoryWrite({
231
+ target: request.scope === "durable" ? "long_term" : "daily",
232
+ content,
233
+ sessionId: `plugin-${manifest.id}`,
234
+ sourceUri: request.sourceUri,
235
+ }),
236
+ );
237
+ },
238
+ scheduleSearchRefresh: () => {
239
+ assertPermission(manifest, "jobs:run");
240
+ scheduleQmdUpdate();
241
+ },
242
+ };
243
+ }
244
+
245
+ private async refreshEntitlement(): Promise<PluginEntitlementStatusV1> {
246
+ const entitlement = await this.backend.getLocalEntitlement();
247
+ validatePluginEntitlementStatusV1(entitlement);
248
+ return entitlement;
249
+ }
250
+ }
251
+
252
+ export function createInstalledBundleHealthCheck(
253
+ coreVersion: string,
254
+ backend: PluginBootstrapBackendV1,
255
+ storeRoot: string,
256
+ ): (directory: string, release: SignedPluginReleaseV1) => Promise<void> {
257
+ return async (directory, release) => {
258
+ const entitlement = await backend.getLocalEntitlement();
259
+ validatePluginEntitlementStatusV1(entitlement);
260
+ const receipt = {
261
+ bundleId: release.manifest.id,
262
+ version: release.manifest.version,
263
+ entrypoint: release.manifest.entrypoint,
264
+ };
265
+ const bundle = await importBundle(directory, receipt);
266
+ for (const plugin of bundle.plugins) {
267
+ const stateDirectory = path.join(storeRoot, "health", OFFICIAL_BUNDLE_ID);
268
+ const host: AgentMemoryPluginHostV1 = {
269
+ apiVersion: 1,
270
+ coreVersion,
271
+ registerCommand() {},
272
+ registerSessionStartHook() {},
273
+ getStateDirectory: () => stateDirectory,
274
+ getMemoryDirectory: () => {
275
+ assertPermission(plugin.manifest, "memory:read");
276
+ return getMemoryDir();
277
+ },
278
+ getEntitlement: async () => structuredClone(entitlement),
279
+ redactSecrets: (value) => redactSecrets(value).content,
280
+ async writeMemory() {
281
+ throw new PluginBootstrapFailure("plugin_health_check_invalid", "Health checks cannot write memory");
282
+ },
283
+ async correctMemory() {
284
+ throw new PluginBootstrapFailure("plugin_health_check_invalid", "Health checks cannot correct memory");
285
+ },
286
+ scheduleSearchRefresh() {},
287
+ };
288
+ const health = await plugin.healthCheck(host);
289
+ if (!health.ok)
290
+ throw new PluginBootstrapFailure(
291
+ "plugin_health_check_failed",
292
+ health.message ?? `Plugin ${plugin.manifest.id} failed its health check`,
293
+ );
294
+ }
295
+ };
296
+ }