@hydraharness/harness-plugin-runtime 0.1.1-rc.5

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/lib/index.js ADDED
@@ -0,0 +1,1766 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { Service } from "@hydraharness/cordis";
7
+ import { withFileLock, writeFileAtomic } from "@hydraharness/harness-atomic-write";
8
+ import { resolveHydraHome } from "@hydraharness/harness-home-paths";
9
+ import { apply, inject as inject$1 } from "@hydraharness/harness-hooks-codex";
10
+ import { parseCodexConfig } from "@hydraharness/harness-hooks-codex/config";
11
+ import { createUserMessage } from "@hydraharness/harness-llm";
12
+ import { apply as apply$1, inject as inject$2, publicToolName } from "@hydraharness/harness-mcp-client";
13
+ import { BUNDLED_SKILL_RANK, parseSkillDocument } from "@hydraharness/harness-skill";
14
+ import { parse } from "smol-toml";
15
+ import { parse as parse$1 } from "yaml";
16
+ //#region lib/types/index.js
17
+ /** Shared, immutable runtime for imported OpenAI/Codex and Claude plugin bundles. */
18
+ /** Services required by the imported-plugin runtime. */
19
+ const inject = [
20
+ "skills",
21
+ "commands",
22
+ "tools"
23
+ ];
24
+ const REGISTRY_VERSION = 1;
25
+ const MAX_PACKAGE_BYTES = 64 * 1024 * 1024;
26
+ const MAX_FILE_BYTES = 8 * 1024 * 1024;
27
+ const MAX_FILES = 4e3;
28
+ const MAX_MANIFEST_BYTES = 256 * 1024;
29
+ const MAX_MARKETPLACE_BYTES = 1024 * 1024;
30
+ const GIT_TIMEOUT_MS = 3e4;
31
+ const PLUGIN_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/u;
32
+ const COMMAND_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
33
+ const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
34
+ const GITHUB_SHORTHAND = /^([A-Za-z0-9][A-Za-z0-9._-]*)\/([A-Za-z0-9][A-Za-z0-9._-]*)$/u;
35
+ const SCP_GIT_SOURCE = /^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:[A-Za-z0-9._~/-]+$/u;
36
+ const AGENT_PLUGIN_SCHEMA_URI = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
37
+ const AGENT_PLUGIN_SCHEMA_PREFIX = "https://agent-plugins.org/schemas/";
38
+ const PLUGIN_COMMAND_HINT = "[list|import <folder-or-git-source>|install <plugin[@marketplace]|folder-or-git-source>|marketplace add <folder-or-git-source>|marketplace list|marketplace remove <source>|info <name>|enable <name>|disable <name>|trust <name>|untrust <name>|remove <name>]";
39
+ /** Plugin root resolver and manifest parser. It never executes plugin files. */
40
+ var PluginManifestLoader = class {
41
+ /**
42
+ * Validate and discover one bundle without executing it.
43
+ * @param root - Candidate plugin-root directory.
44
+ * @param identity - Source-qualified identity used to namespace MCP servers.
45
+ * @returns Parsed manifest and discovered component definitions.
46
+ */
47
+ async load(root, identity = "plugin") {
48
+ const checkedRoot = await validateBundleTree(root);
49
+ const manifestLocation = await pluginManifestPath(checkedRoot);
50
+ const manifest = parseManifest(await readBoundedJson(manifestLocation.path, MAX_MANIFEST_BYTES, "plugin manifest"), manifestLocation.format);
51
+ if (manifest.format === "claude") await rejectUnsupportedClaudeComponents(checkedRoot);
52
+ const agentMetadata = await discoverOpenAiMetadata(checkedRoot);
53
+ const skills = await discoverSkills(checkedRoot, manifest, agentMetadata);
54
+ const commands = await discoverCommands(checkedRoot);
55
+ const mcp = await discoverMcp(checkedRoot, manifest, identity);
56
+ const hooks = await discoverHooks(checkedRoot, manifest);
57
+ const apps = await discoverApps(checkedRoot, manifest);
58
+ return {
59
+ root: checkedRoot,
60
+ manifest,
61
+ skills,
62
+ commands,
63
+ mcp,
64
+ hooks,
65
+ ...hooks.length === 0 ? {} : { hookDigest: digest(hooks.map((hook) => hook.raw)) },
66
+ ...apps === void 0 ? {} : { apps },
67
+ ...agentMetadata === void 0 ? {} : { agentMetadata }
68
+ };
69
+ }
70
+ };
71
+ /** Durable registry and immutable bundle store beneath one Hydra home directory. */
72
+ var PluginStore = class {
73
+ home;
74
+ /** Immutable versioned plugin-bundle root. */
75
+ cacheRoot;
76
+ /** Persistent writable plugin-data root. */
77
+ dataRoot;
78
+ /** Durable source-qualified plugin registry path. */
79
+ registryPath;
80
+ manifests = new PluginManifestLoader();
81
+ constructor(home = resolveHydraHome()) {
82
+ this.home = home;
83
+ this.cacheRoot = join(home, "plugins", "cache");
84
+ this.dataRoot = join(home, "plugins", "data");
85
+ this.registryPath = join(home, "plugins", "registry.json");
86
+ }
87
+ /**
88
+ * List every stored entry by source-qualified identity.
89
+ * @returns Every stored entry by source-qualified identity.
90
+ */
91
+ async list() {
92
+ return new Map(Object.entries((await this.readRegistry()).plugins));
93
+ }
94
+ /**
95
+ * Read one stored plugin entry.
96
+ * @param identity - Source-qualified plugin identity.
97
+ * @returns Stored entry, or undefined when absent.
98
+ */
99
+ async get(identity) {
100
+ return (await this.readRegistry()).plugins[identity];
101
+ }
102
+ /**
103
+ * Validate, stage, and activate one imported source.
104
+ * @param source - Local, Git, or marketplace source.
105
+ * @returns Source-qualified identity of the activated plugin.
106
+ */
107
+ async install(source) {
108
+ const material = await materializeSource(source);
109
+ try {
110
+ const preliminary = await this.manifests.load(material.root);
111
+ const identity = pluginIdentity(preliminary.manifest.name, material.source.sourceId);
112
+ await this.ensureRegistryDirectory();
113
+ return await withFileLock(this.registryPath, async () => {
114
+ const stageParent = join(this.cacheRoot, material.source.sourceId, preliminary.manifest.name);
115
+ const stage = join(stageParent, `.staging-${randomUUID()}`);
116
+ const target = join(stageParent, preliminary.manifest.version);
117
+ try {
118
+ await copyTree(material.root, stage);
119
+ const staged = await this.manifests.load(stage, identity);
120
+ if (staged.manifest.name !== preliminary.manifest.name || staged.manifest.version !== preliminary.manifest.version) throw new Error("plugin runtime: plugin manifest changed while staging");
121
+ const registry = await this.readRegistry();
122
+ const duplicate = Object.entries(registry.plugins).find(([candidateIdentity, candidate]) => candidateIdentity !== identity && candidate.name === staged.manifest.name);
123
+ if (duplicate !== void 0) throw new Error(`plugin runtime: ${staged.manifest.name} is already installed from another source as ${duplicate[0]}; remove it before importing this source`);
124
+ await mkdir(stageParent, {
125
+ recursive: true,
126
+ mode: 448
127
+ });
128
+ if (!await exists(target)) await rename(stage, target);
129
+ const previous = registry.plugins[identity];
130
+ registry.plugins[identity] = {
131
+ name: staged.manifest.name,
132
+ source: material.source,
133
+ activeVersion: staged.manifest.version,
134
+ versions: {
135
+ ...previous?.versions,
136
+ [staged.manifest.version]: { installedAt: (/* @__PURE__ */ new Date()).toISOString() }
137
+ },
138
+ enabled: previous?.enabled ?? false,
139
+ ...previous?.hookTrustDigest === void 0 ? {} : { hookTrustDigest: previous.hookTrustDigest },
140
+ mcp: initializeMcpState(staged.mcp, previous?.mcp)
141
+ };
142
+ await this.writeRegistry(registry);
143
+ await mkdir(join(this.dataRoot, material.source.sourceId, staged.manifest.name), {
144
+ recursive: true,
145
+ mode: 448
146
+ });
147
+ return identity;
148
+ } finally {
149
+ await rm(stage, {
150
+ recursive: true,
151
+ force: true
152
+ });
153
+ }
154
+ }, { waitMs: GIT_TIMEOUT_MS });
155
+ } finally {
156
+ await material.dispose();
157
+ }
158
+ }
159
+ /**
160
+ * Mutate one durable entry while holding the registry lock.
161
+ * @param identity - Source-qualified plugin identity.
162
+ * @param mutation - In-place mutation applied before atomic persistence.
163
+ * @returns After the registry is persisted.
164
+ */
165
+ async update(identity, mutation) {
166
+ await this.ensureRegistryDirectory();
167
+ await withFileLock(this.registryPath, async () => {
168
+ const registry = await this.readRegistry();
169
+ const entry = registry.plugins[identity];
170
+ if (entry === void 0) throw new Error(`plugin runtime: imported plugin ${identity} is not installed`);
171
+ mutation(entry);
172
+ await this.writeRegistry(registry);
173
+ });
174
+ }
175
+ /**
176
+ * Remove one registry entry and its owned bundle and data directories.
177
+ * @param identity - Source-qualified plugin identity.
178
+ * @returns Removed entry, or undefined when absent.
179
+ */
180
+ async remove(identity) {
181
+ await this.ensureRegistryDirectory();
182
+ return await withFileLock(this.registryPath, async () => {
183
+ const registry = await this.readRegistry();
184
+ const entry = registry.plugins[identity];
185
+ if (entry === void 0) return void 0;
186
+ const { [identity]: _removed, ...remainingPlugins } = registry.plugins;
187
+ registry.plugins = remainingPlugins;
188
+ await this.writeRegistry(registry);
189
+ const cache = join(this.cacheRoot, entry.source.sourceId, entry.name);
190
+ const data = join(this.dataRoot, entry.source.sourceId, entry.name);
191
+ assertStoreDescendant(this.cacheRoot, cache);
192
+ assertStoreDescendant(this.dataRoot, data);
193
+ await rm(cache, {
194
+ recursive: true,
195
+ force: true
196
+ });
197
+ await rm(data, {
198
+ recursive: true,
199
+ force: true
200
+ });
201
+ return entry;
202
+ });
203
+ }
204
+ /**
205
+ * Resolve the active immutable bundle path.
206
+ * @param entry - Stored plugin entry.
207
+ * @returns Active version directory.
208
+ */
209
+ bundlePath(entry) {
210
+ return join(this.cacheRoot, entry.source.sourceId, entry.name, entry.activeVersion);
211
+ }
212
+ /**
213
+ * Resolve the persistent writable data path.
214
+ * @param entry - Stored plugin entry.
215
+ * @returns Plugin data directory.
216
+ */
217
+ dataPath(entry) {
218
+ return join(this.dataRoot, entry.source.sourceId, entry.name);
219
+ }
220
+ /**
221
+ * Load one active stored bundle.
222
+ * @param entry - Stored plugin entry.
223
+ * @param identity - Source-qualified plugin identity.
224
+ * @returns Parsed active bundle.
225
+ */
226
+ async load(entry, identity) {
227
+ return await this.manifests.load(this.bundlePath(entry), identity);
228
+ }
229
+ async readRegistry() {
230
+ try {
231
+ const raw = JSON.parse(await readFile(this.registryPath, "utf8"));
232
+ if (!isRecord(raw) || raw.version !== REGISTRY_VERSION || !isRecord(raw.plugins)) throw new Error("plugin runtime: registry has an unsupported format");
233
+ return raw;
234
+ } catch (error) {
235
+ if (isMissing(error)) return {
236
+ version: REGISTRY_VERSION,
237
+ plugins: {}
238
+ };
239
+ throw error;
240
+ }
241
+ }
242
+ async writeRegistry(registry) {
243
+ await writeFileAtomic(this.registryPath, `${JSON.stringify(registry, null, 2)}\n`, {
244
+ mode: 384,
245
+ dirMode: 448
246
+ });
247
+ }
248
+ async ensureRegistryDirectory() {
249
+ await mkdir(dirname(this.registryPath), {
250
+ recursive: true,
251
+ mode: 448
252
+ });
253
+ }
254
+ };
255
+ /** Lightweight views over the durable records; the runtime owns live components. */
256
+ var PluginRegistry = class {
257
+ store;
258
+ constructor(store) {
259
+ this.store = store;
260
+ }
261
+ /**
262
+ * List every stored entry by source-qualified identity.
263
+ * @returns Every stored entry by source-qualified identity.
264
+ */
265
+ list() {
266
+ return this.store.list();
267
+ }
268
+ /**
269
+ * Read one stored plugin entry.
270
+ * @param identity - Source-qualified plugin identity.
271
+ * @returns Stored entry, or undefined when absent.
272
+ */
273
+ get(identity) {
274
+ return this.store.get(identity);
275
+ }
276
+ };
277
+ /** Explicit names for the runtime's independent trust and component registries. */
278
+ var PluginTrustStore = class {
279
+ store;
280
+ constructor(store) {
281
+ this.store = store;
282
+ }
283
+ /**
284
+ * Record trust for one exact hook definition digest.
285
+ * @param identity - Source-qualified plugin identity.
286
+ * @param definitionDigest - Current canonical hook-definition digest.
287
+ * @returns After trust persistence completes.
288
+ */
289
+ async trust(identity, definitionDigest) {
290
+ await this.store.update(identity, (entry) => {
291
+ entry.hookTrustDigest = definitionDigest;
292
+ });
293
+ }
294
+ /**
295
+ * Remove recorded hook trust.
296
+ * @param identity - Source-qualified plugin identity.
297
+ * @returns After trust removal persists.
298
+ */
299
+ async untrust(identity) {
300
+ await this.store.update(identity, (entry) => {
301
+ delete entry.hookTrustDigest;
302
+ });
303
+ }
304
+ };
305
+ /** Public lifecycle facade over the shared imported-plugin runtime. */
306
+ var PluginLifecycleManager = class {
307
+ runtime;
308
+ constructor(runtime) {
309
+ this.runtime = runtime;
310
+ }
311
+ /**
312
+ * Enable and load one plugin.
313
+ * @param identity - Source-qualified plugin identity.
314
+ * @returns Refreshed installed-plugin projection.
315
+ */
316
+ enable(identity) {
317
+ return this.runtime.enable(identity);
318
+ }
319
+ /**
320
+ * Disable and unload one plugin.
321
+ * @param identity - Source-qualified plugin identity.
322
+ * @returns Refreshed installed-plugin projection.
323
+ */
324
+ disable(identity) {
325
+ return this.runtime.disable(identity);
326
+ }
327
+ /**
328
+ * Unload one plugin's live components.
329
+ * @param identity - Source-qualified plugin identity.
330
+ * @returns After owned components quiesce.
331
+ */
332
+ unload(identity) {
333
+ return this.runtime.unload(identity);
334
+ }
335
+ };
336
+ /** Shared `@hydraharness/harness-base` service for every imported bundle, not a Web-only facility. */
337
+ var ImportedPluginRuntime = class extends Service {
338
+ static inject = inject;
339
+ /** Component effects belong to the Host service, independent of a method caller's context. */
340
+ owner;
341
+ store;
342
+ /** Read-only facade over durable imported-plugin records. */
343
+ registry;
344
+ /** Hook-definition trust record manager. */
345
+ trust;
346
+ /** Enablement and teardown facade. */
347
+ lifecycle;
348
+ live = /* @__PURE__ */ new Map();
349
+ initialEnabled = /* @__PURE__ */ new Map();
350
+ startup = /* @__PURE__ */ new Map();
351
+ mutationTail = Promise.resolve();
352
+ constructor(ctx, config = {}) {
353
+ super(ctx, "importedPlugins");
354
+ this.owner = ctx;
355
+ this.store = new PluginStore(resolveHydraHome(config.hydraHome));
356
+ this.registry = new PluginRegistry(this.store);
357
+ this.trust = new PluginTrustStore(this.store);
358
+ this.lifecycle = new PluginLifecycleManager(this);
359
+ }
360
+ async [Service.init]() {
361
+ this.ctx.effect(() => () => this.dispose(), "plugin-runtime.dispose");
362
+ this.ctx.on("tools/pre-execute", (exec, next) => this.approveMcpTool(exec, next));
363
+ this.ctx.commands.register({
364
+ name: "plugin",
365
+ description: "Manage imported OpenAI/Codex-compatible plugin bundles",
366
+ input: { hint: PLUGIN_COMMAND_HINT },
367
+ handler: async ({ rawInput }) => this.command(rawInput)
368
+ });
369
+ const entries = await this.store.list();
370
+ for (const [identity, entry] of entries) {
371
+ this.initialEnabled.set(identity, entry.enabled);
372
+ if (entry.enabled) await this.load(identity, entry);
373
+ }
374
+ }
375
+ /**
376
+ * Return every imported plugin's current source-qualified runtime view.
377
+ * @returns Current installed-plugin projection.
378
+ */
379
+ async list() {
380
+ const entries = await this.store.list();
381
+ return { plugins: await Promise.all([...entries].sort(([left], [right]) => left.localeCompare(right)).map(async ([identity, entry]) => await this.view(identity, entry))) };
382
+ }
383
+ /**
384
+ * Return one installed plugin view.
385
+ * @param identityOrName - Source-qualified identity or an unambiguous plugin name.
386
+ * @returns Current installed-plugin projection.
387
+ */
388
+ async info(identityOrName) {
389
+ const [identity, entry] = await this.resolve(identityOrName);
390
+ return await this.view(identity, entry);
391
+ }
392
+ /**
393
+ * Stage one local, Git, or marketplace source.
394
+ * @param source - Source to import.
395
+ * @returns Refreshed installed-plugin projection.
396
+ */
397
+ import(source) {
398
+ return this.enqueue(async () => {
399
+ const identity = await this.store.install(source);
400
+ const entry = await this.require(identity);
401
+ if (entry.enabled) await this.reload(identity, entry);
402
+ return await this.list();
403
+ });
404
+ }
405
+ /**
406
+ * Enable and load one plugin.
407
+ * @param identityOrName - Plugin identity or unambiguous name.
408
+ * @returns Refreshed installed-plugin projection.
409
+ */
410
+ enable(identityOrName) {
411
+ return this.enqueue(async () => {
412
+ const [identity] = await this.resolve(identityOrName);
413
+ await this.store.update(identity, (entry) => {
414
+ entry.enabled = true;
415
+ });
416
+ await this.load(identity, await this.require(identity));
417
+ return await this.list();
418
+ });
419
+ }
420
+ /**
421
+ * Disable and unload one plugin.
422
+ * @param identityOrName - Plugin identity or unambiguous name.
423
+ * @returns Refreshed installed-plugin projection.
424
+ */
425
+ disable(identityOrName) {
426
+ return this.enqueue(async () => {
427
+ const [identity] = await this.resolve(identityOrName);
428
+ await this.unload(identity);
429
+ await this.store.update(identity, (entry) => {
430
+ entry.enabled = false;
431
+ });
432
+ return await this.list();
433
+ });
434
+ }
435
+ /**
436
+ * Trust one plugin's current hook definition.
437
+ * @param identityOrName - Plugin identity or unambiguous name.
438
+ * @returns Refreshed installed-plugin projection.
439
+ */
440
+ trustHooks(identityOrName) {
441
+ return this.enqueue(async () => {
442
+ const [identity, entry] = await this.resolve(identityOrName);
443
+ const loaded = await this.store.load(entry, identity);
444
+ if (loaded.hookDigest === void 0) throw new Error(`plugin runtime: ${identity} has no hooks to trust`);
445
+ await this.trust.trust(identity, loaded.hookDigest);
446
+ if (entry.enabled) await this.reload(identity, await this.require(identity));
447
+ return await this.list();
448
+ });
449
+ }
450
+ /**
451
+ * Revoke one plugin's hook trust.
452
+ * @param identityOrName - Plugin identity or unambiguous name.
453
+ * @returns Refreshed installed-plugin projection.
454
+ */
455
+ untrustHooks(identityOrName) {
456
+ return this.enqueue(async () => {
457
+ const [identity, entry] = await this.resolve(identityOrName);
458
+ await this.trust.untrust(identity);
459
+ if (entry.enabled) await this.reload(identity, await this.require(identity));
460
+ return await this.list();
461
+ });
462
+ }
463
+ /**
464
+ * Unload and remove one plugin with its owned store paths.
465
+ * @param identityOrName - Plugin identity or unambiguous name.
466
+ * @returns Refreshed installed-plugin projection.
467
+ */
468
+ remove(identityOrName) {
469
+ return this.enqueue(async () => {
470
+ const [identity] = await this.resolve(identityOrName);
471
+ await this.unload(identity);
472
+ await this.store.remove(identity);
473
+ return await this.list();
474
+ });
475
+ }
476
+ /**
477
+ * Set one plugin MCP server's lifecycle state.
478
+ * @param identityOrName - Owning plugin identity or unambiguous name.
479
+ * @param server - Manifest MCP server name.
480
+ * @param enabled - Desired server state.
481
+ * @returns Refreshed installed-plugin projection.
482
+ */
483
+ async setMcpServerEnabled(identityOrName, server, enabled) {
484
+ return this.enqueue(async () => {
485
+ const [identity] = await this.resolve(identityOrName);
486
+ await this.store.update(identity, (entry) => {
487
+ const state = entry.mcp[server];
488
+ if (!Object.hasOwn(entry.mcp, server) || state === void 0) throw new Error(`plugin runtime: MCP server ${server} is not declared`);
489
+ state.enabled = enabled;
490
+ entry.mcp[server] = state;
491
+ });
492
+ const entry = await this.require(identity);
493
+ if (entry.enabled) await this.reload(identity, entry);
494
+ return await this.list();
495
+ });
496
+ }
497
+ /**
498
+ * Set one MCP tool's approval mode.
499
+ * @param identityOrName - Owning plugin identity or unambiguous name.
500
+ * @param server - Manifest MCP server name.
501
+ * @param tool - Raw MCP tool name or host-qualified public tool name.
502
+ * @param approval - Independent per-tool approval mode.
503
+ * @returns Refreshed installed-plugin projection.
504
+ */
505
+ async setMcpToolApproval(identityOrName, server, tool, approval) {
506
+ return this.enqueue(async () => {
507
+ const [identity] = await this.resolve(identityOrName);
508
+ await this.store.update(identity, (entry) => {
509
+ const state = entry.mcp[server];
510
+ if (!Object.hasOwn(entry.mcp, server) || state === void 0) throw new Error(`plugin runtime: MCP server ${server} is not declared`);
511
+ state.toolApproval[tool] = approval;
512
+ entry.mcp[server] = state;
513
+ });
514
+ return await this.list();
515
+ });
516
+ }
517
+ /**
518
+ * Unload every live component owned by one plugin.
519
+ * @param identity - Installed plugin identity.
520
+ * @returns After skills, hooks, and MCP fibers quiesce.
521
+ */
522
+ async unload(identity) {
523
+ const component = this.live.get(identity);
524
+ if (component === void 0) return;
525
+ this.live.delete(identity);
526
+ component.skillDispose?.();
527
+ for (const dispose of [...component.commandDisposes].reverse()) dispose();
528
+ const fibers = [...component.mcpFibers.values(), component.hookFiber].filter((fiber) => fiber !== void 0);
529
+ for (const fiber of fibers) fiber.dispose();
530
+ await Promise.all(fibers.map(async (fiber) => {
531
+ await fiber.await().catch(() => {});
532
+ }));
533
+ this.startup.delete(identity);
534
+ }
535
+ async load(identity, entry) {
536
+ if (this.live.has(identity)) return;
537
+ const loaded = await this.store.load(entry, identity);
538
+ const component = {
539
+ commandDisposes: [],
540
+ mcpFibers: /* @__PURE__ */ new Map()
541
+ };
542
+ this.live.set(identity, component);
543
+ try {
544
+ const skillDispose = this.registerSkills(identity, loaded.skills);
545
+ if (skillDispose !== void 0) component.skillDispose = skillDispose;
546
+ this.registerCommands(identity, loaded.commands, component.commandDisposes);
547
+ await this.startMcp(identity, entry, loaded, component);
548
+ if (loaded.hookDigest !== void 0 && entry.hookTrustDigest === loaded.hookDigest) component.hookFiber = await this.startHooks(identity, entry, loaded);
549
+ } catch (error) {
550
+ await this.unload(identity);
551
+ throw error;
552
+ }
553
+ }
554
+ async reload(identity, entry) {
555
+ await this.unload(identity);
556
+ await this.load(identity, entry);
557
+ }
558
+ registerSkills(identity, skills) {
559
+ if (skills.length === 0) return void 0;
560
+ return this.owner.skills.registerProvider(() => ({
561
+ name: `codex-plugin:${identity}`,
562
+ list: (_options) => {
563
+ return Promise.resolve(skills.map((skill) => ({
564
+ name: qualifiedSkillName(identity, skill.rawName),
565
+ aliases: [skill.rawName],
566
+ description: skill.description,
567
+ ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
568
+ invocation: skill.invocation,
569
+ source: `codex-plugin:${identity}`,
570
+ provider: `codex-plugin:${identity}`,
571
+ rank: BUNDLED_SKILL_RANK,
572
+ locator: skill,
573
+ path: skill.path,
574
+ resourceBase: {
575
+ kind: "directory",
576
+ path: skill.directory
577
+ },
578
+ metadata: {
579
+ pluginIdentity: identity,
580
+ skillName: skill.rawName
581
+ }
582
+ })));
583
+ },
584
+ get: async (candidate) => {
585
+ const skill = candidate.locator;
586
+ const parsed = parseSkillDocument(await readBoundedText(skill.path, MAX_FILE_BYTES, "skill file"));
587
+ if (parsed.name !== skill.rawName) return void 0;
588
+ return {
589
+ name: candidate.name,
590
+ description: parsed.description,
591
+ ...parsed.whenToUse === void 0 ? {} : { whenToUse: parsed.whenToUse },
592
+ invocation: {
593
+ ...parsed.invocation,
594
+ modelInvocable: parsed.invocation.modelInvocable && skill.openAiModelInvocable
595
+ },
596
+ source: `codex-plugin:${identity}`,
597
+ provider: `codex-plugin:${identity}`,
598
+ resourceBase: {
599
+ kind: "directory",
600
+ path: skill.directory
601
+ },
602
+ path: skill.path,
603
+ metadata: {
604
+ pluginIdentity: identity,
605
+ skillName: skill.rawName
606
+ },
607
+ content: parsed.content
608
+ };
609
+ }
610
+ }));
611
+ }
612
+ registerCommands(identity, commands, disposes) {
613
+ for (const command of commands) try {
614
+ disposes.push(this.owner.commands.register({
615
+ name: command.name,
616
+ description: command.description,
617
+ input: { hint: command.inputHint ?? "[arguments]" },
618
+ handler: ({ agent, rawInput }) => {
619
+ agent.followup(createUserMessage({
620
+ content: [{
621
+ type: "text",
622
+ text: renderCommandPrompt(command.prompt, rawInput)
623
+ }],
624
+ source: {
625
+ kind: "plugin",
626
+ plugin: `codex-plugin:${identity}`
627
+ }
628
+ }));
629
+ return { kind: "success" };
630
+ }
631
+ }));
632
+ } catch (error) {
633
+ if (!(error instanceof Error) || !error.message.includes("is already registered")) throw error;
634
+ this.ctx.logger.warn(`plugin runtime: command "/${command.name}" from ${identity} ignored because a command with that name is already registered`);
635
+ }
636
+ }
637
+ async startMcp(identity, entry, loaded, component) {
638
+ const states = /* @__PURE__ */ new Map();
639
+ this.startup.set(identity, states);
640
+ const pending = [];
641
+ for (const server of loaded.mcp) {
642
+ if (entry.mcp[server.name]?.enabled !== true) continue;
643
+ states.set(server.name, "starting");
644
+ const fiber = this.owner.plugin({
645
+ name: `imported-mcp:${identity}:${server.name}`,
646
+ inject: inject$2,
647
+ apply: apply$1
648
+ }, server.config);
649
+ component.mcpFibers.set(server.name, fiber);
650
+ pending.push(Promise.resolve(fiber).then(() => {
651
+ states.set(server.name, "started");
652
+ }, async () => {
653
+ states.set(server.name, "failed");
654
+ component.mcpFibers.delete(server.name);
655
+ try {
656
+ await fiber.dispose();
657
+ } catch {}
658
+ }));
659
+ }
660
+ await Promise.all(pending);
661
+ }
662
+ async startHooks(identity, entry, loaded) {
663
+ const configPath = join(this.store.dataPath(entry), `hooks-${loaded.hookDigest}.json`);
664
+ const hookConfig = mergeHookDefinitions(loaded.hooks);
665
+ await writeFileAtomic(configPath, `${JSON.stringify(hookConfig, (key, value) => key === "command" && typeof value === "string" && process.platform === "win32" ? value.replaceAll(/\$\{((?:CLAUDE_)?PLUGIN_(?:ROOT|DATA))\}/gu, "${env:$1}") : value)}\n`, {
666
+ mode: 384,
667
+ dirMode: 448
668
+ });
669
+ return this.owner.plugin({
670
+ name: `imported-hooks:${identity}`,
671
+ inject: inject$1,
672
+ apply
673
+ }, {
674
+ configPath,
675
+ env: {
676
+ PLUGIN_ROOT: loaded.root,
677
+ PLUGIN_DATA: this.store.dataPath(entry),
678
+ CLAUDE_PLUGIN_ROOT: loaded.root,
679
+ CLAUDE_PLUGIN_DATA: this.store.dataPath(entry)
680
+ }
681
+ });
682
+ }
683
+ async view(identity, entry) {
684
+ const loaded = await this.store.load(entry, identity);
685
+ const hookTrustState = loaded.hookDigest === void 0 ? "not-applicable" : loaded.hookDigest === entry.hookTrustDigest ? "trusted" : "pending";
686
+ const started = this.startup.get(identity);
687
+ return {
688
+ identity,
689
+ name: entry.name,
690
+ ...loaded.manifest.description === void 0 ? {} : { description: loaded.manifest.description },
691
+ ...loaded.manifest.application === void 0 ? {} : { application: loaded.manifest.application },
692
+ version: entry.activeVersion,
693
+ source: entry.source,
694
+ pluginRoot: loaded.root,
695
+ dataPath: this.store.dataPath(entry),
696
+ enabled: entry.enabled,
697
+ initialEnabled: this.initialEnabled.get(identity) ?? false,
698
+ lifecycle: this.live.has(identity) ? "loaded" : entry.enabled ? "enabled" : "disabled",
699
+ hookTrustState,
700
+ ...loaded.hookDigest === void 0 ? {} : { hookDefinitionDigest: loaded.hookDigest },
701
+ skills: loaded.skills.map((skill) => skill.rawName),
702
+ mcpServers: loaded.mcp.map((server) => ({
703
+ name: server.name,
704
+ enabled: entry.mcp[server.name]?.enabled === true,
705
+ startupState: started?.get(server.name) ?? "not-started",
706
+ authenticationState: server.config.transport === "stdio" ? "not-applicable" : "unknown",
707
+ defaultToolsApprovalMode: entry.mcp[server.name]?.defaultToolsApprovalMode ?? "ask",
708
+ toolApproval: entry.mcp[server.name]?.toolApproval ?? {},
709
+ tools: this.ctx.tools.schemas().map((tool) => tool.name).filter((name) => name.startsWith(`mcp__${mcpServerName(identity, server.name)}__`)).sort()
710
+ })),
711
+ hooks: loaded.hooks.map((hook) => hook.label),
712
+ ...loaded.agentMetadata === void 0 ? {} : { agentMetadata: loaded.agentMetadata },
713
+ ...loaded.apps === void 0 ? {} : { appMappings: Object.keys(loaded.apps).sort() },
714
+ installationStatus: "installed"
715
+ };
716
+ }
717
+ async resolve(identityOrName) {
718
+ const value = identityOrName.trim();
719
+ const entries = await this.store.list();
720
+ const exact = entries.get(value);
721
+ if (exact !== void 0) return [value, exact];
722
+ const matches = [...entries].filter(([, entry]) => entry.name === value);
723
+ if (matches.length === 1) return matches[0];
724
+ if (matches.length > 1) throw new Error(`plugin runtime: ${value} exists in multiple sources; use <name>@<source-id>`);
725
+ throw new Error(`plugin runtime: imported plugin ${value} is not installed`);
726
+ }
727
+ async require(identity) {
728
+ const entry = await this.store.get(identity);
729
+ if (entry === void 0) throw new Error(`plugin runtime: imported plugin ${identity} is not installed`);
730
+ return entry;
731
+ }
732
+ enqueue(task) {
733
+ const run = this.mutationTail.then(task);
734
+ this.mutationTail = run.then(() => void 0, () => void 0);
735
+ return run;
736
+ }
737
+ async approveMcpTool(exec, next) {
738
+ if (!exec.name.startsWith("mcp__")) return await next();
739
+ for (const [identity, component] of this.live) {
740
+ const matching = [...component.mcpFibers.keys()].find((name) => exec.name.startsWith(`mcp__${mcpServerName(identity, name)}__`));
741
+ if (matching === void 0) continue;
742
+ const state = (await this.require(identity)).mcp[matching];
743
+ const override = Object.entries(state?.toolApproval ?? {}).find(([raw]) => raw === exec.name || publicToolName(mcpServerName(identity, matching), raw) === exec.name);
744
+ const toolName = override?.[0] ?? exec.name;
745
+ const approval = override?.[1] ?? state?.defaultToolsApprovalMode ?? "ask";
746
+ if (approval === "allow") return await next();
747
+ if (approval === "deny") return {
748
+ kind: "deny",
749
+ reason: `MCP tool ${toolName} is disabled by its plugin policy`
750
+ };
751
+ return {
752
+ kind: "ask",
753
+ reason: `MCP tool ${toolName} requires approval`
754
+ };
755
+ }
756
+ return await next();
757
+ }
758
+ marketplaceInventory() {
759
+ const inventory = this.ctx.get("pluginInventory");
760
+ return inventory === void 0 || inventory === null ? void 0 : inventory;
761
+ }
762
+ requireMarketplaceInventory() {
763
+ const inventory = this.marketplaceInventory();
764
+ if (inventory === void 0) throw new Error("plugin runtime: plugin inventory is unavailable");
765
+ return inventory;
766
+ }
767
+ async resolveMarketplace(name) {
768
+ const trimmed = name.trim();
769
+ if (isExplicitSource(trimmed)) return trimmed;
770
+ const inventory = this.marketplaceInventory();
771
+ if (inventory === void 0) throw new Error(`plugin runtime: unknown marketplace ${trimmed}`);
772
+ const { marketplaces } = await inventory.listMarketplaces();
773
+ const key = marketplaceRepoKey(trimmed);
774
+ const matches = marketplaces.filter((entry) => entry.source === trimmed || marketplaceRepoKey(entry.source) === key);
775
+ if (matches.length === 1)
776
+ /* v8 ignore next -- filter produced exactly one marketplace with a required source. */
777
+ return matches[0]?.source ?? trimmed;
778
+ if (matches.length > 1) throw new Error(`plugin runtime: marketplace ${trimmed} matches multiple sources`);
779
+ throw new Error(`plugin runtime: unknown marketplace ${trimmed}`);
780
+ }
781
+ async marketplaceCommand(input) {
782
+ const [verb, ...rest] = input.split(/\s+/u).filter((part) => part !== "");
783
+ const argument = rest.join(" ");
784
+ switch (verb) {
785
+ case "add": {
786
+ if (argument === "") return usage();
787
+ const inventory = this.marketplaceInventory();
788
+ if (inventory === void 0) {
789
+ await this.import(argument);
790
+ return {
791
+ kind: "success",
792
+ text: "Plugin imported. It is disabled until explicitly enabled."
793
+ };
794
+ }
795
+ await inventory.addMarketplace({ source: argument });
796
+ return {
797
+ kind: "success",
798
+ text: "Marketplace added. Install a plugin with /plugin install <name>@<marketplace>."
799
+ };
800
+ }
801
+ case "list": {
802
+ const { marketplaces } = await this.requireMarketplaceInventory().listMarketplaces();
803
+ return {
804
+ kind: "success",
805
+ text: marketplaces.length === 0 ? "No plugin marketplaces." : marketplaces.map((entry) => `${entry.source} ${entry.status ?? "ready"}${entry.enabled === false ? " disabled" : ""}`).join("\n")
806
+ };
807
+ }
808
+ case "remove":
809
+ if (argument === "") return usage();
810
+ await this.requireMarketplaceInventory().removeMarketplace(await this.resolveMarketplace(argument));
811
+ return {
812
+ kind: "success",
813
+ text: "Marketplace removed."
814
+ };
815
+ default: return usage();
816
+ }
817
+ }
818
+ async installCommand(argument) {
819
+ if (argument === "") return usage();
820
+ const qualified = /^([A-Za-z0-9][A-Za-z0-9._-]{0,99})@(.+)$/u.exec(argument);
821
+ let request;
822
+ if (isExplicitSource(argument)) request = argument;
823
+ else if (qualified !== null) {
824
+ /* v8 ignore next -- both regex capture groups require at least one character. */
825
+ const marketplace = qualified[2] ?? "";
826
+ /* v8 ignore next -- both regex capture groups require at least one character. */
827
+ const plugin = qualified[1] ?? "";
828
+ request = {
829
+ source: await this.resolveMarketplace(marketplace),
830
+ plugin
831
+ };
832
+ } else {
833
+ const entries = [...await this.store.list()].filter(([, entry]) => entry.name === argument);
834
+ if (entries.length === 1) {
835
+ /* v8 ignore next -- the filtered map contains exactly one entry tuple. */
836
+ const [entryId] = entries[0] ?? [];
837
+ /* v8 ignore if -- every map entry tuple starts with its string key. */
838
+ if (entryId === void 0) throw new Error(`plugin runtime: imported plugin ${argument} is not installed`);
839
+ await this.enable(entryId);
840
+ return {
841
+ kind: "success",
842
+ text: `Plugin ${argument} enabled.`
843
+ };
844
+ }
845
+ const inventory = this.marketplaceInventory();
846
+ const marketplaces = inventory === void 0 ? [] : (await inventory.listMarketplaces()).marketplaces;
847
+ if (marketplaces.length !== 1) throw new Error(`plugin runtime: imported plugin ${argument} is not installed`);
848
+ const marketplace = marketplaces[0];
849
+ /* v8 ignore if -- the preceding length check requires exactly one marketplace. */
850
+ if (marketplace === void 0) throw new Error(`plugin runtime: imported plugin ${argument} is not installed`);
851
+ request = {
852
+ source: marketplace.source,
853
+ plugin: argument
854
+ };
855
+ }
856
+ const name = (await this.import(request)).plugins[0]?.name;
857
+ if (name === void 0) throw new Error("plugin runtime: import produced no plugin");
858
+ await this.enable(name);
859
+ return {
860
+ kind: "success",
861
+ text: `Plugin ${name} enabled.`
862
+ };
863
+ }
864
+ async command(rawInput) {
865
+ const input = rawInput.trim();
866
+ if (input === "" || input === "list") {
867
+ const plugins = (await this.list()).plugins;
868
+ return {
869
+ kind: "success",
870
+ text: plugins.length === 0 ? "No imported plugins." : plugins.map((plugin) => `${plugin.identity} ${plugin.version} ${plugin.enabled ? "enabled" : "disabled"}${plugin.hookTrustState === "pending" ? " (hook review pending)" : ""}`).join("\n")
871
+ };
872
+ }
873
+ const [verb] = input.split(/\s+/u);
874
+ /* v8 ignore next -- splitting a non-empty input always yields a first word. */
875
+ const argument = input.slice(verb?.length ?? 0).trim();
876
+ try {
877
+ switch (verb) {
878
+ case "import":
879
+ if (argument === "") return usage();
880
+ await this.import(argument);
881
+ return {
882
+ kind: "success",
883
+ text: "Plugin imported. It is disabled until explicitly enabled."
884
+ };
885
+ case "install": return await this.installCommand(argument);
886
+ case "marketplace": return await this.marketplaceCommand(argument);
887
+ case "info": return argument === "" ? usage() : {
888
+ kind: "success",
889
+ text: renderInfo(await this.info(argument))
890
+ };
891
+ case "enable":
892
+ if (argument === "") return usage();
893
+ await this.enable(argument);
894
+ return {
895
+ kind: "success",
896
+ text: "Plugin enabled."
897
+ };
898
+ case "disable":
899
+ if (argument === "") return usage();
900
+ await this.disable(argument);
901
+ return {
902
+ kind: "success",
903
+ text: "Plugin disabled and unloaded."
904
+ };
905
+ case "trust":
906
+ if (argument === "") return usage();
907
+ await this.trustHooks(argument);
908
+ return {
909
+ kind: "success",
910
+ text: "Current hook definitions trusted; no MCP/tool approval changed."
911
+ };
912
+ case "untrust":
913
+ if (argument === "") return usage();
914
+ await this.untrustHooks(argument);
915
+ return {
916
+ kind: "success",
917
+ text: "Hook trust removed."
918
+ };
919
+ case "remove":
920
+ if (argument === "") return usage();
921
+ await this.remove(argument);
922
+ return {
923
+ kind: "success",
924
+ text: "Plugin removed."
925
+ };
926
+ default: return usage();
927
+ }
928
+ } catch (error) {
929
+ return {
930
+ kind: "error",
931
+ text: error instanceof Error ? error.message : String(error)
932
+ };
933
+ }
934
+ }
935
+ async dispose() {
936
+ for (const identity of [...this.live.keys()]) await this.unload(identity);
937
+ }
938
+ };
939
+ const name = "plugin-runtime";
940
+ function usage() {
941
+ return {
942
+ kind: "error",
943
+ text: `Usage: /plugin ${PLUGIN_COMMAND_HINT}`
944
+ };
945
+ }
946
+ function isExplicitSource(value) {
947
+ if (GITHUB_SHORTHAND.test(value) || SCP_GIT_SOURCE.test(value)) return true;
948
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(value) || isAbsolute(value)) return true;
949
+ return !value.includes("@") && (value.includes("/") || value.includes("\\"));
950
+ }
951
+ function marketplaceRepoKey(source) {
952
+ /* v8 ignore next -- persisted marketplace sources always retain a host or path segment */
953
+ return (source.replace(/\.git$/iu, "").split(/[/\\:]/u).filter((part) => part.length > 0).at(-1) ?? source).toLowerCase();
954
+ }
955
+ function renderInfo(plugin) {
956
+ return [
957
+ `${plugin.identity} ${plugin.version}`,
958
+ `source: ${plugin.source.source}`,
959
+ `enabled: ${String(plugin.enabled)}; hooks: ${plugin.hookTrustState}`,
960
+ `skills: ${plugin.skills.join(", ") || "none"}`,
961
+ `MCP: ${plugin.mcpServers.map((server) => `${server.name}=${server.enabled ? server.startupState : "disabled"}`).join(", ") || "none"}`
962
+ ].join("\n");
963
+ }
964
+ function pluginIdentity(name, sourceId) {
965
+ return `${name}@${sourceId}`;
966
+ }
967
+ function digest(value) {
968
+ return createHash("sha256").update(stableJson(value)).digest("hex");
969
+ }
970
+ function sourceId(value) {
971
+ return digest(value).slice(0, 16);
972
+ }
973
+ function qualifiedSkillName(identity, name) {
974
+ return `plugin-${digest(identity).slice(0, 12)}-${name}`.replace(/[^a-z0-9-]/giu, "-").toLowerCase();
975
+ }
976
+ function renderCommandPrompt(prompt, rawInput) {
977
+ const argumentsText = rawInput.trim();
978
+ const positional = argumentsText === "" ? [] : argumentsText.split(/\s+/u);
979
+ return prompt.replaceAll("{{args}}", argumentsText).replaceAll("$ARGUMENTS", argumentsText).replaceAll(/\$(\d+)\b/gu, (_match, index) => positional[Number(index) - 1] ?? "");
980
+ }
981
+ function mcpServerName(identity, name) {
982
+ return `p${digest(identity).slice(0, 10)}-${digest(name).slice(0, 10)}`;
983
+ }
984
+ async function materializeSource(input) {
985
+ const request = typeof input === "string" ? { source: input } : input;
986
+ const source = request.source.trim();
987
+ if (source === "" || source.length > 2048 || /[\u0000-\u001F\u007F]/u.test(source)) throw new Error("plugin runtime: plugin source is invalid");
988
+ const local = resolve(source);
989
+ try {
990
+ if ((await stat(local)).isDirectory()) {
991
+ const base = await realpath(local);
992
+ const marketplacePlugin = await marketplaceGitPlugin(base, request);
993
+ if (marketplacePlugin !== void 0) return await materializeMarketplaceGitPlugin("marketplace-local", base, void 0, marketplacePlugin);
994
+ const selected = await selectPluginRoot(base, request);
995
+ const marketplace = !await isDirectPluginRoot(base, request);
996
+ return {
997
+ root: selected,
998
+ source: {
999
+ kind: marketplace ? "marketplace-local" : "local",
1000
+ source: base,
1001
+ sourceId: sourceId({
1002
+ kind: marketplace ? "marketplace-local" : "local",
1003
+ source: base,
1004
+ path: request.path,
1005
+ plugin: request.plugin
1006
+ }),
1007
+ ...request.path === void 0 ? {} : { path: request.path },
1008
+ ...marketplace ? { marketplace: base } : {}
1009
+ },
1010
+ dispose: async () => {}
1011
+ };
1012
+ }
1013
+ } catch (error) {
1014
+ if (!isMissing(error)) throw error;
1015
+ }
1016
+ const remote = normalizeGitSource(source);
1017
+ const temp = await mkdirTemp("hydra-imported-plugin-");
1018
+ const checkout = join(temp, "repository");
1019
+ try {
1020
+ await runGit(temp, [
1021
+ "clone",
1022
+ "--depth",
1023
+ "1",
1024
+ "--filter=blob:none",
1025
+ "--",
1026
+ remote,
1027
+ checkout
1028
+ ], request.ref);
1029
+ const marketplacePlugin = await marketplaceGitPlugin(checkout, request);
1030
+ if (marketplacePlugin !== void 0) {
1031
+ const nested = await materializeMarketplaceGitPlugin("marketplace-git", remote, request.ref, marketplacePlugin);
1032
+ return {
1033
+ ...nested,
1034
+ dispose: async () => {
1035
+ await nested.dispose();
1036
+ await rm(temp, {
1037
+ recursive: true,
1038
+ force: true
1039
+ });
1040
+ }
1041
+ };
1042
+ }
1043
+ const selected = await selectPluginRoot(checkout, request);
1044
+ const marketplace = !await isDirectPluginRoot(checkout, request);
1045
+ return {
1046
+ root: selected,
1047
+ source: {
1048
+ kind: marketplace ? "marketplace-git" : "git",
1049
+ source: remote,
1050
+ sourceId: sourceId({
1051
+ kind: marketplace ? "marketplace-git" : "git",
1052
+ source: remote,
1053
+ ref: request.ref,
1054
+ path: request.path,
1055
+ plugin: request.plugin
1056
+ }),
1057
+ ...request.ref === void 0 ? {} : { ref: request.ref },
1058
+ ...request.path === void 0 ? {} : { path: request.path },
1059
+ ...marketplace ? { marketplace: remote } : {}
1060
+ },
1061
+ dispose: async () => {
1062
+ await rm(temp, {
1063
+ recursive: true,
1064
+ force: true
1065
+ });
1066
+ }
1067
+ };
1068
+ } catch (error) {
1069
+ await rm(temp, {
1070
+ recursive: true,
1071
+ force: true
1072
+ });
1073
+ throw error;
1074
+ }
1075
+ }
1076
+ async function mkdirTemp(prefix) {
1077
+ const { mkdtemp } = await import("node:fs/promises");
1078
+ return await mkdtemp(join(tmpdir(), prefix));
1079
+ }
1080
+ function normalizeGitSource(source) {
1081
+ const shorthand = GITHUB_SHORTHAND.exec(source);
1082
+ if (shorthand !== null)
1083
+ /* v8 ignore next -- GITHUB_SHORTHAND requires a non-empty repository capture. */
1084
+ return `https://github.com/${shorthand[1]}/${(shorthand[2] ?? "").replace(/\.git$/u, "")}.git`;
1085
+ if (/^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:[A-Za-z0-9._~/-]+$/u.test(source)) return source;
1086
+ let parsed;
1087
+ try {
1088
+ parsed = new URL(source);
1089
+ } catch {
1090
+ throw new Error("plugin runtime: source must be an existing folder, GitHub shorthand, or HTTPS/SSH Git URL");
1091
+ }
1092
+ if (parsed.protocol !== "https:" && parsed.protocol !== "ssh:" || parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "") throw new Error("plugin runtime: remote source must be credential-free HTTPS or SSH Git URL");
1093
+ return parsed.href;
1094
+ }
1095
+ function runGit(cwd, args, ref) {
1096
+ if (ref !== void 0 && (ref.length === 0 || ref.length > 255 || ref.startsWith("-") || /[\u0000-\u0020\u007F]/u.test(ref))) return Promise.reject(/* @__PURE__ */ new Error("plugin runtime: Git ref is invalid"));
1097
+ const cloneArgs = ref === void 0 ? args : [
1098
+ ...args.slice(0, -3),
1099
+ "--branch",
1100
+ ref,
1101
+ ...args.slice(-3)
1102
+ ];
1103
+ const env = {};
1104
+ for (const [key, value] of Object.entries(process.env)) if (!/(?:KEY|SECRET|TOKEN|PASSWORD)/iu.test(key)) env[key] = value;
1105
+ env.GCM_INTERACTIVE = "Never";
1106
+ env.GIT_TERMINAL_PROMPT = "0";
1107
+ return new Promise((resolvePromise, reject) => {
1108
+ execFile("git", [
1109
+ "-c",
1110
+ "protocol.ext.allow=never",
1111
+ "-c",
1112
+ "protocol.file.allow=never",
1113
+ ...cloneArgs
1114
+ ], {
1115
+ cwd,
1116
+ env,
1117
+ timeout: GIT_TIMEOUT_MS,
1118
+ maxBuffer: MAX_MARKETPLACE_BYTES,
1119
+ windowsHide: true
1120
+ }, (error) => {
1121
+ if (error === null) resolvePromise();
1122
+ else reject(new Error("plugin runtime: Git import failed", { cause: error }));
1123
+ });
1124
+ });
1125
+ }
1126
+ async function selectPluginRoot(root, request) {
1127
+ const candidate = request.path === void 0 ? root : await existingInside(root, request.path, "plugin path");
1128
+ if (await hasPluginManifest(candidate)) return candidate;
1129
+ const location = marketplaceLocalPluginPath(await selectMarketplacePlugin(candidate, request.plugin));
1130
+ if (location === void 0) throw new Error("plugin runtime: marketplace plugin requires a relative path or source");
1131
+ const pluginRoot = await existingInside(candidate, location, "marketplace plugin path");
1132
+ if (!await hasPluginManifest(pluginRoot)) throw new Error("plugin runtime: marketplace entry is not a Codex plugin root");
1133
+ return pluginRoot;
1134
+ }
1135
+ async function materializeMarketplaceGitPlugin(kind, marketplace, marketplaceRef, plugin) {
1136
+ const nested = await materializeSource({
1137
+ source: plugin.source,
1138
+ ...plugin.path === void 0 ? {} : { path: plugin.path },
1139
+ ...plugin.ref === void 0 ? {} : { ref: plugin.ref }
1140
+ });
1141
+ return {
1142
+ root: nested.root,
1143
+ source: {
1144
+ kind,
1145
+ source: marketplace,
1146
+ sourceId: sourceId({
1147
+ kind,
1148
+ marketplace,
1149
+ marketplaceRef,
1150
+ plugin: plugin.selector,
1151
+ source: plugin.source,
1152
+ path: plugin.path,
1153
+ ref: plugin.ref
1154
+ }),
1155
+ ...plugin.ref === void 0 ? {} : { ref: plugin.ref },
1156
+ ...plugin.path === void 0 ? {} : { path: plugin.path },
1157
+ marketplace
1158
+ },
1159
+ dispose: nested.dispose
1160
+ };
1161
+ }
1162
+ async function marketplaceGitPlugin(root, request) {
1163
+ const candidate = request.path === void 0 ? root : await existingInside(root, request.path, "plugin path");
1164
+ if (await hasPluginManifest(candidate)) return void 0;
1165
+ const marketplacePath = await optionalMarketplaceManifestPath(candidate);
1166
+ if (marketplacePath === void 0) return void 0;
1167
+ const selected = await selectMarketplacePlugin(candidate, request.plugin, marketplacePath);
1168
+ const source = isRecord(selected.source) ? selected.source : void 0;
1169
+ if (source === void 0 || source.source === "local") return void 0;
1170
+ const remote = typeof source.url === "string" ? source.url : typeof source.source === "string" && source.source !== "git-subdir" ? source.source : void 0;
1171
+ if (remote === void 0) throw new Error("plugin runtime: marketplace Git plugin requires source.url");
1172
+ const path = typeof source.path === "string" ? source.path : void 0;
1173
+ const ref = typeof source.ref === "string" ? source.ref : typeof source.sha === "string" ? source.sha : void 0;
1174
+ return {
1175
+ source: remote,
1176
+ ...path === void 0 ? {} : { path },
1177
+ ...ref === void 0 ? {} : { ref },
1178
+ selector: typeof selected.id === "string" ? selected.id : string(selected.name)
1179
+ };
1180
+ }
1181
+ async function selectMarketplacePlugin(root, plugin, knownPath) {
1182
+ const marketplace = await readBoundedJson(knownPath ?? await marketplaceManifestPath(root), MAX_MARKETPLACE_BYTES, "marketplace manifest");
1183
+ if (!isRecord(marketplace) || !Array.isArray(marketplace.plugins)) throw new Error("plugin runtime: marketplace.json requires a plugins array");
1184
+ const plugins = marketplace.plugins.filter(isRecord);
1185
+ const selected = plugin === void 0 ? plugins.length === 1 ? plugins[0] : void 0 : plugins.find((entry) => entry.name === plugin || entry.id === plugin);
1186
+ if (selected === void 0) throw new Error("plugin runtime: select a marketplace plugin by name");
1187
+ return selected;
1188
+ }
1189
+ function marketplaceLocalPluginPath(selected) {
1190
+ const source = isRecord(selected.source) ? selected.source : void 0;
1191
+ return typeof selected.path === "string" ? selected.path : typeof selected.source === "string" ? selected.source : typeof selected.directory === "string" ? selected.directory : source?.source === "local" && typeof source.path === "string" ? source.path : void 0;
1192
+ }
1193
+ async function marketplaceManifestPath(root) {
1194
+ const path = await optionalMarketplaceManifestPath(root);
1195
+ if (path !== void 0) return path;
1196
+ throw new Error("plugin runtime: marketplace.json is missing");
1197
+ }
1198
+ async function optionalMarketplaceManifestPath(root) {
1199
+ for (const path of [
1200
+ ".agents/plugins/marketplace.json",
1201
+ ".agents/plugins/api_marketplace.json",
1202
+ "marketplace.json"
1203
+ ]) if (await exists(join(root, path))) return await existingInside(root, path, "marketplace manifest");
1204
+ }
1205
+ async function isDirectPluginRoot(root, request) {
1206
+ return await hasPluginManifest(request.path === void 0 ? root : resolveInside(root, request.path, "plugin path"));
1207
+ }
1208
+ async function hasPluginManifest(root) {
1209
+ return await findManifestLocation(root) !== void 0;
1210
+ }
1211
+ async function pluginManifestPath(root) {
1212
+ const location = await findManifestLocation(root);
1213
+ if (location !== void 0) return location;
1214
+ throw new Error("plugin runtime: plugin manifest is missing (expected .codex-plugin/plugin.json, .claude-plugin/plugin.json, or plugin.json)");
1215
+ }
1216
+ async function findManifestLocation(root) {
1217
+ const portablePath = join(root, "plugin.json");
1218
+ if (await exists(portablePath)) {
1219
+ const info = await lstat(portablePath);
1220
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error("plugin runtime: root plugin.json must be a regular file");
1221
+ let raw;
1222
+ try {
1223
+ raw = JSON.parse(await readBoundedText(portablePath, MAX_MANIFEST_BYTES, "plugin manifest"));
1224
+ } catch (error) {
1225
+ if (!await exists(join(root, ".codex-plugin", "plugin.json"))) throw error;
1226
+ raw = void 0;
1227
+ }
1228
+ const schema = isRecord(raw) && typeof raw.$schema === "string" ? raw.$schema : void 0;
1229
+ if (schema === AGENT_PLUGIN_SCHEMA_URI) return {
1230
+ path: await existingInside(root, "plugin.json", "plugin manifest"),
1231
+ format: "portable"
1232
+ };
1233
+ if (schema?.startsWith(AGENT_PLUGIN_SCHEMA_PREFIX)) throw new Error(`plugin runtime: unsupported Agent Plugins schema ${JSON.stringify(schema)}`);
1234
+ }
1235
+ if (await exists(join(root, ".codex-plugin", "plugin.json"))) return {
1236
+ path: await existingInside(root, ".codex-plugin/plugin.json", "plugin manifest"),
1237
+ format: "legacy"
1238
+ };
1239
+ if (await exists(join(root, ".claude-plugin", "plugin.json"))) return {
1240
+ path: await existingInside(root, ".claude-plugin/plugin.json", "plugin manifest"),
1241
+ format: "claude"
1242
+ };
1243
+ if (await exists(join(root, ".claude-plugin"))) throw new Error("plugin runtime: Claude plugin manifest is missing (expected .claude-plugin/plugin.json)");
1244
+ }
1245
+ function parseManifest(raw, format) {
1246
+ if (format === "portable") return parsePortableManifest(raw);
1247
+ if (format === "claude") return parseClaudeManifest(raw);
1248
+ if (!isRecord(raw) || !PLUGIN_NAME.test(string(raw.name)) || !VERSION.test(string(raw.version))) throw new Error("plugin runtime: plugin manifest requires a valid name and semantic version");
1249
+ for (const key of [
1250
+ "description",
1251
+ "application",
1252
+ "homepage",
1253
+ "repository",
1254
+ "license"
1255
+ ]) if (raw[key] !== void 0 && typeof raw[key] !== "string") throw new Error(`plugin runtime: manifest ${key} must be a string`);
1256
+ if (raw.keywords !== void 0 && (!Array.isArray(raw.keywords) || raw.keywords.some((value) => typeof value !== "string"))) throw new Error("plugin runtime: manifest keywords must be a string array");
1257
+ if (raw.author !== void 0 && typeof raw.author !== "string" && !isRecord(raw.author)) throw new Error("plugin runtime: manifest author must be a string or object");
1258
+ if (raw.interface !== void 0 && !isRecord(raw.interface)) throw new Error("plugin runtime: manifest interface must be an object");
1259
+ validateManifestPaths(raw);
1260
+ return {
1261
+ ...raw,
1262
+ format: "legacy"
1263
+ };
1264
+ }
1265
+ function parseClaudeManifest(raw) {
1266
+ if (!isRecord(raw)) throw new Error("plugin runtime: Claude plugin manifest must be an object");
1267
+ const name = string(raw.name).trim();
1268
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(name)) throw new Error(`plugin runtime: invalid Claude plugin name ${JSON.stringify(name)}`);
1269
+ const version = raw.version === void 0 ? "unknown" : string(raw.version).trim();
1270
+ if (!isSafeVersionDirectory(version)) throw new Error("plugin runtime: Claude plugin version is not a safe directory name");
1271
+ for (const key of [
1272
+ "displayName",
1273
+ "description",
1274
+ "application",
1275
+ "homepage",
1276
+ "repository",
1277
+ "license"
1278
+ ]) if (raw[key] !== void 0 && typeof raw[key] !== "string") throw new Error(`plugin runtime: Claude plugin manifest ${key} must be a string`);
1279
+ if (raw.keywords !== void 0 && (!Array.isArray(raw.keywords) || raw.keywords.some((value) => typeof value !== "string"))) throw new Error("plugin runtime: Claude plugin manifest keywords must be a string array");
1280
+ if (raw.author !== void 0 && !isRecord(raw.author)) throw new Error("plugin runtime: Claude plugin manifest author must be an object");
1281
+ for (const key of [
1282
+ "commands",
1283
+ "agents",
1284
+ "lspServers",
1285
+ "monitors",
1286
+ "outputStyles",
1287
+ "themes",
1288
+ "settings",
1289
+ "dependencies",
1290
+ "bin",
1291
+ "apps",
1292
+ "interface"
1293
+ ]) if (raw[key] !== void 0) throw new Error(`plugin runtime: Claude plugin field ${key} is unsupported`);
1294
+ validateManifestPaths(raw);
1295
+ const components = {};
1296
+ if (raw.skills !== void 0) components.skills = raw.skills;
1297
+ if (raw.mcpServers !== void 0) components.mcpServers = raw.mcpServers;
1298
+ if (raw.hooks !== void 0) components.hooks = raw.hooks;
1299
+ return {
1300
+ format: "claude",
1301
+ name,
1302
+ version,
1303
+ ...typeof raw.description === "string" ? { description: raw.description } : {},
1304
+ ...typeof raw.application === "string" ? { application: raw.application } : {},
1305
+ ...isRecord(raw.author) ? { author: raw.author } : {},
1306
+ ...typeof raw.homepage === "string" ? { homepage: raw.homepage } : {},
1307
+ ...typeof raw.repository === "string" ? { repository: raw.repository } : {},
1308
+ ...typeof raw.license === "string" ? { license: raw.license } : {},
1309
+ ...Array.isArray(raw.keywords) ? { keywords: raw.keywords } : {},
1310
+ ...components
1311
+ };
1312
+ }
1313
+ function parsePortableManifest(raw) {
1314
+ if (!isRecord(raw) || typeof raw.$schema !== "string" || raw.$schema !== AGENT_PLUGIN_SCHEMA_URI) throw new Error(`plugin runtime: root plugin.json requires Agent Plugins schema ${AGENT_PLUGIN_SCHEMA_URI}`);
1315
+ const name = string(raw.name).trim();
1316
+ if (!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/u.test(name) || name.includes("..") || name.includes("--")) throw new Error(`plugin runtime: invalid Agent Plugins name ${JSON.stringify(name)}`);
1317
+ const version = raw.version === void 0 ? "1.0.0" : string(raw.version).trim();
1318
+ if (!isSafeVersionDirectory(version)) throw new Error("plugin runtime: portable plugin version is not a safe directory name");
1319
+ for (const key of [
1320
+ "description",
1321
+ "application",
1322
+ "homepage",
1323
+ "repository",
1324
+ "license"
1325
+ ]) if (raw[key] !== void 0 && typeof raw[key] !== "string") throw new Error(`plugin runtime: manifest ${key} must be a string`);
1326
+ if (raw.keywords !== void 0 && (!Array.isArray(raw.keywords) || raw.keywords.some((value) => typeof value !== "string"))) throw new Error("plugin runtime: manifest keywords must be a string array");
1327
+ if (raw.author !== void 0 && typeof raw.author !== "string" && !isRecord(raw.author)) throw new Error("plugin runtime: manifest author must be a string or object");
1328
+ const extension = isRecord(raw.extensions) && isRecord(raw.extensions["com.openai"]) ? raw.extensions["com.openai"] : void 0;
1329
+ const presentation = extension?.interface ?? raw.interface;
1330
+ if (presentation !== void 0 && !isRecord(presentation)) throw new Error("plugin runtime: manifest interface must be an object");
1331
+ const component = (key) => extension?.[key] ?? raw[key];
1332
+ const optional = {};
1333
+ if (typeof raw.description === "string") optional.description = raw.description;
1334
+ if (typeof raw.application === "string") optional.application = raw.application;
1335
+ if (typeof raw.author === "string" || isRecord(raw.author)) optional.author = raw.author;
1336
+ if (typeof raw.homepage === "string") optional.homepage = raw.homepage;
1337
+ if (typeof raw.repository === "string") optional.repository = raw.repository;
1338
+ if (typeof raw.license === "string") optional.license = raw.license;
1339
+ if (Array.isArray(raw.keywords)) optional.keywords = raw.keywords;
1340
+ const skills = raw.skills === void 0 ? "./skills" : raw.skills;
1341
+ const mcpServers = raw.mcpServers === void 0 ? "./mcp.json" : raw.mcpServers;
1342
+ const manifest = {
1343
+ format: "portable",
1344
+ name,
1345
+ version,
1346
+ ...optional,
1347
+ skills,
1348
+ mcpServers,
1349
+ ...component("apps") === void 0 ? {} : { apps: component("apps") },
1350
+ ...component("hooks") === void 0 ? {} : { hooks: component("hooks") },
1351
+ ...isRecord(presentation) ? { interface: presentation } : {}
1352
+ };
1353
+ validateManifestPaths(manifest);
1354
+ return manifest;
1355
+ }
1356
+ function isSafeVersionDirectory(version) {
1357
+ return /^[a-z0-9][a-z0-9._+-]*$/iu.test(version) && !version.endsWith(".") && !/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(version);
1358
+ }
1359
+ function validateManifestPaths(manifest) {
1360
+ for (const key of [
1361
+ "skills",
1362
+ "mcpServers",
1363
+ "apps",
1364
+ "hooks"
1365
+ ]) {
1366
+ const value = manifest[key];
1367
+ if (typeof value === "string") assertRelativePluginPath(value, `manifest ${key}`);
1368
+ if (Array.isArray(value)) {
1369
+ for (const item of value) if (typeof item === "string") assertRelativePluginPath(item, `manifest ${key}`);
1370
+ }
1371
+ }
1372
+ const ui = isRecord(manifest.interface) ? manifest.interface : void 0;
1373
+ if (ui === void 0) return;
1374
+ for (const key of ["composerIcon", "logo"]) if (typeof ui[key] === "string") assertRelativePluginPath(ui[key], `manifest interface.${key}`);
1375
+ if (ui.screenshots !== void 0) for (const path of stringArray(ui.screenshots, "manifest interface.screenshots")) assertRelativePluginPath(path, "manifest interface.screenshots");
1376
+ }
1377
+ async function rejectUnsupportedClaudeComponents(root) {
1378
+ for (const path of [
1379
+ "agents",
1380
+ "output-styles",
1381
+ "themes",
1382
+ "monitors",
1383
+ "bin",
1384
+ ".lsp.json",
1385
+ "settings.json"
1386
+ ]) if (await exists(join(root, path))) throw new Error(`plugin runtime: Claude plugin component ${path} is unsupported`);
1387
+ }
1388
+ /** Read the optional OpenAI presentation/policy document beside a plugin or skill. */
1389
+ async function discoverOpenAiMetadata(root) {
1390
+ const relativePath = "agents/openai.yaml";
1391
+ const path = join(root, relativePath);
1392
+ if (!await exists(path)) return void 0;
1393
+ const info = await lstat(path);
1394
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error(`plugin runtime: ${relativePath} must be a regular file`);
1395
+ let parsed;
1396
+ try {
1397
+ parsed = parse$1(await readBoundedText(path, MAX_MANIFEST_BYTES, relativePath));
1398
+ } catch (error) {
1399
+ throw new Error(`plugin runtime: ${relativePath} is invalid YAML`, { cause: error });
1400
+ }
1401
+ if (!isRecord(parsed)) throw new Error(`plugin runtime: ${relativePath} must contain a YAML object`);
1402
+ const presentation = parsed.interface;
1403
+ if (presentation !== void 0 && !isRecord(presentation)) throw new Error(`plugin runtime: ${relativePath} interface must be an object`);
1404
+ const policy = parsed.policy;
1405
+ if (policy !== void 0 && !isRecord(policy)) throw new Error(`plugin runtime: ${relativePath} policy must be an object`);
1406
+ const values = {};
1407
+ for (const [source, target] of [
1408
+ ["display_name", "displayName"],
1409
+ ["short_description", "shortDescription"],
1410
+ ["icon_small", "iconSmall"],
1411
+ ["icon_large", "iconLarge"],
1412
+ ["brand_color", "brandColor"],
1413
+ ["default_prompt", "defaultPrompt"]
1414
+ ]) {
1415
+ const value = presentation?.[source];
1416
+ if (value === void 0) continue;
1417
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`plugin runtime: ${relativePath} interface.${source} must be a non-empty string`);
1418
+ if (source === "icon_small" || source === "icon_large") assertRelativePluginPath(value, `${relativePath} interface.${source}`);
1419
+ values[target] = value;
1420
+ }
1421
+ const allowImplicitInvocation = policy?.allow_implicit_invocation;
1422
+ if (allowImplicitInvocation !== void 0) {
1423
+ if (typeof allowImplicitInvocation !== "boolean") throw new Error(`plugin runtime: ${relativePath} policy.allow_implicit_invocation must be a boolean`);
1424
+ values.allowImplicitInvocation = allowImplicitInvocation;
1425
+ }
1426
+ if (parsed.dependencies !== void 0 && !isRecord(parsed.dependencies)) throw new Error(`plugin runtime: ${relativePath} dependencies must be an object`);
1427
+ return Object.keys(values).length === 0 ? void 0 : values;
1428
+ }
1429
+ async function discoverSkills(root, manifest, pluginAgentMetadata) {
1430
+ const paths = stringPaths(manifest.skills);
1431
+ if (paths.length === 0) {
1432
+ if (await exists(join(root, "skills"))) paths.push("skills");
1433
+ else if (manifest.format === "claude" && await exists(join(root, "SKILL.md"))) paths.push("SKILL.md");
1434
+ }
1435
+ const skills = [];
1436
+ for (const path of paths) {
1437
+ if (manifest.format === "portable" && path === "./skills" && !await exists(join(root, path))) continue;
1438
+ const location = await existingInside(root, path, "skill path");
1439
+ const files = (await lstat(location)).isDirectory() ? (await readdir(location, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => join(location, entry.name, "SKILL.md")) : [location];
1440
+ for (const file of files) {
1441
+ if (basename(file) !== "SKILL.md" || !await exists(file)) continue;
1442
+ const resolved = await existingInside(root, relative(root, file), "skill file");
1443
+ const parsed = parseSkillDocument(await readBoundedText(resolved, MAX_FILE_BYTES, "skill file"));
1444
+ const agentMetadata = await discoverOpenAiMetadata(dirname(resolved));
1445
+ const openAiModelInvocable = pluginAgentMetadata?.allowImplicitInvocation !== false && agentMetadata?.allowImplicitInvocation !== false;
1446
+ skills.push({
1447
+ rawName: parsed.name,
1448
+ description: parsed.description,
1449
+ invocation: {
1450
+ ...parsed.invocation,
1451
+ modelInvocable: parsed.invocation.modelInvocable && openAiModelInvocable
1452
+ },
1453
+ ...parsed.whenToUse === void 0 ? {} : { whenToUse: parsed.whenToUse },
1454
+ path: resolved,
1455
+ directory: dirname(resolved),
1456
+ ...agentMetadata === void 0 ? {} : { agentMetadata },
1457
+ openAiModelInvocable
1458
+ });
1459
+ }
1460
+ }
1461
+ return skills.sort((left, right) => left.rawName.localeCompare(right.rawName));
1462
+ }
1463
+ async function discoverCommands(root) {
1464
+ const directory = join(root, "commands");
1465
+ if (!await exists(directory)) return [];
1466
+ const info = await lstat(directory);
1467
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("plugin runtime: commands must be a real directory");
1468
+ const commands = [];
1469
+ const entries = (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.isFile() && (extname(entry.name) === ".toml" || extname(entry.name) === ".md")).sort((left, right) => left.name.localeCompare(right.name));
1470
+ const tomlNames = new Set(entries.filter((entry) => extname(entry.name) === ".toml").map((entry) => entry.name.slice(0, -5)));
1471
+ for (const entry of entries) {
1472
+ const extension = extname(entry.name);
1473
+ const name = entry.name.slice(0, -extension.length);
1474
+ if (extension === ".md" && tomlNames.has(name)) continue;
1475
+ if (!COMMAND_NAME.test(name)) throw new Error(`plugin runtime: command filename "${entry.name}" must use lower kebab-case`);
1476
+ const path = await existingInside(root, join("commands", entry.name), "command");
1477
+ if (extension === ".md") {
1478
+ commands.push(parseMarkdownCommand(name, await readBoundedText(path, MAX_FILE_BYTES, "command"), entry.name));
1479
+ continue;
1480
+ }
1481
+ let definition;
1482
+ try {
1483
+ definition = parse(await readBoundedText(path, MAX_FILE_BYTES, "command"));
1484
+ } catch (error) {
1485
+ throw new Error(`plugin runtime: command "${entry.name}" is invalid TOML`, { cause: error });
1486
+ }
1487
+ if (!isRecord(definition) || typeof definition.description !== "string" || definition.description.trim() === "" || typeof definition.prompt !== "string" || definition.prompt.trim() === "") throw new Error(`plugin runtime: command "${entry.name}" requires non-empty string description and prompt`);
1488
+ commands.push({
1489
+ name,
1490
+ description: definition.description,
1491
+ prompt: definition.prompt
1492
+ });
1493
+ }
1494
+ return commands.sort((left, right) => left.name.localeCompare(right.name));
1495
+ }
1496
+ /** Parse one Codex/Claude-style Markdown command without executing metadata. */
1497
+ function parseMarkdownCommand(name, raw, filename) {
1498
+ const match = /^---\r?\n([\s\S]*?\r?\n)---(?:\r?\n|$)/u.exec(raw);
1499
+ let metadata = {};
1500
+ let prompt = raw;
1501
+ if (match !== null) {
1502
+ try {
1503
+ /* v8 ignore next -- the frontmatter regex always captures its YAML body. */
1504
+ const parsed = parse$1(match[1] ?? "");
1505
+ if (!isRecord(parsed)) throw new Error("frontmatter must be a YAML object");
1506
+ metadata = parsed;
1507
+ } catch (error) {
1508
+ throw new Error(`plugin runtime: command "${filename}" has invalid YAML frontmatter`, { cause: error });
1509
+ }
1510
+ prompt = raw.slice(match[0].length);
1511
+ }
1512
+ prompt = prompt.trim();
1513
+ if (prompt === "") throw new Error(`plugin runtime: command "${filename}" requires a non-empty Markdown body`);
1514
+ const description = metadata.description === void 0 ? firstMarkdownLine(prompt) : typeof metadata.description === "string" ? metadata.description.trim() : "";
1515
+ if (description === "") throw new Error(`plugin runtime: command "${filename}" requires a non-empty string description`);
1516
+ const hint = metadata["argument-hint"];
1517
+ if (hint !== void 0 && (typeof hint !== "string" || hint.trim() === "")) throw new Error(`plugin runtime: command "${filename}" argument-hint must be a non-empty string`);
1518
+ return {
1519
+ name,
1520
+ description,
1521
+ prompt,
1522
+ ...typeof hint === "string" ? { inputHint: hint.trim() } : {}
1523
+ };
1524
+ }
1525
+ function firstMarkdownLine(prompt) {
1526
+ return (prompt.split(/\r?\n/u)[0] ?? "").replace(/^#{1,6}\s+/u, "").trim();
1527
+ }
1528
+ async function discoverMcp(root, manifest, identity) {
1529
+ const documents = [];
1530
+ const sources = stringPaths(manifest.mcpServers);
1531
+ if (manifest.mcpServers === void 0 && await exists(join(root, ".mcp.json"))) sources.push(".mcp.json");
1532
+ for (const path of sources) {
1533
+ if (manifest.format === "portable" && path === "./mcp.json" && !await exists(join(root, path))) continue;
1534
+ documents.push(await readBoundedJson(await existingInside(root, path, "MCP configuration"), MAX_MANIFEST_BYTES, "MCP configuration"));
1535
+ }
1536
+ if (isRecord(manifest.mcpServers)) documents.push(manifest.mcpServers);
1537
+ const result = [];
1538
+ for (const document of documents) {
1539
+ const map = normalizeMcpMap(document);
1540
+ for (const [name, definition] of Object.entries(map)) {
1541
+ if (!isRecord(definition)) throw new Error(`plugin runtime: MCP server ${name} must be an object`);
1542
+ result.push({
1543
+ name,
1544
+ config: normalizeMcpConfig(root, identity, name, definition)
1545
+ });
1546
+ }
1547
+ }
1548
+ const seen = /* @__PURE__ */ new Set();
1549
+ for (const server of result) {
1550
+ if (seen.has(server.name)) throw new Error(`plugin runtime: duplicate MCP server ${server.name}`);
1551
+ seen.add(server.name);
1552
+ }
1553
+ return result;
1554
+ }
1555
+ function normalizeMcpMap(raw) {
1556
+ if (!isRecord(raw)) throw new Error("plugin runtime: MCP configuration must be an object");
1557
+ if (isRecord(raw.mcp_servers)) return raw.mcp_servers;
1558
+ if (isRecord(raw.mcpServers)) return raw.mcpServers;
1559
+ return raw;
1560
+ }
1561
+ function normalizeMcpConfig(root, identity, name, raw) {
1562
+ const serverName = mcpServerName(identity, name);
1563
+ const type = raw.type === void 0 ? void 0 : string(raw.type).trim().toLowerCase();
1564
+ if (raw.type !== void 0 && type !== "stdio" && type !== "http" && type !== "streamable_http" && type !== "streamable-http") throw new Error(`plugin runtime: MCP server ${name} has unsupported transport type`);
1565
+ if (type === "http" || type === "streamable_http" || type === "streamable-http" || type === void 0 && typeof raw.url === "string") {
1566
+ if (typeof raw.url !== "string" || raw.url.trim() === "") throw new Error(`plugin runtime: MCP server ${name} requires url for HTTP transport`);
1567
+ if (raw.oauth !== void 0 || raw.oauth_resource !== void 0) throw new Error(`plugin runtime: MCP server ${name} OAuth metadata is not supported by the local MCP client`);
1568
+ const bearerTokenEnvVar = raw.bearer_token_env_var === void 0 ? void 0 : environmentVariableName(raw.bearer_token_env_var, `MCP ${name}.bearer_token_env_var`);
1569
+ return {
1570
+ transport: "streamable-http",
1571
+ serverName,
1572
+ url: raw.url,
1573
+ headers: stringRecord(raw.headers ?? raw.http_headers, `MCP ${name}.headers`),
1574
+ ...bearerTokenEnvVar === void 0 ? {} : { bearerTokenEnvVar },
1575
+ toolCallTimeoutMs: 6e4,
1576
+ failOnStartupError: true
1577
+ };
1578
+ }
1579
+ if (typeof raw.command !== "string" || raw.command.trim() === "") throw new Error(`plugin runtime: MCP server ${name} requires command or url`);
1580
+ if (raw.bearer_token_env_var !== void 0 || raw.oauth !== void 0 || raw.oauth_resource !== void 0) throw new Error(`plugin runtime: MCP server ${name} authentication metadata requires HTTP transport`);
1581
+ const cwd = raw.cwd === void 0 ? root : resolveInside(root, string(raw.cwd), `MCP ${name}.cwd`);
1582
+ return {
1583
+ transport: "stdio",
1584
+ serverName,
1585
+ command: raw.command,
1586
+ args: raw.args === void 0 ? [] : stringArray(raw.args, `MCP ${name}.args`),
1587
+ env: stringRecord(raw.env, `MCP ${name}.env`),
1588
+ cwd,
1589
+ toolCallTimeoutMs: 6e4,
1590
+ failOnStartupError: true
1591
+ };
1592
+ }
1593
+ async function discoverHooks(root, manifest) {
1594
+ const hooks = [];
1595
+ if (manifest.hooks === void 0) for (const entry of ["hooks/hooks.json", "hooks.json"]) {
1596
+ if (!await exists(join(root, entry))) continue;
1597
+ hooks.push({
1598
+ label: entry,
1599
+ raw: record(await readBoundedJson(await existingInside(root, entry, "hooks configuration"), MAX_MANIFEST_BYTES, "hooks configuration"), "hooks configuration")
1600
+ });
1601
+ }
1602
+ const value = manifest.hooks;
1603
+ const entries = Array.isArray(value) ? value : value === void 0 ? [] : [value];
1604
+ for (const [index, entry] of entries.entries()) if (typeof entry === "string") {
1605
+ const path = await existingInside(root, entry, "hooks configuration");
1606
+ hooks.push({
1607
+ label: entry,
1608
+ raw: record(await readBoundedJson(path, MAX_MANIFEST_BYTES, "hooks configuration"), "hooks configuration")
1609
+ });
1610
+ } else if (isRecord(entry)) hooks.push({
1611
+ label: `manifest hooks ${String(index + 1)}`,
1612
+ raw: entry
1613
+ });
1614
+ else throw new Error("plugin runtime: manifest hooks must be paths or objects");
1615
+ for (const hook of hooks) {
1616
+ const parsed = parseCodexConfig(hook.raw);
1617
+ if (Object.keys(parsed.config).length === 0) throw new Error(`plugin runtime: ${hook.label} contains no supported synchronous command hooks`);
1618
+ }
1619
+ return hooks;
1620
+ }
1621
+ async function discoverApps(root, manifest) {
1622
+ const mappings = {};
1623
+ if (await exists(join(root, ".app.json"))) Object.assign(mappings, record(await readBoundedJson(await existingInside(root, ".app.json", ".app.json"), MAX_MANIFEST_BYTES, ".app.json"), ".app.json"));
1624
+ if (typeof manifest.apps === "string" || Array.isArray(manifest.apps)) for (const path of stringPaths(manifest.apps)) Object.assign(mappings, record(await readBoundedJson(await existingInside(root, path, "app mapping"), MAX_MANIFEST_BYTES, "app mapping"), "app mapping"));
1625
+ else if (isRecord(manifest.apps)) Object.assign(mappings, manifest.apps);
1626
+ return Object.keys(mappings).length === 0 ? void 0 : mappings;
1627
+ }
1628
+ function mergeHookDefinitions(definitions) {
1629
+ const hooks = {};
1630
+ for (const definition of definitions) {
1631
+ const source = isRecord(definition.raw.hooks) ? definition.raw.hooks : definition.raw;
1632
+ for (const [event, groups] of Object.entries(source)) {
1633
+ if (!Array.isArray(groups)) continue;
1634
+ hooks[event] = [...hooks[event] ?? [], ...groups];
1635
+ }
1636
+ }
1637
+ return { hooks };
1638
+ }
1639
+ function initializeMcpState(definitions, old) {
1640
+ return Object.fromEntries(definitions.map(({ name }) => [name, old?.[name] ?? {
1641
+ enabled: false,
1642
+ defaultToolsApprovalMode: "ask",
1643
+ toolApproval: {}
1644
+ }]));
1645
+ }
1646
+ async function validateBundleTree(root) {
1647
+ const metadata = await lstat(root);
1648
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("plugin runtime: plugin root must be a real directory");
1649
+ const canonical = await realpath(root);
1650
+ let total = 0;
1651
+ let count = 0;
1652
+ const walk = async (directory) => {
1653
+ if (!isInside(canonical, await realpath(directory))) throw new Error("plugin runtime: symbolic link or junction escapes plugin root");
1654
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
1655
+ const path = join(directory, entry.name);
1656
+ const info = await lstat(path);
1657
+ if (info.isSymbolicLink()) throw new Error(`plugin runtime: symbolic links are not allowed (${path})`);
1658
+ count += 1;
1659
+ if (count > MAX_FILES) throw new Error(`plugin runtime: plugin contains more than ${String(MAX_FILES)} files`);
1660
+ if (info.isDirectory()) await walk(path);
1661
+ else if (info.isFile()) {
1662
+ if (info.size > MAX_FILE_BYTES) throw new Error(`plugin runtime: file exceeds ${String(MAX_FILE_BYTES)} bytes`);
1663
+ total += info.size;
1664
+ if (total > MAX_PACKAGE_BYTES) throw new Error(`plugin runtime: package exceeds ${String(MAX_PACKAGE_BYTES)} bytes`);
1665
+ } else throw new Error(`plugin runtime: unsupported filesystem entry (${path})`);
1666
+ }
1667
+ };
1668
+ await walk(canonical);
1669
+ return canonical;
1670
+ }
1671
+ async function copyTree(source, destination) {
1672
+ await mkdir(destination, {
1673
+ recursive: true,
1674
+ mode: 448
1675
+ });
1676
+ for (const entry of await readdir(source, { withFileTypes: true })) {
1677
+ const from = join(source, entry.name);
1678
+ const to = join(destination, entry.name);
1679
+ const info = await lstat(from);
1680
+ if (info.isSymbolicLink()) throw new Error("plugin runtime: symbolic links are not allowed");
1681
+ if (info.isDirectory()) await copyTree(from, to);
1682
+ else if (info.isFile()) await copyFile(from, to);
1683
+ else throw new Error("plugin runtime: unsupported filesystem entry");
1684
+ }
1685
+ }
1686
+ function assertRelativePluginPath(path, label) {
1687
+ if (path.length === 0 || path.includes("\0") || isAbsolute(path) || /^[A-Za-z]:/u.test(path) || /^\\\\/u.test(path) || path.startsWith("/") || path.startsWith("\\")) throw new Error(`plugin runtime: ${label} must be relative to the plugin root`);
1688
+ if (path.replaceAll("\\", "/").replace(/\/+$/u, "").split("/").some((segment) => segment === ".." || segment === "")) throw new Error(`plugin runtime: ${label} cannot traverse outside the plugin root`);
1689
+ }
1690
+ function resolveInside(root, path, label) {
1691
+ assertRelativePluginPath(path, label);
1692
+ return resolve(root, path);
1693
+ }
1694
+ async function existingInside(root, path, label) {
1695
+ const candidate = resolveInside(root, path, label);
1696
+ if ((await lstat(candidate)).isSymbolicLink()) throw new Error(`plugin runtime: ${label} must not be a symbolic link`);
1697
+ const actual = await realpath(candidate);
1698
+ if (!isInside(await realpath(root), actual)) throw new Error(`plugin runtime: ${label} escapes plugin root`);
1699
+ return actual;
1700
+ }
1701
+ function assertStoreDescendant(root, path) {
1702
+ if (!isInside(resolve(root), resolve(path))) throw new Error("plugin runtime: refusing to remove outside the plugin store");
1703
+ }
1704
+ function isInside(root, path) {
1705
+ const child = relative(resolve(root), resolve(path));
1706
+ return child === "" || !child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute(child);
1707
+ }
1708
+ async function readBoundedJson(path, limit, label) {
1709
+ return JSON.parse(await readBoundedText(path, limit, label));
1710
+ }
1711
+ async function readBoundedText(path, limit, label) {
1712
+ const info = await lstat(path);
1713
+ if (!info.isFile() || info.isSymbolicLink() || info.size > limit) throw new Error(`plugin runtime: ${label} is invalid or too large`);
1714
+ const text = await readFile(path, "utf8");
1715
+ if (Buffer.byteLength(text) > limit) throw new Error(`plugin runtime: ${label} is too large`);
1716
+ return text;
1717
+ }
1718
+ function string(value) {
1719
+ return typeof value === "string" ? value : "";
1720
+ }
1721
+ function isRecord(value) {
1722
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1723
+ }
1724
+ function record(value, label) {
1725
+ if (!isRecord(value)) throw new Error(`plugin runtime: ${label} must be an object`);
1726
+ return value;
1727
+ }
1728
+ function stringArray(value, label) {
1729
+ const items = Array.isArray(value) ? value : [];
1730
+ if (!Array.isArray(value) || items.some((item) => typeof item !== "string")) throw new Error(`plugin runtime: ${label} must be a string array`);
1731
+ return items.filter((item) => typeof item === "string");
1732
+ }
1733
+ function stringRecord(value, label) {
1734
+ if (value === void 0) return {};
1735
+ if (!isRecord(value) || Object.values(value).some((item) => typeof item !== "string")) throw new Error(`plugin runtime: ${label} must be a string map`);
1736
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, item]));
1737
+ }
1738
+ function environmentVariableName(value, label) {
1739
+ if (typeof value !== "string" || value.length === 0 || /[=\u0000-\u001F\u007F]/u.test(value)) throw new Error(`plugin runtime: ${label} must be a valid environment variable name`);
1740
+ return value;
1741
+ }
1742
+ function stringPaths(value) {
1743
+ if (value === void 0 || isRecord(value)) return [];
1744
+ const paths = typeof value === "string" ? [value] : stringArray(value, "manifest paths");
1745
+ for (const path of paths) assertRelativePluginPath(path, "manifest path");
1746
+ return paths;
1747
+ }
1748
+ function isMissing(error) {
1749
+ return error?.code === "ENOENT" || error?.code === "ENOTDIR";
1750
+ }
1751
+ async function exists(path) {
1752
+ try {
1753
+ await lstat(path);
1754
+ return true;
1755
+ } catch (error) {
1756
+ if (isMissing(error)) return false;
1757
+ throw error;
1758
+ }
1759
+ }
1760
+ function stableJson(value) {
1761
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1762
+ if (isRecord(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
1763
+ return JSON.stringify(value) ?? "null";
1764
+ }
1765
+ //#endregion
1766
+ export { ImportedPluginRuntime, ImportedPluginRuntime as default, PluginLifecycleManager, PluginManifestLoader, PluginRegistry, PluginStore, PluginTrustStore, inject, name };