@hydraharness/harness-host-plugin-inventory 0.1.1-rc.10

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,1230 @@
1
+ /** Runtime plugin inventory plus shared user-setting enablement controls. */
2
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
3
+ var useValue = arguments.length > 2;
4
+ for (var i = 0; i < initializers.length; i++) {
5
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
6
+ }
7
+ return useValue ? value : void 0;
8
+ };
9
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
10
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
11
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
12
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
13
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
14
+ var _, done = false;
15
+ for (var i = decorators.length - 1; i >= 0; i--) {
16
+ var context = {};
17
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
18
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
19
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
20
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
21
+ if (kind === "accessor") {
22
+ if (result === void 0) continue;
23
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
24
+ if (_ = accept(result.get)) descriptor.get = _;
25
+ if (_ = accept(result.set)) descriptor.set = _;
26
+ if (_ = accept(result.init)) initializers.unshift(_);
27
+ }
28
+ else if (_ = accept(result)) {
29
+ if (kind === "field") initializers.unshift(_);
30
+ else descriptor[key] = _;
31
+ }
32
+ }
33
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
34
+ done = true;
35
+ };
36
+ import { execFile } from 'node:child_process';
37
+ import { lstat, mkdtemp, readFile, realpath, rm, stat } from 'node:fs/promises';
38
+ import { createRequire } from 'node:module';
39
+ import { tmpdir } from 'node:os';
40
+ import { basename, dirname, join, resolve } from 'node:path';
41
+ import { fileURLToPath } from 'node:url';
42
+ import { Service } from '@hydraharness/cordis';
43
+ import { profilePluginEnablement, readProfileManifest, resolveProfileDir } from '@hydraharness/harness-app-boot';
44
+ import { withFileLock, writeFileAtomic } from '@hydraharness/harness-atomic-write';
45
+ import { settingsNamespace } from '@hydraharness/harness-settings';
46
+ import { TypertRemoteService, Remote } from '@hydraharness/harness-typert-protocol';
47
+ import z from '@hydraharness/schemastery';
48
+ import { z as zod } from 'zod';
49
+ /** Brand an existing Loader-tree entry id at the owning boundary. */
50
+ function pluginEntryId(value) {
51
+ return value;
52
+ }
53
+ /** Runtime mirror: FiberState is a cross-package const enum. */
54
+ const FIBER_STATE = {
55
+ PENDING: 0,
56
+ LOADING: 1,
57
+ ACTIVE: 2,
58
+ FAILED: 3,
59
+ DISPOSED: 4,
60
+ UNLOADING: 5,
61
+ };
62
+ /** Complete public projection of Cordis Fiber states. */
63
+ const FIBER_PHASE = {
64
+ [FIBER_STATE.PENDING]: 'pending',
65
+ [FIBER_STATE.LOADING]: 'loading',
66
+ [FIBER_STATE.ACTIVE]: 'active',
67
+ [FIBER_STATE.FAILED]: 'failed',
68
+ [FIBER_STATE.DISPOSED]: null,
69
+ [FIBER_STATE.UNLOADING]: 'unloading',
70
+ };
71
+ const PLUGIN_SETTINGS_NAMESPACE = settingsNamespace('plugins');
72
+ const MARKETPLACE_SETTINGS_NAMESPACE = settingsNamespace('plugin-marketplaces');
73
+ const HMR_MODULE = '@hydraharness/cordis-plugin-hmr';
74
+ const AGENT_PRESET_ENTRY_PREFIX = 'agent-preset:';
75
+ const MAX_MARKETPLACE_BYTES = 1024 * 1024;
76
+ const MARKETPLACE_GIT_TIMEOUT_MS = 30_000;
77
+ const MAX_MARKETPLACE_SPARSE_PATHS = 20;
78
+ const MAX_MARKETPLACE_SPARSE_PATH_LENGTH = 512;
79
+ const GITHUB_SHORTHAND_PATTERN = /^([A-Za-z0-9][A-Za-z0-9._-]*)\/([A-Za-z0-9][A-Za-z0-9._-]*)$/u;
80
+ const MarketplaceDocumentSchema = zod.object({
81
+ plugins: zod.array(zod.record(zod.string(), zod.unknown())).max(500),
82
+ }).loose();
83
+ const PluginMetadataSchema = zod.object({
84
+ description: zod.string().trim().min(1).optional(),
85
+ application: zod.string().trim().min(1).optional(),
86
+ });
87
+ const PluginPackageSchema = zod.object({
88
+ description: PluginMetadataSchema.shape.description,
89
+ hydra: zod.object({
90
+ plugin: zod.object({ application: zod.string().trim().min(1).optional() }).optional(),
91
+ }).optional(),
92
+ });
93
+ /** Read presentation metadata without importing or activating the plugin. */
94
+ async function pluginMetadata(moduleName, baseUrl, options = {}) {
95
+ const overrides = PluginMetadataSchema.parse(options);
96
+ const metadata = {
97
+ ...overrides.description === undefined ? {} : { description: overrides.description },
98
+ ...overrides.application === undefined ? {} : { application: overrides.application },
99
+ };
100
+ if (baseUrl === undefined || moduleName.startsWith('.') || moduleName.startsWith('/')
101
+ || moduleName.includes(':') || moduleName.includes('\\'))
102
+ return metadata;
103
+ const packageName = moduleName.split('/').slice(0, moduleName.startsWith('@') ? 2 : 1).join('/');
104
+ let path;
105
+ try {
106
+ path = createRequire(baseUrl).resolve(`${packageName}/package.json`);
107
+ }
108
+ catch (error) {
109
+ // Disabled packages may be absent; third-party packages may hide their manifest.
110
+ if (error instanceof Error && 'code' in error
111
+ && (error.code === 'MODULE_NOT_FOUND' || error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED'))
112
+ return metadata;
113
+ throw error;
114
+ }
115
+ const result = PluginPackageSchema.safeParse(JSON.parse(await readFile(path, 'utf8')));
116
+ if (!result.success)
117
+ throw new Error(`pluginInventory: invalid plugin metadata in ${path}`, { cause: result.error });
118
+ const { description, hydra } = result.data;
119
+ return {
120
+ ...description === undefined ? {} : { description },
121
+ ...hydra?.plugin?.application === undefined ? {} : { application: hydra.plugin.application },
122
+ ...metadata,
123
+ };
124
+ }
125
+ const PluginSettingsSchema = z.object({
126
+ enabled: z.dict(z.boolean()).default({}),
127
+ });
128
+ const MarketplaceSourceSchema = z.object({
129
+ source: z.string(),
130
+ gitRef: z.string().default(''),
131
+ sparsePaths: z.array(z.string()).max(MAX_MARKETPLACE_SPARSE_PATHS).default([]),
132
+ enabled: z.boolean().default(true),
133
+ });
134
+ const MarketplaceSettingsSchema = z.object({
135
+ sources: z.array(MarketplaceSourceSchema).max(20).default([]),
136
+ });
137
+ function missingPath(error) {
138
+ return error instanceof Error
139
+ && 'code' in error
140
+ && (error.code === 'ENOENT' || error.code === 'ENOTDIR');
141
+ }
142
+ function normalizeGitRef(value) {
143
+ const gitRef = value?.trim();
144
+ if (gitRef === undefined || gitRef === '')
145
+ return undefined;
146
+ if (gitRef.length > 255
147
+ || gitRef.startsWith('-')
148
+ || /[\u0000-\u0020\u007F]/u.test(gitRef)) {
149
+ throw new Error('pluginInventory: Git ref is invalid');
150
+ }
151
+ return gitRef;
152
+ }
153
+ function normalizeSparsePaths(values) {
154
+ if ((values?.length ?? 0) > MAX_MARKETPLACE_SPARSE_PATHS) {
155
+ throw new Error(`pluginInventory: at most ${String(MAX_MARKETPLACE_SPARSE_PATHS)} sparse paths are allowed`);
156
+ }
157
+ const paths = (values ?? []).map((value) => {
158
+ const path = value.trim().replaceAll('\\', '/').replace(/\/+$/u, '');
159
+ if (path.length > MAX_MARKETPLACE_SPARSE_PATH_LENGTH) {
160
+ throw new Error(`pluginInventory: sparse paths may contain at most ${String(MAX_MARKETPLACE_SPARSE_PATH_LENGTH)} characters`);
161
+ }
162
+ const segments = path.split('/');
163
+ if (path === ''
164
+ || path.startsWith('/')
165
+ || path.startsWith('-')
166
+ || /^[A-Za-z]:\//u.test(path)
167
+ || segments.some(segment => segment === '' || segment === '.' || segment === '..')) {
168
+ throw new Error(`pluginInventory: sparse path ${JSON.stringify(value)} must be repository-relative`);
169
+ }
170
+ return path;
171
+ });
172
+ return [...new Set(paths)].sort();
173
+ }
174
+ function normalizeGitSource(source) {
175
+ const shorthand = GITHUB_SHORTHAND_PATTERN.exec(source);
176
+ if (shorthand !== null) {
177
+ /* v8 ignore next -- A successful shorthand match contains both required captures. */
178
+ const owner = shorthand[1] ?? '';
179
+ /* v8 ignore next -- A successful shorthand match contains both required captures. */
180
+ const repository = (shorthand[2] ?? '').replace(/\.git$/u, '');
181
+ return `https://github.com/${owner}/${repository}.git`;
182
+ }
183
+ if (/^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:[A-Za-z0-9._~/-]+$/u.test(source))
184
+ return source;
185
+ let url;
186
+ try {
187
+ url = new URL(source);
188
+ }
189
+ catch (cause) {
190
+ throw new Error('pluginInventory: source must be a GitHub repo, Git URL, or existing local folder', { cause });
191
+ }
192
+ if (url.protocol !== 'https:' && url.protocol !== 'ssh:') {
193
+ throw new Error('pluginInventory: remote marketplace source must use HTTPS or SSH');
194
+ }
195
+ if (url.password !== '' || url.hash !== '' || url.search !== ''
196
+ || (url.protocol === 'https:' && url.username !== '')) {
197
+ throw new Error('pluginInventory: marketplace source cannot contain credentials, a query, or a fragment');
198
+ }
199
+ return url.href;
200
+ }
201
+ async function normalizeMarketplaceSource(request) {
202
+ const source = request.source.trim();
203
+ if (source === '' || source.length > 2048 || /[\u0000-\u001F\u007F]/u.test(source)) {
204
+ throw new Error('pluginInventory: marketplace source is invalid');
205
+ }
206
+ const gitRef = normalizeGitRef(request.gitRef);
207
+ const sparsePaths = normalizeSparsePaths(request.sparsePaths);
208
+ const localPath = resolve(source);
209
+ try {
210
+ if (!GITHUB_SHORTHAND_PATTERN.test(source) && (await stat(localPath)).isDirectory()) {
211
+ if (gitRef !== undefined || sparsePaths.length > 0) {
212
+ throw new Error('pluginInventory: Git ref and sparse paths require a Git source');
213
+ }
214
+ return { kind: 'local', source: await realpath(localPath), gitRef: '', sparsePaths: [], enabled: true };
215
+ }
216
+ }
217
+ catch (error) {
218
+ if (!missingPath(error))
219
+ throw error;
220
+ }
221
+ return {
222
+ kind: 'git',
223
+ source: normalizeGitSource(source),
224
+ gitRef: gitRef ?? '',
225
+ sparsePaths,
226
+ enabled: true,
227
+ };
228
+ }
229
+ function marketplaceGitEnv() {
230
+ const env = {};
231
+ for (const [name, value] of Object.entries(process.env)) {
232
+ if (!/(?:KEY|SECRET|TOKEN|PASSWORD)/iu.test(name))
233
+ env[name] = value;
234
+ }
235
+ env.GCM_INTERACTIVE = 'Never';
236
+ env.GIT_TERMINAL_PROMPT = '0';
237
+ return env;
238
+ }
239
+ function runGit(cwd, args) {
240
+ return new Promise((resolvePromise, reject) => {
241
+ execFile('git', [
242
+ '-c', 'protocol.ext.allow=never',
243
+ '-c', 'protocol.file.allow=never',
244
+ ...args,
245
+ ], {
246
+ cwd,
247
+ encoding: 'utf8',
248
+ env: marketplaceGitEnv(),
249
+ maxBuffer: MAX_MARKETPLACE_BYTES,
250
+ timeout: MARKETPLACE_GIT_TIMEOUT_MS,
251
+ windowsHide: true,
252
+ }, (error, _stdout, stderr) => {
253
+ if (error === null) {
254
+ resolvePromise();
255
+ return;
256
+ }
257
+ const detail = stderr.trim().split(/\r?\n/u)[0];
258
+ reject(new Error(`pluginInventory: failed to load Git marketplace${detail === undefined || detail === '' ? '' : `: ${detail}`}`, { cause: error }));
259
+ });
260
+ });
261
+ }
262
+ async function readMarketplaceDocument(root) {
263
+ for (const relativePath of ['.agents/plugins/marketplace.json', '.agents/plugins/api_marketplace.json', 'marketplace.json']) {
264
+ const filename = join(root, relativePath);
265
+ try {
266
+ const metadata = await lstat(filename);
267
+ if (!metadata.isFile())
268
+ continue;
269
+ if (metadata.size > MAX_MARKETPLACE_BYTES) {
270
+ throw new Error(`pluginInventory: marketplace exceeds ${String(MAX_MARKETPLACE_BYTES)} bytes`);
271
+ }
272
+ const body = await readFile(filename);
273
+ if (body.byteLength > MAX_MARKETPLACE_BYTES) {
274
+ throw new Error(`pluginInventory: marketplace exceeds ${String(MAX_MARKETPLACE_BYTES)} bytes`);
275
+ }
276
+ return MarketplaceDocumentSchema.parse(JSON.parse(body.toString('utf8')));
277
+ }
278
+ catch (error) {
279
+ if (missingPath(error))
280
+ continue;
281
+ throw error;
282
+ }
283
+ }
284
+ throw new Error('pluginInventory: OpenAI/Codex marketplace.json is missing');
285
+ }
286
+ /** Load and validate one marketplace root without retaining an executable checkout. */
287
+ async function loadMarketplace(request) {
288
+ const normalized = await normalizeMarketplaceSource(request);
289
+ const { kind, ...source } = normalized;
290
+ if (kind === 'local') {
291
+ return { source, document: await readMarketplaceDocument(source.source) };
292
+ }
293
+ // Temporary shallow clones avoid cache invalidation; add snapshots if list latency becomes material.
294
+ const tempRoot = await mkdtemp(join(tmpdir(), 'hydra-marketplace-'));
295
+ const checkout = join(tempRoot, 'repository');
296
+ try {
297
+ if (source.gitRef !== '') {
298
+ try {
299
+ await runGit(tempRoot, ['check-ref-format', '--allow-onelevel', source.gitRef]);
300
+ }
301
+ catch (cause) {
302
+ throw new Error('pluginInventory: Git ref is invalid', { cause });
303
+ }
304
+ }
305
+ const cloneArgs = ['clone', '--depth', '1', '--filter=blob:none'];
306
+ if (source.sparsePaths.length > 0)
307
+ cloneArgs.push('--sparse');
308
+ if (source.gitRef !== '')
309
+ cloneArgs.push('--no-checkout');
310
+ cloneArgs.push('--', source.source, checkout);
311
+ await runGit(tempRoot, cloneArgs);
312
+ if (source.gitRef !== '') {
313
+ await runGit(tempRoot, ['-C', checkout, 'fetch', '--depth', '1', 'origin', source.gitRef]);
314
+ }
315
+ if (source.sparsePaths.length > 0) {
316
+ await runGit(tempRoot, [
317
+ '-C', checkout, 'sparse-checkout', 'set', '--cone', '--sparse-index', '--',
318
+ '.agents/plugins', ...source.sparsePaths,
319
+ ]);
320
+ }
321
+ if (source.gitRef !== '') {
322
+ await runGit(tempRoot, ['-C', checkout, 'checkout', '--detach', 'FETCH_HEAD']);
323
+ }
324
+ return { source, document: await readMarketplaceDocument(checkout) };
325
+ }
326
+ finally {
327
+ await rm(tempRoot, { recursive: true, force: true });
328
+ }
329
+ }
330
+ /** Remote-only service exposing the Loader's current non-group entry state. */
331
+ let PluginInventoryGateway = (() => {
332
+ let _classSuper = TypertRemoteService;
333
+ let _instanceExtraInitializers = [];
334
+ let _list_decorators;
335
+ let _setEnabled_decorators;
336
+ let _listMarketplaces_decorators;
337
+ let _addMarketplace_decorators;
338
+ let _setMarketplaceEnabled_decorators;
339
+ let _removeMarketplace_decorators;
340
+ let _listImportedPlugins_decorators;
341
+ let _importPlugin_decorators;
342
+ let _infoPlugin_decorators;
343
+ let _enablePlugin_decorators;
344
+ let _disablePlugin_decorators;
345
+ let _setPluginMcpServerEnabled_decorators;
346
+ let _setPluginMcpToolApproval_decorators;
347
+ let _trustPlugin_decorators;
348
+ let _untrustPlugin_decorators;
349
+ let _removePlugin_decorators;
350
+ let _listMcpServers_decorators;
351
+ let _defineMcpServer_decorators;
352
+ let _setMcpServerEnabled_decorators;
353
+ let _removeMcpServer_decorators;
354
+ let _listHookRecords_decorators;
355
+ let _defineHookRecord_decorators;
356
+ let _setHookRecordEnabled_decorators;
357
+ let _removeHookRecord_decorators;
358
+ return class PluginInventoryGateway extends _classSuper {
359
+ static {
360
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
361
+ _list_decorators = [Remote('list')];
362
+ _setEnabled_decorators = [Remote('setEnabled')];
363
+ _listMarketplaces_decorators = [Remote('listMarketplaces')];
364
+ _addMarketplace_decorators = [Remote('addMarketplace')];
365
+ _setMarketplaceEnabled_decorators = [Remote('setMarketplaceEnabled')];
366
+ _removeMarketplace_decorators = [Remote('removeMarketplace')];
367
+ _listImportedPlugins_decorators = [Remote('listImportedPlugins')];
368
+ _importPlugin_decorators = [Remote('importPlugin')];
369
+ _infoPlugin_decorators = [Remote('infoPlugin')];
370
+ _enablePlugin_decorators = [Remote('enablePlugin')];
371
+ _disablePlugin_decorators = [Remote('disablePlugin')];
372
+ _setPluginMcpServerEnabled_decorators = [Remote('setPluginMcpServerEnabled')];
373
+ _setPluginMcpToolApproval_decorators = [Remote('setPluginMcpToolApproval')];
374
+ _trustPlugin_decorators = [Remote('trustPlugin')];
375
+ _untrustPlugin_decorators = [Remote('untrustPlugin')];
376
+ _removePlugin_decorators = [Remote('removePlugin')];
377
+ _listMcpServers_decorators = [Remote('listMcpServers')];
378
+ _defineMcpServer_decorators = [Remote('defineMcpServer')];
379
+ _setMcpServerEnabled_decorators = [Remote('setMcpServerEnabled')];
380
+ _removeMcpServer_decorators = [Remote('removeMcpServer')];
381
+ _listHookRecords_decorators = [Remote('listHookRecords')];
382
+ _defineHookRecord_decorators = [Remote('defineHookRecord')];
383
+ _setHookRecordEnabled_decorators = [Remote('setHookRecordEnabled')];
384
+ _removeHookRecord_decorators = [Remote('removeHookRecord')];
385
+ __esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
386
+ __esDecorate(this, null, _setEnabled_decorators, { kind: "method", name: "setEnabled", static: false, private: false, access: { has: obj => "setEnabled" in obj, get: obj => obj.setEnabled }, metadata: _metadata }, null, _instanceExtraInitializers);
387
+ __esDecorate(this, null, _listMarketplaces_decorators, { kind: "method", name: "listMarketplaces", static: false, private: false, access: { has: obj => "listMarketplaces" in obj, get: obj => obj.listMarketplaces }, metadata: _metadata }, null, _instanceExtraInitializers);
388
+ __esDecorate(this, null, _addMarketplace_decorators, { kind: "method", name: "addMarketplace", static: false, private: false, access: { has: obj => "addMarketplace" in obj, get: obj => obj.addMarketplace }, metadata: _metadata }, null, _instanceExtraInitializers);
389
+ __esDecorate(this, null, _setMarketplaceEnabled_decorators, { kind: "method", name: "setMarketplaceEnabled", static: false, private: false, access: { has: obj => "setMarketplaceEnabled" in obj, get: obj => obj.setMarketplaceEnabled }, metadata: _metadata }, null, _instanceExtraInitializers);
390
+ __esDecorate(this, null, _removeMarketplace_decorators, { kind: "method", name: "removeMarketplace", static: false, private: false, access: { has: obj => "removeMarketplace" in obj, get: obj => obj.removeMarketplace }, metadata: _metadata }, null, _instanceExtraInitializers);
391
+ __esDecorate(this, null, _listImportedPlugins_decorators, { kind: "method", name: "listImportedPlugins", static: false, private: false, access: { has: obj => "listImportedPlugins" in obj, get: obj => obj.listImportedPlugins }, metadata: _metadata }, null, _instanceExtraInitializers);
392
+ __esDecorate(this, null, _importPlugin_decorators, { kind: "method", name: "importPlugin", static: false, private: false, access: { has: obj => "importPlugin" in obj, get: obj => obj.importPlugin }, metadata: _metadata }, null, _instanceExtraInitializers);
393
+ __esDecorate(this, null, _infoPlugin_decorators, { kind: "method", name: "infoPlugin", static: false, private: false, access: { has: obj => "infoPlugin" in obj, get: obj => obj.infoPlugin }, metadata: _metadata }, null, _instanceExtraInitializers);
394
+ __esDecorate(this, null, _enablePlugin_decorators, { kind: "method", name: "enablePlugin", static: false, private: false, access: { has: obj => "enablePlugin" in obj, get: obj => obj.enablePlugin }, metadata: _metadata }, null, _instanceExtraInitializers);
395
+ __esDecorate(this, null, _disablePlugin_decorators, { kind: "method", name: "disablePlugin", static: false, private: false, access: { has: obj => "disablePlugin" in obj, get: obj => obj.disablePlugin }, metadata: _metadata }, null, _instanceExtraInitializers);
396
+ __esDecorate(this, null, _setPluginMcpServerEnabled_decorators, { kind: "method", name: "setPluginMcpServerEnabled", static: false, private: false, access: { has: obj => "setPluginMcpServerEnabled" in obj, get: obj => obj.setPluginMcpServerEnabled }, metadata: _metadata }, null, _instanceExtraInitializers);
397
+ __esDecorate(this, null, _setPluginMcpToolApproval_decorators, { kind: "method", name: "setPluginMcpToolApproval", static: false, private: false, access: { has: obj => "setPluginMcpToolApproval" in obj, get: obj => obj.setPluginMcpToolApproval }, metadata: _metadata }, null, _instanceExtraInitializers);
398
+ __esDecorate(this, null, _trustPlugin_decorators, { kind: "method", name: "trustPlugin", static: false, private: false, access: { has: obj => "trustPlugin" in obj, get: obj => obj.trustPlugin }, metadata: _metadata }, null, _instanceExtraInitializers);
399
+ __esDecorate(this, null, _untrustPlugin_decorators, { kind: "method", name: "untrustPlugin", static: false, private: false, access: { has: obj => "untrustPlugin" in obj, get: obj => obj.untrustPlugin }, metadata: _metadata }, null, _instanceExtraInitializers);
400
+ __esDecorate(this, null, _removePlugin_decorators, { kind: "method", name: "removePlugin", static: false, private: false, access: { has: obj => "removePlugin" in obj, get: obj => obj.removePlugin }, metadata: _metadata }, null, _instanceExtraInitializers);
401
+ __esDecorate(this, null, _listMcpServers_decorators, { kind: "method", name: "listMcpServers", static: false, private: false, access: { has: obj => "listMcpServers" in obj, get: obj => obj.listMcpServers }, metadata: _metadata }, null, _instanceExtraInitializers);
402
+ __esDecorate(this, null, _defineMcpServer_decorators, { kind: "method", name: "defineMcpServer", static: false, private: false, access: { has: obj => "defineMcpServer" in obj, get: obj => obj.defineMcpServer }, metadata: _metadata }, null, _instanceExtraInitializers);
403
+ __esDecorate(this, null, _setMcpServerEnabled_decorators, { kind: "method", name: "setMcpServerEnabled", static: false, private: false, access: { has: obj => "setMcpServerEnabled" in obj, get: obj => obj.setMcpServerEnabled }, metadata: _metadata }, null, _instanceExtraInitializers);
404
+ __esDecorate(this, null, _removeMcpServer_decorators, { kind: "method", name: "removeMcpServer", static: false, private: false, access: { has: obj => "removeMcpServer" in obj, get: obj => obj.removeMcpServer }, metadata: _metadata }, null, _instanceExtraInitializers);
405
+ __esDecorate(this, null, _listHookRecords_decorators, { kind: "method", name: "listHookRecords", static: false, private: false, access: { has: obj => "listHookRecords" in obj, get: obj => obj.listHookRecords }, metadata: _metadata }, null, _instanceExtraInitializers);
406
+ __esDecorate(this, null, _defineHookRecord_decorators, { kind: "method", name: "defineHookRecord", static: false, private: false, access: { has: obj => "defineHookRecord" in obj, get: obj => obj.defineHookRecord }, metadata: _metadata }, null, _instanceExtraInitializers);
407
+ __esDecorate(this, null, _setHookRecordEnabled_decorators, { kind: "method", name: "setHookRecordEnabled", static: false, private: false, access: { has: obj => "setHookRecordEnabled" in obj, get: obj => obj.setHookRecordEnabled }, metadata: _metadata }, null, _instanceExtraInitializers);
408
+ __esDecorate(this, null, _removeHookRecord_decorators, { kind: "method", name: "removeHookRecord", static: false, private: false, access: { has: obj => "removeHookRecord" in obj, get: obj => obj.removeHookRecord }, metadata: _metadata }, null, _instanceExtraInitializers);
409
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
410
+ }
411
+ static inject = ['loader', 'settings'];
412
+ static Config = z.object({
413
+ compositionEntryIds: z.array(z.string()).default([]),
414
+ });
415
+ compositionEntryIds = __runInitializers(this, _instanceExtraInitializers);
416
+ settings;
417
+ marketplaceSettings;
418
+ defaultEnabled = new Map();
419
+ initialEnabled = new Map();
420
+ originalConfigs = new Map();
421
+ suspended = new Map();
422
+ required = new Set();
423
+ appliedSettings = new Set();
424
+ restartEnablement = {};
425
+ mutationTail = Promise.resolve();
426
+ constructor(ctx, config = {}) {
427
+ super(ctx, 'pluginInventory');
428
+ this.compositionEntryIds = new Set(config.compositionEntryIds);
429
+ this.settings = ctx.settings.register(PLUGIN_SETTINGS_NAMESPACE, PluginSettingsSchema);
430
+ this.marketplaceSettings = ctx.settings.register(MARKETPLACE_SETTINGS_NAMESPACE, MarketplaceSettingsSchema);
431
+ ctx.inject(['optionalPlugins'], (pctx) => {
432
+ pctx.effect(() => pctx.optionalPlugins.subscribe((enabled) => {
433
+ void this.enqueue(() => this.applyOptionalGate(enabled)).catch((error) => { this.ctx.logger.error(error); });
434
+ }), 'pluginInventory: optional policy');
435
+ void this.enqueue(() => this.applyOptionalGate(pctx.optionalPlugins.enabled))
436
+ .catch((error) => { this.ctx.logger.error(error); });
437
+ });
438
+ ctx.on('loader/config-update', () => {
439
+ const policy = ctx.get('optionalPlugins');
440
+ if (policy !== undefined)
441
+ void this.enqueue(() => this.applyOptionalGate(policy.enabled))
442
+ .catch((error) => { ctx.logger.error(error); });
443
+ });
444
+ }
445
+ async [Service.init]() {
446
+ const rootInclude = this.rootInclude();
447
+ if (rootInclude === undefined)
448
+ return;
449
+ const representatives = this.representatives(rootInclude);
450
+ for (const entry of representatives.values()) {
451
+ this.defaultEnabled.set(entry.options.name, !entry.disabled && !this.isHmrWatchOnly(entry));
452
+ this.originalConfigs.set(entry.options.name, structuredClone(entry.options.config));
453
+ }
454
+ if ([...representatives.values()].some(entry => this.isRestartOnly(entry))) {
455
+ this.restartEnablement = profilePluginEnablement(readProfileManifest('pluginInventory', this.profileDir()));
456
+ }
457
+ await this.applySettings(this.settings.get().enabled);
458
+ for (const entry of representatives.values())
459
+ this.initialEnabled.set(entry.id, this.enabled(entry));
460
+ this.ctx.effect(() => this.settings.watch(next => this.enqueue(() => this.applySettings(next.enabled))), 'pluginInventory.settingsWatch');
461
+ }
462
+ /** Protect required modules and their declared dependency closure before suspending root-owned features. */
463
+ async applyOptionalGate(enabled) {
464
+ const policy = this.ctx.get('optionalPlugins');
465
+ if (policy === undefined)
466
+ return;
467
+ if (!enabled)
468
+ await policy.drain();
469
+ if (policy.enabled !== enabled)
470
+ return;
471
+ const entries = [...this.representatives(this.rootInclude()).values()];
472
+ const byModule = new Map(entries.map(entry => [entry.options.name, entry]));
473
+ const pending = entries.filter(entry => entry.options.pluginRequired === true).map(entry => entry.options.name);
474
+ this.required.clear();
475
+ for (const name of pending) {
476
+ if (this.required.has(name))
477
+ continue;
478
+ this.required.add(name);
479
+ const entry = byModule.get(name);
480
+ const baseUrl = entry?.parent.tree.ctx.baseUrl;
481
+ if (baseUrl === undefined || !name.startsWith('@'))
482
+ continue;
483
+ const packageName = name.split('/').slice(0, 2).join('/');
484
+ const manifest = createRequire(baseUrl).resolve(`${packageName}/package.json`);
485
+ const metadata = zod.object({ dependencies: zod.record(zod.string(), zod.string()).optional(), peerDependencies: zod.record(zod.string(), zod.string()).optional(), hydra: zod.object({ bundle: zod.unknown().optional() }).optional() }).parse(JSON.parse(await readFile(manifest, 'utf8')));
486
+ // Bundle manifests list optional composition rows; required runtime rows carry their own flags.
487
+ if (metadata.hydra?.bundle !== undefined)
488
+ continue;
489
+ for (const dependency of Object.keys({ ...metadata.dependencies, ...metadata.peerDependencies })) {
490
+ for (const candidate of entries)
491
+ if (candidate.options.name === dependency || candidate.options.name.startsWith(`${dependency}/`))
492
+ pending.push(candidate.options.name);
493
+ }
494
+ }
495
+ for (const entry of entries) {
496
+ if (entry.subtree !== undefined || entry.subgroup !== undefined || this.required.has(entry.options.name))
497
+ continue;
498
+ const previous = this.suspended.get(entry);
499
+ if (!enabled) {
500
+ if (!this.suspended.has(entry))
501
+ this.suspended.set(entry, !entry.disabled);
502
+ await entry.update({ disabled: true });
503
+ }
504
+ else if (previous !== undefined) {
505
+ const nextEnabled = this.isRestartOnly(entry) ? previous
506
+ : this.settings.get().enabled[entry.options.name] ?? this.defaultEnabled.get(entry.options.name) ?? previous;
507
+ this.suspended.delete(entry);
508
+ await entry.update({ disabled: !nextEnabled });
509
+ }
510
+ }
511
+ }
512
+ rootInclude() {
513
+ return Object.values(this.ctx.loader.store)
514
+ .map(entry => entry.subtree)
515
+ .find((tree) => tree !== undefined
516
+ && 'filename' in tree
517
+ && typeof tree.filename === 'string');
518
+ }
519
+ /** Project persisted OpenAI/Codex marketplace sources without exposing their entries here. */
520
+ async marketplaceSnapshot() {
521
+ const sources = this.marketplaceSettings.get().sources;
522
+ if (sources.length === 0)
523
+ return { marketplaces: [] };
524
+ const marketplaces = [];
525
+ for (const source of sources) {
526
+ const sourceView = {
527
+ source: source.source,
528
+ ...(source.gitRef === '' ? {} : { gitRef: source.gitRef }),
529
+ sparsePaths: source.sparsePaths,
530
+ enabled: source.enabled,
531
+ };
532
+ try {
533
+ await loadMarketplace(source);
534
+ marketplaces.push({ status: 'ready', ...sourceView });
535
+ }
536
+ catch {
537
+ marketplaces.push({ status: 'unavailable', ...sourceView });
538
+ }
539
+ }
540
+ return { marketplaces };
541
+ }
542
+ moduleEntries(moduleName) {
543
+ const entries = [];
544
+ for (const entry of this.ctx.loader.entries()) {
545
+ if (!entry.options.group
546
+ && !this.compositionEntryIds.has(entry.options.id)
547
+ && entry.options.name === moduleName)
548
+ entries.push(entry);
549
+ }
550
+ return entries;
551
+ }
552
+ /** One logical row per module, preferring the profile-owned entry. */
553
+ representatives(rootInclude) {
554
+ const entries = new Map();
555
+ for (const entry of this.ctx.loader.entries()) {
556
+ if (entry.options.group || this.compositionEntryIds.has(entry.options.id))
557
+ continue;
558
+ this.isRestartOnly(entry);
559
+ if (entry.options.pluginRequired !== undefined && typeof entry.options.pluginRequired !== 'boolean')
560
+ throw new Error(`pluginInventory: ${entry.id}.pluginRequired must be boolean`);
561
+ const group = entry.options.pluginGroup;
562
+ if (group !== undefined && (typeof group !== 'string' || group.trim() === '')) {
563
+ throw new Error(`pluginInventory: ${entry.id}.pluginGroup must be a non-empty string`);
564
+ }
565
+ if ('plugin_type' in entry.options)
566
+ throw new Error(`pluginInventory: ${entry.id}.plugin_type: use pluginType`);
567
+ const current = entries.get(entry.options.name);
568
+ if (current === undefined
569
+ || (entry.parent.tree === rootInclude && current.parent.tree !== rootInclude)
570
+ || (this.isRestartOnly(entry) && !this.isRestartOnly(current))) {
571
+ entries.set(entry.options.name, entry);
572
+ }
573
+ }
574
+ return entries;
575
+ }
576
+ /** Whether changing this root entry must wait for the next profile boot. */
577
+ isRestartOnly(entry) {
578
+ const value = entry.options.pluginType;
579
+ if (value !== undefined && value !== 'core' && value !== 'normal') {
580
+ throw new Error(`pluginInventory: ${entry.id}.pluginType must be core or normal`);
581
+ }
582
+ return value === 'core' || (entry.options.pluginGroup !== undefined
583
+ && [...this.ctx.loader.entries()].some(candidate => candidate.options.pluginGroup === entry.options.pluginGroup
584
+ && candidate.options.pluginType === 'core'));
585
+ }
586
+ isToggleable(entry, rootInclude = this.rootInclude()) {
587
+ return rootInclude !== undefined
588
+ && entry.parent.tree === rootInclude
589
+ && entry.options.pluginRequired !== true && !this.required.has(entry.options.name);
590
+ }
591
+ enabled(entry) {
592
+ if (this.isRestartOnly(entry) || !this.isToggleable(entry))
593
+ return this.suspended.get(entry) ?? !entry.disabled;
594
+ return this.settings.get().enabled[entry.options.name]
595
+ ?? this.defaultEnabled.get(entry.options.name)
596
+ ?? !entry.disabled;
597
+ }
598
+ /** Desired state used by the switch; restart-only entries retain the live Loader state until reboot. */
599
+ desiredEnabled(entry) {
600
+ return this.isRestartOnly(entry)
601
+ ? this.restartEnablement[entry.options.id] ?? !entry.disabled
602
+ : this.enabled(entry);
603
+ }
604
+ /** Whether the currently loaded entry still differs from its persisted desired state. */
605
+ restartRequired(entry) {
606
+ const pending = this.restartEnablement[entry.options.id];
607
+ return this.isRestartOnly(entry) && pending !== undefined && pending !== !entry.disabled;
608
+ }
609
+ /** Resolve the active root Include to the profile directory the launcher owns. */
610
+ profileDir() {
611
+ const filename = this.rootInclude()?.filename;
612
+ /* v8 ignore next -- Restart updates only follow a synchronous rootInclude-backed toggleability check. */
613
+ if (typeof filename !== 'string')
614
+ throw new Error('pluginInventory: active profile root is unavailable');
615
+ /* v8 ignore next -- The root Include stores filename as the filesystem path returned by fileURLToPath. */
616
+ const path = filename.startsWith('file:') ? fileURLToPath(filename) : filename;
617
+ const actual = resolve(dirname(path));
618
+ const expected = resolve(resolveProfileDir(basename(actual)));
619
+ if (actual !== expected) {
620
+ throw new Error(`pluginInventory: active root ${path} is not a managed hydra profile`);
621
+ }
622
+ return expected;
623
+ }
624
+ /** Persist one restart-only root switch without changing the live Loader tree. */
625
+ async setRestartEnabled(entries, enabled) {
626
+ const profileDir = this.profileDir();
627
+ const manifestPath = join(profileDir, 'package.json');
628
+ await withFileLock(manifestPath, async () => {
629
+ // Re-read while holding the cross-process lock so a concurrent profile
630
+ // edit cannot be replaced by a stale manifest snapshot.
631
+ const manifest = readProfileManifest('pluginInventory', profileDir);
632
+ const pluginEnablement = {
633
+ ...profilePluginEnablement(manifest),
634
+ ...Object.fromEntries(entries.map(entry => [entry.options.id, enabled])),
635
+ };
636
+ const next = {
637
+ ...manifest,
638
+ hydra: {
639
+ ...manifest.hydra,
640
+ profile: {
641
+ ...manifest.hydra?.profile,
642
+ pluginEnablement,
643
+ },
644
+ },
645
+ };
646
+ await writeFileAtomic(manifestPath, `${JSON.stringify(next, undefined, 2)}\n`, { mode: 0o600 });
647
+ this.restartEnablement = pluginEnablement;
648
+ });
649
+ }
650
+ isHmrWatchOnly(entry) {
651
+ const config = entry.options.config;
652
+ return entry.options.name === HMR_MODULE && Array.isArray(config?.root) && config.root.length === 0;
653
+ }
654
+ enqueue(task) {
655
+ const run = this.mutationTail.then(task, task);
656
+ this.mutationTail = run.then(() => undefined, () => undefined);
657
+ return run;
658
+ }
659
+ async restore(states) {
660
+ for (const state of states.toReversed()) {
661
+ await state.entry.update({
662
+ disabled: state.disabled ?? null,
663
+ config: state.config ?? null,
664
+ });
665
+ }
666
+ }
667
+ /** Keep one configured instance active; HMR's disabled state remains watch-only. */
668
+ async updateModule(entry, enabled) {
669
+ const candidates = this.moduleEntries(entry.options.name);
670
+ const states = candidates.map(candidate => ({
671
+ entry: candidate,
672
+ disabled: candidate.options.disabled,
673
+ config: candidate.options.config,
674
+ }));
675
+ try {
676
+ for (const candidate of candidates) {
677
+ if (candidate !== entry && !candidate.disabled)
678
+ await candidate.update({ disabled: true });
679
+ }
680
+ if (entry.options.name === HMR_MODULE) {
681
+ const original = this.originalConfigs.get(entry.options.name);
682
+ const config = { ...original };
683
+ if (enabled)
684
+ delete config.root;
685
+ else
686
+ config.root = [];
687
+ await entry.update({ disabled: null, config });
688
+ }
689
+ else {
690
+ await entry.update({ disabled: enabled ? null : true });
691
+ }
692
+ return states;
693
+ }
694
+ catch (error) {
695
+ try {
696
+ await this.restore(states);
697
+ }
698
+ catch (rollbackError) {
699
+ throw new AggregateError([error, rollbackError], `pluginInventory: failed to update and roll back ${entry.options.name}`);
700
+ }
701
+ throw error;
702
+ }
703
+ }
704
+ async applySettings(enabled) {
705
+ const rootInclude = this.rootInclude();
706
+ const representatives = this.representatives(rootInclude);
707
+ const modules = new Set([...this.appliedSettings, ...Object.keys(enabled)]);
708
+ this.appliedSettings.clear();
709
+ for (const moduleName of Object.keys(enabled))
710
+ this.appliedSettings.add(moduleName);
711
+ const failures = [];
712
+ for (const moduleName of modules) {
713
+ const entry = representatives.get(moduleName);
714
+ if (entry === undefined)
715
+ continue;
716
+ if (!this.isToggleable(entry, rootInclude) || this.isRestartOnly(entry))
717
+ continue;
718
+ const desired = enabled[moduleName]
719
+ ?? this.defaultEnabled.get(moduleName)
720
+ ?? !entry.disabled;
721
+ try {
722
+ await this.updateModule(entry, this.ctx.get('optionalPlugins')?.enabled === false && !this.required.has(moduleName) ? false : desired);
723
+ }
724
+ catch (error) {
725
+ failures.push(error);
726
+ }
727
+ }
728
+ if (failures.length === 1)
729
+ throw failures[0];
730
+ if (failures.length > 1)
731
+ throw new AggregateError(failures, 'pluginInventory: failed to apply plugin settings');
732
+ }
733
+ /**
734
+ * Read the Loader and optional preset roster directly on every call. Cordis's
735
+ * internal plugin/status events already maintain Entry.fiber and Fiber state,
736
+ * so a second Host cache would only add another lifecycle truth to synchronize.
737
+ * @returns Current Host entries followed by preset-owned leaf entries.
738
+ */
739
+ async hostEntries() {
740
+ const rootInclude = this.rootInclude();
741
+ return Promise.all(this.inventoryGroups(rootInclude).map(async (group) => {
742
+ const entry = group[0];
743
+ const members = group.filter(candidate => this.isToggleable(candidate, rootInclude));
744
+ const controlled = members.length > 0 ? members : group;
745
+ const initialStates = controlled.map(candidate => this.initialEnabled.get(candidate.id) ?? this.enabled(candidate));
746
+ const desiredStates = controlled.map(candidate => this.desiredEnabled(candidate));
747
+ const enabled = controlled.every(candidate => this.enabled(candidate));
748
+ const restartRequired = controlled.some(candidate => this.restartRequired(candidate));
749
+ const pendingEnabled = restartRequired ? controlled.every(candidate => this.desiredEnabled(candidate)) : undefined;
750
+ const active = enabled
751
+ ? this.moduleEntries(entry.options.name).find(candidate => !candidate.disabled && candidate.fiber !== undefined)
752
+ : undefined;
753
+ const fiber = active?.fiber;
754
+ return {
755
+ entryId: pluginEntryId(entry.id),
756
+ moduleName: entry.options.name,
757
+ ...await pluginMetadata(entry.options.name, entry.parent.tree.ctx.baseUrl, entry.options),
758
+ pluginType: group.some(candidate => this.isRestartOnly(candidate)) ? 'core' : 'normal',
759
+ ...entry.options.pluginRequired === true || this.required.has(entry.options.name) ? { required: true } : {},
760
+ initialEnabled: initialStates.every(Boolean) ? true : initialStates.some(Boolean) ? null : false,
761
+ changedSinceStart: controlled.some((candidate, index) => this.desiredEnabled(candidate) !== initialStates[index]),
762
+ ...(desiredStates.some(Boolean) && !desiredStates.every(Boolean) ? { mixedEnabled: true } : {}),
763
+ ...(group.length > 1 ? { relatedModules: group.slice(1).map(candidate => candidate.options.name) } : {}),
764
+ enabled,
765
+ ...pendingEnabled === undefined ? {} : { pendingEnabled },
766
+ restartRequired,
767
+ toggleable: this.isToggleable(entry, rootInclude),
768
+ fiberPhase: fiber === undefined ? null : FIBER_PHASE[fiber.state],
769
+ };
770
+ }));
771
+ }
772
+ async presetEntries(presets, settings, target = {}) {
773
+ if (presets === undefined)
774
+ return [];
775
+ const entries = settings === undefined ? await presets.listPluginEntries() : await presets.listPluginEntries(settings);
776
+ return Promise.all(entries.map(async (entry) => ({
777
+ entryId: pluginEntryId(entry.entryId),
778
+ moduleName: entry.moduleName,
779
+ ...await pluginMetadata(entry.moduleName, this.ctx.baseUrl, entry),
780
+ enabled: entry.enabled,
781
+ presetId: entry.presetId,
782
+ ...entry.rowId === undefined ? {} : { presetRowId: entry.rowId },
783
+ newSessionsOnly: true,
784
+ initialEnabled: this.initialEnabled.get(this.initialKey(entry.entryId, target)) ?? entry.enabled,
785
+ restartRequired: false,
786
+ toggleable: true,
787
+ fiberPhase: null,
788
+ })));
789
+ }
790
+ /**
791
+ * List Host entries and preset leaves, or only the selected workspace's preset leaves.
792
+ * @param request - Workspace and editable settings layer; omission selects global inventory.
793
+ * @returns the current inventory projection.
794
+ */
795
+ async list(request) {
796
+ const target = request ?? {};
797
+ this.assertTarget(target);
798
+ const workspace = target.workspaceId === undefined ? undefined : await this.workspaceTarget(target.workspaceId, target.layer);
799
+ const selected = workspace?.target ?? {};
800
+ const entries = workspace === undefined
801
+ ? [...await this.hostEntries(), ...await this.presetEntries(this.agentPresets())]
802
+ : await this.presetEntries(workspace.presets, workspace.settings, selected);
803
+ for (const entry of entries) {
804
+ const key = this.initialKey(entry.entryId, selected);
805
+ if (!this.initialEnabled.has(key))
806
+ this.initialEnabled.set(key, entry.enabled);
807
+ }
808
+ return { entries, ...selected };
809
+ }
810
+ /** Keep initial preset states separate for global and workspace settings files. */
811
+ initialKey(entryId, target) {
812
+ return target.workspaceId === undefined ? entryId : `${target.workspaceId}:${target.layer ?? 'local'}:${entryId}`;
813
+ }
814
+ /** Group only explicitly related entries, preferring a persistently editable owner. */
815
+ inventoryGroups(rootInclude) {
816
+ const groups = new Map();
817
+ for (const entry of this.representatives(rootInclude).values()) {
818
+ const key = entry.options.pluginGroup === undefined ? `module:${entry.options.name}`
819
+ : `group:${entry.options.pluginGroup}:${entry.options.pluginRequired === true || this.required.has(entry.options.name) ? 'required' : 'optional'}`;
820
+ const group = groups.get(key) ?? [];
821
+ if (this.isToggleable(entry, rootInclude) && group[0] !== undefined && !this.isToggleable(group[0], rootInclude))
822
+ group.unshift(entry);
823
+ else
824
+ group.push(entry);
825
+ groups.set(key, group);
826
+ }
827
+ return [...groups.values()];
828
+ }
829
+ /**
830
+ * Persist a restart-only root state without live unload, or apply one normal
831
+ * module-wide live state and persist it in the shared settings document.
832
+ * @param request - Target entry, desired enablement, and optional workspace settings file.
833
+ * @returns The fresh snapshot and whether this mutation needs a restart.
834
+ */
835
+ setEnabled(request) {
836
+ this.assertTarget(request);
837
+ return this.enqueue(async () => {
838
+ const workspace = request.workspaceId === undefined ? undefined : await this.workspaceTarget(request.workspaceId, request.layer);
839
+ if (workspace !== undefined && !request.entryId.startsWith(AGENT_PRESET_ENTRY_PREFIX)) {
840
+ throw new Error('pluginInventory: workspace settings can change only agent preset entries');
841
+ }
842
+ if (request.entryId.startsWith(AGENT_PRESET_ENTRY_PREFIX)) {
843
+ const presets = workspace === undefined ? this.agentPresets() : workspace.presets;
844
+ if (presets === undefined) {
845
+ throw new Error('pluginInventory: agent preset controls are unavailable');
846
+ }
847
+ const entries = workspace === undefined ? await presets.listPluginEntries() : await presets.listPluginEntries(workspace.settings);
848
+ const entry = entries
849
+ .find(candidate => candidate.entryId === request.entryId);
850
+ if (entry === undefined) {
851
+ throw new Error(`pluginInventory: preset entry ${request.entryId} cannot be toggled in-app`);
852
+ }
853
+ const key = this.initialKey(entry.entryId, workspace?.target ?? {});
854
+ if (!this.initialEnabled.has(key))
855
+ this.initialEnabled.set(key, entry.enabled);
856
+ if (entry.enabled !== request.enabled) {
857
+ if (workspace === undefined)
858
+ await presets.setPluginEnabled(entry.entryId, request.enabled);
859
+ else
860
+ await presets.setPluginEnabled(entry.entryId, request.enabled, workspace.settings);
861
+ }
862
+ return { snapshot: await this.list(workspace?.target), restartRequired: false };
863
+ }
864
+ const resolved = this.ctx.loader.resolve(request.entryId);
865
+ const rootInclude = this.rootInclude();
866
+ const group = this.inventoryGroups(rootInclude)
867
+ .find(candidates => candidates.some(candidate => candidate.options.name === resolved.options.name)) ?? [];
868
+ const entry = group[0];
869
+ if (entry === undefined || !this.isToggleable(entry, rootInclude)) {
870
+ throw new Error(`pluginInventory: entry ${request.entryId} cannot be toggled in-app`);
871
+ }
872
+ const members = group.filter(candidate => this.isToggleable(candidate, rootInclude));
873
+ if (members.every(candidate => request.enabled === this.desiredEnabled(candidate))) {
874
+ return { snapshot: await this.list(), restartRequired: members.some(candidate => this.restartRequired(candidate)) };
875
+ }
876
+ if (group.some(candidate => this.isRestartOnly(candidate))) {
877
+ await this.setRestartEnabled(members, request.enabled);
878
+ return { snapshot: await this.list(), restartRequired: members.some(candidate => this.restartRequired(candidate)) };
879
+ }
880
+ const states = [];
881
+ try {
882
+ for (const member of members)
883
+ states.push(...await this.updateModule(member, request.enabled));
884
+ await this.settings.update({ enabled: Object.fromEntries(members.map(member => [member.options.name, request.enabled])) });
885
+ }
886
+ catch (error) {
887
+ try {
888
+ await this.restore(states);
889
+ }
890
+ catch (rollbackError) {
891
+ throw new AggregateError([error, rollbackError], `pluginInventory: failed to persist and roll back ${request.entryId}`);
892
+ }
893
+ throw error;
894
+ }
895
+ return { snapshot: await this.list(), restartRequired: false };
896
+ });
897
+ }
898
+ /**
899
+ * Project every persisted OpenAI/Codex marketplace source.
900
+ * @returns OpenAI/Codex marketplace sources in persisted order.
901
+ */
902
+ listMarketplaces() {
903
+ return this.marketplaceSnapshot();
904
+ }
905
+ /**
906
+ * Persist one validated OpenAI/Codex marketplace source, replacing any source of the same location.
907
+ * @param request - marketplace location with its optional Git ref and sparse paths.
908
+ * @returns The refreshed OpenAI/Codex marketplace source list.
909
+ */
910
+ addMarketplace(request) {
911
+ return this.enqueue(async () => {
912
+ const loaded = await loadMarketplace(request);
913
+ const sources = this.marketplaceSettings.get().sources;
914
+ const existing = sources.findIndex(source => source.source === loaded.source.source);
915
+ const entry = { ...loaded.source, enabled: sources[existing]?.enabled ?? true };
916
+ const next = existing === -1
917
+ ? [...sources, entry]
918
+ : sources.map((source, index) => index === existing ? entry : source);
919
+ await this.marketplaceSettings.update({ sources: next });
920
+ return this.marketplaceSnapshot();
921
+ });
922
+ }
923
+ /**
924
+ * Enable or disable one marketplace slot, cascading to its imported plugins.
925
+ * @param request - marketplace source and its desired enablement.
926
+ * @returns The refreshed OpenAI/Codex marketplace source list.
927
+ */
928
+ setMarketplaceEnabled(request) {
929
+ return this.enqueue(async () => {
930
+ const sources = this.marketplaceSettings.get().sources;
931
+ const index = sources.findIndex(candidate => candidate.source === request.source);
932
+ if (index === -1)
933
+ throw new Error(`pluginInventory: marketplace ${request.source} is not configured`);
934
+ const next = sources.map((candidate, position) => position === index
935
+ ? { ...candidate, enabled: request.enabled }
936
+ : candidate);
937
+ await this.marketplaceSettings.update({ sources: next });
938
+ await this.cascadeMarketplacePlugins(request.source, (runtime, identity) => request.enabled ? runtime.enable(identity) : runtime.disable(identity));
939
+ return this.marketplaceSnapshot();
940
+ });
941
+ }
942
+ /**
943
+ * Remove one persisted OpenAI/Codex marketplace source and uninstall its imported plugins.
944
+ * @param source - the persisted marketplace location to delete.
945
+ * @returns The refreshed OpenAI/Codex marketplace source list.
946
+ */
947
+ removeMarketplace(source) {
948
+ return this.enqueue(async () => {
949
+ if (!this.marketplaceSettings.get().sources.some(candidate => candidate.source === source)) {
950
+ throw new Error(`pluginInventory: marketplace ${source} is not configured`);
951
+ }
952
+ await this.cascadeMarketplacePlugins(source, (runtime, identity) => runtime.remove(identity));
953
+ const next = this.marketplaceSettings.get().sources.filter(candidate => candidate.source !== source);
954
+ await this.marketplaceSettings.update({ sources: next });
955
+ return await this.marketplaceSnapshot();
956
+ });
957
+ }
958
+ /**
959
+ * Apply one lifecycle action to every plugin imported from this marketplace root.
960
+ * A no-op when the imported-plugin runtime is not part of this composition.
961
+ */
962
+ async cascadeMarketplacePlugins(marketplace, action) {
963
+ const runtime = this.ctx.get('importedPlugins');
964
+ if (runtime === undefined)
965
+ return;
966
+ const plugins = (await runtime.list()).plugins.filter(plugin => plugin.source.marketplace === marketplace);
967
+ const failures = [];
968
+ for (const plugin of plugins) {
969
+ try {
970
+ await action(runtime, plugin.identity);
971
+ }
972
+ catch (error) {
973
+ failures.push(error);
974
+ }
975
+ }
976
+ if (failures.length === 1)
977
+ throw failures[0];
978
+ if (failures.length > 1) {
979
+ throw new AggregateError(failures, `pluginInventory: failed to update plugins for marketplace ${marketplace}`);
980
+ }
981
+ }
982
+ /**
983
+ * List source-qualified imported OpenAI/Codex plugin bundles.
984
+ * @returns Current imported-plugin projection.
985
+ */
986
+ listImportedPlugins() {
987
+ return this.importedPlugins().list();
988
+ }
989
+ /**
990
+ * Stage a direct local/Git source or marketplace bundle through the shared runtime.
991
+ * @param source - Plugin source to import.
992
+ * @returns Current imported-plugin projection.
993
+ */
994
+ importPlugin(source) {
995
+ return this.importedPlugins().import(source);
996
+ }
997
+ /**
998
+ * Read one imported bundle by source-qualified identity or an unambiguous name.
999
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1000
+ * @returns Current plugin projection.
1001
+ */
1002
+ infoPlugin(identityOrName) {
1003
+ return this.importedPlugins().info(identityOrName);
1004
+ }
1005
+ /**
1006
+ * Enable and load one imported bundle without changing native Hydra package state.
1007
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1008
+ * @returns Current imported-plugin projection.
1009
+ */
1010
+ enablePlugin(identityOrName) {
1011
+ return this.importedPlugins().enable(identityOrName);
1012
+ }
1013
+ /**
1014
+ * Disable and deterministically unload one imported bundle.
1015
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1016
+ * @returns Current imported-plugin projection.
1017
+ */
1018
+ disablePlugin(identityOrName) {
1019
+ return this.importedPlugins().disable(identityOrName);
1020
+ }
1021
+ /**
1022
+ * Change one imported plugin MCP server without changing hook trust.
1023
+ * @param request - plugin identity, server name, and desired enablement.
1024
+ * @returns Current imported-plugin projection.
1025
+ */
1026
+ setPluginMcpServerEnabled(request) {
1027
+ return this.importedPlugins().setMcpServerEnabled(request.identity, request.server, request.enabled);
1028
+ }
1029
+ /**
1030
+ * Set one imported MCP tool's approval mode without changing server enablement.
1031
+ * @param request - Plugin identity, server, tool, and approval mode.
1032
+ * @returns Current imported-plugin projection.
1033
+ */
1034
+ setPluginMcpToolApproval(request) {
1035
+ return this.importedPlugins().setMcpToolApproval(request.identity, request.server, request.tool, request.approval);
1036
+ }
1037
+ /**
1038
+ * Trust only the current hook definitions; MCP/tool approval remains independent.
1039
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1040
+ * @returns Current imported-plugin projection.
1041
+ */
1042
+ trustPlugin(identityOrName) {
1043
+ return this.importedPlugins().trustHooks(identityOrName);
1044
+ }
1045
+ /**
1046
+ * Revoke hook trust without disabling other plugin components.
1047
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1048
+ * @returns Current imported-plugin projection.
1049
+ */
1050
+ untrustPlugin(identityOrName) {
1051
+ return this.importedPlugins().untrustHooks(identityOrName);
1052
+ }
1053
+ /**
1054
+ * Remove one imported bundle and its writable plugin data.
1055
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1056
+ * @returns Current imported-plugin projection.
1057
+ */
1058
+ removePlugin(identityOrName) {
1059
+ return this.importedPlugins().remove(identityOrName);
1060
+ }
1061
+ /**
1062
+ * List the user's own MCP server records with their live state.
1063
+ * @param request - Workspace to inspect; omission selects the user's global records.
1064
+ * @returns Selected settings records with runtime status and the workspace target.
1065
+ */
1066
+ listMcpServers(request) {
1067
+ const target = request ?? {};
1068
+ this.assertTarget(target);
1069
+ if (target.workspaceId !== undefined) {
1070
+ return this.workspaceTarget(target.workspaceId, target.layer)
1071
+ .then(({ ctx, settings, target: selected }) => ({ ...this.mcpServers(ctx).list(settings), ...selected }));
1072
+ }
1073
+ return Promise.resolve(this.mcpServers().list());
1074
+ }
1075
+ /**
1076
+ * Store one complete MCP server definition and converge its mount.
1077
+ * @param request - Complete server definition and optional workspace file to edit.
1078
+ * @returns Refreshed MCP record projection.
1079
+ */
1080
+ defineMcpServer(request) {
1081
+ this.assertTarget(request);
1082
+ if (request.workspaceId !== undefined) {
1083
+ return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1084
+ const registry = this.mcpServers(ctx);
1085
+ await registry.define(request, settings);
1086
+ return { ...registry.list(settings), ...target };
1087
+ });
1088
+ }
1089
+ return this.mcpServers().define(request);
1090
+ }
1091
+ /**
1092
+ * Change one MCP record's desired state without touching its definition.
1093
+ * @param request - Target record, desired state, and optional workspace file to edit.
1094
+ * @returns Refreshed MCP record projection.
1095
+ */
1096
+ setMcpServerEnabled(request) {
1097
+ this.assertTarget(request);
1098
+ if (request.workspaceId !== undefined) {
1099
+ return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1100
+ const registry = this.mcpServers(ctx);
1101
+ await registry.setEnabled(request, settings);
1102
+ return { ...registry.list(settings), ...target };
1103
+ });
1104
+ }
1105
+ return this.mcpServers().setEnabled(request);
1106
+ }
1107
+ /**
1108
+ * Remove one MCP record and unmount its server.
1109
+ * @param request - Stored record name and optional workspace file to edit; a string selects global records.
1110
+ * @returns Refreshed MCP record projection.
1111
+ */
1112
+ removeMcpServer(request) {
1113
+ if (typeof request !== 'string')
1114
+ this.assertTarget(request);
1115
+ if (typeof request !== 'string' && request.workspaceId !== undefined) {
1116
+ return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1117
+ const registry = this.mcpServers(ctx);
1118
+ await registry.remove(request.name, settings);
1119
+ return { ...registry.list(settings), ...target };
1120
+ });
1121
+ }
1122
+ return this.mcpServers().remove(typeof request === 'string' ? request : request.name);
1123
+ }
1124
+ /**
1125
+ * List the user's own hook records with their live state.
1126
+ * @param request - Workspace to inspect; omission selects the user's global records.
1127
+ * @returns Selected settings records with runtime status and the workspace target.
1128
+ */
1129
+ listHookRecords(request) {
1130
+ const target = request ?? {};
1131
+ this.assertTarget(target);
1132
+ if (target.workspaceId !== undefined) {
1133
+ return this.workspaceTarget(target.workspaceId, target.layer)
1134
+ .then(({ ctx, settings, target: selected }) => ({ ...this.hookRecords(ctx).list(settings), ...selected }));
1135
+ }
1136
+ return Promise.resolve(this.hookRecords().list());
1137
+ }
1138
+ /**
1139
+ * Store one complete hook record definition and converge its bridge mount.
1140
+ * @param request - Complete record definition and optional workspace file to edit.
1141
+ * @returns Refreshed hook record projection.
1142
+ */
1143
+ defineHookRecord(request) {
1144
+ this.assertTarget(request);
1145
+ if (request.workspaceId !== undefined) {
1146
+ return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1147
+ const registry = this.hookRecords(ctx);
1148
+ await registry.define(request, settings);
1149
+ return { ...registry.list(settings), ...target };
1150
+ });
1151
+ }
1152
+ return this.hookRecords().define(request);
1153
+ }
1154
+ /**
1155
+ * Change one hook record's desired state without touching its definition.
1156
+ * @param request - Target record, desired state, and optional workspace file to edit.
1157
+ * @returns Refreshed hook record projection.
1158
+ */
1159
+ setHookRecordEnabled(request) {
1160
+ this.assertTarget(request);
1161
+ if (request.workspaceId !== undefined) {
1162
+ return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1163
+ const registry = this.hookRecords(ctx);
1164
+ await registry.setEnabled(request, settings);
1165
+ return { ...registry.list(settings), ...target };
1166
+ });
1167
+ }
1168
+ return this.hookRecords().setEnabled(request);
1169
+ }
1170
+ /**
1171
+ * Remove one hook record and unmount its bridge.
1172
+ * @param request - Stored record name and optional workspace file to edit; a string selects global records.
1173
+ * @returns Refreshed hook record projection.
1174
+ */
1175
+ removeHookRecord(request) {
1176
+ if (typeof request !== 'string')
1177
+ this.assertTarget(request);
1178
+ if (typeof request !== 'string' && request.workspaceId !== undefined) {
1179
+ return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1180
+ const registry = this.hookRecords(ctx);
1181
+ await registry.remove(request.name, settings);
1182
+ return { ...registry.list(settings), ...target };
1183
+ });
1184
+ }
1185
+ return this.hookRecords().remove(typeof request === 'string' ? request : request.name);
1186
+ }
1187
+ /** Reject a workspace file selector that cannot identify a workspace. */
1188
+ assertTarget(target) {
1189
+ if (target.workspaceId === undefined && target.layer !== undefined) {
1190
+ throw new Error('pluginInventory: layer requires workspaceId');
1191
+ }
1192
+ }
1193
+ /** Resolve one workspace's runtime services and selected editable settings file. */
1194
+ async workspaceTarget(workspaceId, layer = 'local') {
1195
+ const service = this.ctx.get('workspaceConfig');
1196
+ if (service === undefined)
1197
+ throw new Error('pluginInventory: workspace configuration is unavailable');
1198
+ const environment = await service.environmentFor(workspaceId);
1199
+ const settings = await service.settingsFor(workspaceId, layer);
1200
+ return { ctx: environment.ctx, presets: environment.presets, settings, target: { workspaceId, layer } };
1201
+ }
1202
+ importedPlugins() {
1203
+ const runtime = this.ctx.get('importedPlugins');
1204
+ if (runtime === undefined)
1205
+ throw new Error('pluginInventory: imported plugin runtime is unavailable');
1206
+ return runtime;
1207
+ }
1208
+ /** The MCP record registry is optional in compositions without user MCP servers. */
1209
+ mcpServers(ctx = this.ctx) {
1210
+ const registry = ctx.get('mcpServers');
1211
+ if (registry === undefined)
1212
+ throw new Error('pluginInventory: MCP server registry is unavailable');
1213
+ return registry;
1214
+ }
1215
+ /** The hook record registry is optional in compositions without user hooks. */
1216
+ hookRecords(ctx = this.ctx) {
1217
+ const registry = ctx.get('hookRecords');
1218
+ if (registry === undefined)
1219
+ throw new Error('pluginInventory: hook record registry is unavailable');
1220
+ return registry;
1221
+ }
1222
+ /** Agent-preset ownership is optional in headless and non-roster compositions. */
1223
+ agentPresets() {
1224
+ return this.ctx.get('agentPresets');
1225
+ }
1226
+ };
1227
+ })();
1228
+ export { PluginInventoryGateway };
1229
+ export default PluginInventoryGateway;
1230
+ //# sourceMappingURL=index.js.map