@sensigo/realm-cli 0.12.1 → 0.14.0
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/dist/agent/gate/slack-gate-notifier.d.ts +1 -1
- package/dist/agent/gate/slack-gate-notifier.d.ts.map +1 -1
- package/dist/agent/gate/slack-gate-notifier.js +1 -1
- package/dist/agent/gate/slack-gate-notifier.js.map +1 -1
- package/dist/agent/gate/slack-gate-server.d.ts +1 -1
- package/dist/agent/gate/slack-gate-server.d.ts.map +1 -1
- package/dist/agent/gate/slack-socket-client.d.ts +1 -1
- package/dist/agent/gate/slack-socket-client.d.ts.map +1 -1
- package/dist/agent/providers/agent-utils.d.ts +7 -2
- package/dist/agent/providers/agent-utils.d.ts.map +1 -1
- package/dist/agent/providers/agent-utils.js +20 -6
- package/dist/agent/providers/agent-utils.js.map +1 -1
- package/dist/agent/run-agent.d.ts +6 -0
- package/dist/agent/run-agent.d.ts.map +1 -1
- package/dist/agent/run-agent.js +5 -0
- package/dist/agent/run-agent.js.map +1 -1
- package/dist/agent/run-attach.d.ts +23 -0
- package/dist/agent/run-attach.d.ts.map +1 -0
- package/dist/agent/run-attach.js +81 -0
- package/dist/agent/run-attach.js.map +1 -0
- package/dist/commands/agent.d.ts +14 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +70 -81
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +69 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/inspect.d.ts +1 -0
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +104 -1
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/listen.d.ts +11 -0
- package/dist/commands/listen.d.ts.map +1 -1
- package/dist/commands/listen.js +36 -1
- package/dist/commands/listen.js.map +1 -1
- package/dist/commands/mcp.d.ts +2 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +16 -5
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/register.d.ts +6 -0
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +35 -1
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +18 -0
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/serve.d.ts +6 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +18 -4
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +36 -1
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +58 -8
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +6 -1
- package/dist/commands/watch.js.map +1 -1
- package/dist/extensions/extension-identity.d.ts +82 -0
- package/dist/extensions/extension-identity.d.ts.map +1 -0
- package/dist/extensions/extension-identity.js +242 -0
- package/dist/extensions/extension-identity.js.map +1 -0
- package/dist/extensions/load-project-extensions.d.ts +59 -0
- package/dist/extensions/load-project-extensions.d.ts.map +1 -0
- package/dist/extensions/load-project-extensions.js +654 -0
- package/dist/extensions/load-project-extensions.js.map +1 -0
- package/dist/extensions/manifest-secrets.d.ts +36 -0
- package/dist/extensions/manifest-secrets.d.ts.map +1 -0
- package/dist/extensions/manifest-secrets.js +126 -0
- package/dist/extensions/manifest-secrets.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/dist/agent/preflight.d.ts +0 -36
- package/dist/agent/preflight.d.ts.map +0 -1
- package/dist/agent/preflight.js +0 -81
- package/dist/agent/preflight.js.map +0 -1
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
// load-project-extensions.ts — the ONE project loader: workflow-declared extension CODE
|
|
2
|
+
// modules (#117) plus the deployment MANIFEST (`<deployment root>/realm.yaml`, v0.14) that
|
|
3
|
+
// owns ALL deployment configuration — adapter construction (built-in catalog + custom
|
|
4
|
+
// factories), handler/processor construction config, secret bindings, gate-notifier config.
|
|
5
|
+
//
|
|
6
|
+
// Every step-executing or config-validating CLI entry point (run, agent, listen, serve,
|
|
7
|
+
// mcp, test, validate, register, watch) resolves through this function. Core validates
|
|
8
|
+
// SHAPES and stores PATHS only — fs, dynamic import, secret resolution, and construction
|
|
9
|
+
// live here, in the CLI composition layer.
|
|
10
|
+
//
|
|
11
|
+
// Anchor mechanics (v6 §1): definitions WITH trust_root → manifest at
|
|
12
|
+
// `<trust_root>/realm.yaml`, loaded iff present (absent = defaults only; no walking).
|
|
13
|
+
// Definitions WITHOUT trust_root (agent-origin, from-string) → the DAEMON's deployment
|
|
14
|
+
// root via `--project` (default cwd for serve/agent/run ONLY; `realm mcp` has NO default —
|
|
15
|
+
// its stdio cwd is CLIENT-controlled, a recorded security decision).
|
|
16
|
+
//
|
|
17
|
+
// Trust model: manifest-write = credential-redirection = code-write-equivalent. Extension
|
|
18
|
+
// module paths and manifest `use:` refs originate ONLY from operator-registered definitions,
|
|
19
|
+
// the operator's deployment root, or operator-typed flags — never from request data.
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
22
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
23
|
+
import { pathToFileURL } from 'node:url';
|
|
24
|
+
import { load as loadYaml } from 'js-yaml';
|
|
25
|
+
import { createDefaultRegistry, validateDeploymentManifest, collectManifestSecretRefs, interpolateConfigTree, findSecretRefSites, VERSION, GitHubAdapter, SlackAdapter, GenericHttpAdapter, AirtableAdapter, GorgiasAdapter, ShopifyAdapter, NotionAdapter, ParcelPanelAdapter, FileSystemAdapter, MockAdapter, } from '@sensigo/realm';
|
|
26
|
+
import { computeExtensionIdentity, errorExtensionIdentityEntry, sha256HexOf, } from './extension-identity.js';
|
|
27
|
+
import { resolveManifestSecrets, redactSecretValues } from './manifest-secrets.js';
|
|
28
|
+
/** Registration surfaces of a declarative extension module, keyed by export map name. */
|
|
29
|
+
const EXTENSION_SURFACES = [
|
|
30
|
+
{ mapKey: 'adapters', type: 'adapter', probeMembers: ['fetch', 'create', 'update'] },
|
|
31
|
+
{ mapKey: 'handlers', type: 'handler', probeMembers: ['execute'] },
|
|
32
|
+
{ mapKey: 'processors', type: 'processor', probeMembers: ['process'] },
|
|
33
|
+
];
|
|
34
|
+
const PROBE_MEMBERS = {
|
|
35
|
+
adapter: ['fetch', 'create', 'update'],
|
|
36
|
+
handler: ['execute'],
|
|
37
|
+
processor: ['process'],
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The built-in adapter catalog: name → factory wrapping the constructor. The factory
|
|
41
|
+
* contract is the ONE construction shape: `({ id, config }) => instance`.
|
|
42
|
+
* `filesystem` and `mock` are config-less — declaring `config:` for them is an ERROR.
|
|
43
|
+
*/
|
|
44
|
+
const BUILTIN_ADAPTER_CATALOG = {
|
|
45
|
+
github: { factory: ({ id, config }) => new GitHubAdapter(id, config) },
|
|
46
|
+
slack: { factory: ({ id, config }) => new SlackAdapter(id, config) },
|
|
47
|
+
http: { factory: ({ id, config }) => new GenericHttpAdapter(id, config) },
|
|
48
|
+
airtable: { factory: ({ id, config }) => new AirtableAdapter(id, config) },
|
|
49
|
+
gorgias: { factory: ({ id, config }) => new GorgiasAdapter(id, config) },
|
|
50
|
+
shopify: { factory: ({ id, config }) => new ShopifyAdapter(id, config) },
|
|
51
|
+
notion: { factory: ({ id, config }) => new NotionAdapter(id, config) },
|
|
52
|
+
parcelpanel: { factory: ({ id, config }) => new ParcelPanelAdapter(id, config) },
|
|
53
|
+
filesystem: { factory: ({ id }) => new FileSystemAdapter(id), configless: true },
|
|
54
|
+
mock: { factory: ({ id }) => new MockAdapter(id, {}), configless: true },
|
|
55
|
+
};
|
|
56
|
+
const DEFAULT_REGISTRY_CACHE_KEY = '\u0000default-registry\u0000';
|
|
57
|
+
const cache = new Map();
|
|
58
|
+
/** @internal Test-only: clears the process-lifetime registry cache. */
|
|
59
|
+
export function clearProjectExtensionsCache() {
|
|
60
|
+
cache.clear();
|
|
61
|
+
}
|
|
62
|
+
function emptyManifest() {
|
|
63
|
+
return { modules: [], adapters: [], handlers: [], processors: [] };
|
|
64
|
+
}
|
|
65
|
+
function errMsg(err) {
|
|
66
|
+
return err instanceof Error ? err.message : String(err);
|
|
67
|
+
}
|
|
68
|
+
function realpathOrFail(path, what) {
|
|
69
|
+
try {
|
|
70
|
+
return realpathSync(path);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
throw new Error(`Cannot resolve ${what}: ${errMsg(err)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** True when `child` is `root` or lives underneath it (both must be realpaths). */
|
|
77
|
+
function isContainedIn(child, root) {
|
|
78
|
+
return child === root || child.startsWith(root + sep);
|
|
79
|
+
}
|
|
80
|
+
function normalizeDeclared(extensions) {
|
|
81
|
+
if (extensions === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
return typeof extensions === 'string' ? [extensions] : extensions;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolves the deployment root and loads+validates the manifest document when present.
|
|
87
|
+
* Definitions WITH trust_root anchor there; definitions WITHOUT it anchor at the daemon's
|
|
88
|
+
* `--project` root (absent → no manifest, defaults only — NO walking, NO error).
|
|
89
|
+
*/
|
|
90
|
+
function loadManifestContext(definition, projectDir) {
|
|
91
|
+
const root = definition.trust_root ?? projectDir;
|
|
92
|
+
if (root === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
const manifestPath = join(root, 'realm.yaml');
|
|
95
|
+
let bytes;
|
|
96
|
+
try {
|
|
97
|
+
bytes = readFileSync(manifestPath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined; // absent manifest = defaults only, valid for config-free projects
|
|
101
|
+
}
|
|
102
|
+
let raw;
|
|
103
|
+
try {
|
|
104
|
+
raw = loadYaml(bytes.toString('utf8'));
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
throw new Error(`Deployment manifest '${manifestPath}' is not valid YAML: ${errMsg(err)}`);
|
|
108
|
+
}
|
|
109
|
+
const errors = validateDeploymentManifest(raw);
|
|
110
|
+
if (errors.length > 0) {
|
|
111
|
+
throw new Error(`Deployment manifest '${manifestPath}' is invalid:\n ${errors.join('\n ')}`);
|
|
112
|
+
}
|
|
113
|
+
const document = raw;
|
|
114
|
+
// dotenv bytes participate in the freshness hash ONLY (rotation detection) — never in
|
|
115
|
+
// drift evidence.
|
|
116
|
+
const dotenvPath = join(dirname(manifestPath), document.secrets?.dotenv ?? '.env');
|
|
117
|
+
let dotenvBytes;
|
|
118
|
+
try {
|
|
119
|
+
dotenvBytes = readFileSync(dotenvPath);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
dotenvBytes = Buffer.alloc(0);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
document,
|
|
126
|
+
path: manifestPath,
|
|
127
|
+
realPath: realpathOrFail(manifestPath, `deployment manifest '${manifestPath}'`),
|
|
128
|
+
dir: dirname(manifestPath),
|
|
129
|
+
rootReal: realpathOrFail(root, `deployment root '${root}'`),
|
|
130
|
+
bytes,
|
|
131
|
+
contentHash: sha256HexOf(bytes),
|
|
132
|
+
dotenvBytes,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** `use:` disambiguation — contains `/` or a module file extension → module ref. */
|
|
136
|
+
function isModuleRef(use) {
|
|
137
|
+
const beforeHash = use.split('#')[0];
|
|
138
|
+
return beforeHash.includes('/') || /\.(js|mjs|cjs|ts|mts|cts)$/.test(beforeHash);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Loads the project extensions and deployment configuration for a definition:
|
|
142
|
+
* workflow-declared CODE modules + the deployment manifest's constructed instances,
|
|
143
|
+
* one registry, one namespace, one collision map. No extensions, no manifest → the
|
|
144
|
+
* process-stable filesystem-only default registry.
|
|
145
|
+
*
|
|
146
|
+
* CONTRACT: returned registries are shared process-lifetime cache entries. The only
|
|
147
|
+
* permitted mutation is `getOrCreateRateLimiter` (serve bucket persistence, by design).
|
|
148
|
+
*/
|
|
149
|
+
export async function loadProjectExtensions(definition, opts) {
|
|
150
|
+
const secretMode = opts?.secretMode ?? 'real';
|
|
151
|
+
const moduleRefs = resolveModuleRefs(definition, opts?.overrideModule);
|
|
152
|
+
const manifestCtx = loadManifestContext(definition, opts?.projectDir);
|
|
153
|
+
if (moduleRefs === undefined && manifestCtx === undefined) {
|
|
154
|
+
// Extension-free, manifest-free fallback: process-stable default registry.
|
|
155
|
+
let entry = cache.get(DEFAULT_REGISTRY_CACHE_KEY);
|
|
156
|
+
if (entry === undefined) {
|
|
157
|
+
entry = { result: { registry: createDefaultRegistry(), manifest: emptyManifest() } };
|
|
158
|
+
cache.set(DEFAULT_REGISTRY_CACHE_KEY, entry);
|
|
159
|
+
}
|
|
160
|
+
return entry.result;
|
|
161
|
+
}
|
|
162
|
+
// Resolve manifest `use:` module refs up front — they are part of the cache key.
|
|
163
|
+
const useModuleRefs = manifestCtx !== undefined ? resolveUseModuleRefs(manifestCtx) : new Map();
|
|
164
|
+
const cacheKey = [
|
|
165
|
+
`mode:${secretMode}`,
|
|
166
|
+
`manifest:${manifestCtx?.realPath ?? '\u0000none'}`,
|
|
167
|
+
`root:${manifestCtx?.rootReal ?? '\u0000none'}`,
|
|
168
|
+
...(moduleRefs ?? []).map((m) => `ext:${m.resolved}`),
|
|
169
|
+
...[...useModuleRefs.values()].sort().map((p) => `use:${p}`),
|
|
170
|
+
].join('\n');
|
|
171
|
+
const freshness = manifestCtx !== undefined
|
|
172
|
+
? sha256HexOf(Buffer.concat([manifestCtx.bytes, Buffer.from('\u0000'), manifestCtx.dotenvBytes]))
|
|
173
|
+
: undefined;
|
|
174
|
+
const cached = cache.get(cacheKey);
|
|
175
|
+
if (cached !== undefined && cached.freshness === freshness)
|
|
176
|
+
return cached.result;
|
|
177
|
+
// freshness mismatch → rebuild-and-REPLACE (dotenv rotation / manifest edit reaches
|
|
178
|
+
// the next run without restart; rate-limiter buckets reset with the new registry).
|
|
179
|
+
const registry = createDefaultRegistry();
|
|
180
|
+
const builtinNames = {
|
|
181
|
+
adapter: new Set(registry.names('adapter')),
|
|
182
|
+
handler: new Set(registry.names('handler')),
|
|
183
|
+
processor: new Set(registry.names('processor')),
|
|
184
|
+
};
|
|
185
|
+
const claimedBy = new Map();
|
|
186
|
+
const manifest = {
|
|
187
|
+
modules: moduleRefs ?? [],
|
|
188
|
+
adapters: [],
|
|
189
|
+
handlers: [],
|
|
190
|
+
processors: [],
|
|
191
|
+
};
|
|
192
|
+
const moduleFormats = new Map();
|
|
193
|
+
for (const ref of moduleRefs ?? []) {
|
|
194
|
+
const { namespace, format } = await importModuleNamespace(ref.resolved, ref.declared);
|
|
195
|
+
moduleFormats.set(ref.resolved, format);
|
|
196
|
+
applyModule(defaultExportOf(namespace, ref.declared), ref, registry, builtinNames, claimedBy, manifest);
|
|
197
|
+
}
|
|
198
|
+
const sentinelWarnings = [];
|
|
199
|
+
let notifiers;
|
|
200
|
+
let secretValues;
|
|
201
|
+
const identityModules = (moduleRefs ?? []).map((ref) => ({
|
|
202
|
+
declared: ref.declared,
|
|
203
|
+
resolved: ref.resolved,
|
|
204
|
+
format: moduleFormats.get(ref.resolved) ?? 'esm',
|
|
205
|
+
}));
|
|
206
|
+
let secretNames = [];
|
|
207
|
+
if (manifestCtx !== undefined) {
|
|
208
|
+
const applied = await applyDeploymentManifest(manifestCtx, useModuleRefs, registry, builtinNames, claimedBy, manifest, secretMode, sentinelWarnings);
|
|
209
|
+
notifiers = applied.notifiers;
|
|
210
|
+
secretNames = applied.secretNames;
|
|
211
|
+
secretValues = applied.secretValues;
|
|
212
|
+
for (const [use, real] of useModuleRefs) {
|
|
213
|
+
identityModules.push({
|
|
214
|
+
declared: use,
|
|
215
|
+
resolved: real,
|
|
216
|
+
format: moduleFormats.get(real) ?? applied.useFormats.get(real) ?? 'esm',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Drift evidence: identity captured ONCE per cache entry at module-LOAD time. The
|
|
221
|
+
// manifest contributes its content hash (never a sweep root); `use:`-resolved module
|
|
222
|
+
// FILES contribute sweep roots exactly like extension-list modules; secret NAMES are
|
|
223
|
+
// recorded, never compared; the freshness hash is NEVER recorded.
|
|
224
|
+
const overrideActive = opts?.overrideModule !== undefined;
|
|
225
|
+
try {
|
|
226
|
+
registry.setIdentity(computeExtensionIdentity(identityModules, {
|
|
227
|
+
...(definition.trust_root !== undefined ? { trustRoot: definition.trust_root } : {}),
|
|
228
|
+
...(overrideActive ? { overrideActive: true } : {}),
|
|
229
|
+
...(manifestCtx !== undefined
|
|
230
|
+
? { manifest: { path: manifestCtx.path, content_hash: manifestCtx.contentHash } }
|
|
231
|
+
: {}),
|
|
232
|
+
secretNames,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
console.error(`[realm] extension identity capture failed: ${errMsg(err)} — drift evidence will carry an error entry for this load.`);
|
|
237
|
+
registry.setIdentity(errorExtensionIdentityEntry(`identity capture failed: ${errMsg(err)}`, overrideActive ? { overrideActive: true } : {}));
|
|
238
|
+
}
|
|
239
|
+
const result = {
|
|
240
|
+
registry,
|
|
241
|
+
manifest,
|
|
242
|
+
...(notifiers !== undefined ? { notifiers } : {}),
|
|
243
|
+
...(sentinelWarnings.length > 0 ? { sentinelWarnings } : {}),
|
|
244
|
+
...(secretValues !== undefined && secretValues.length > 0 ? { secretValues } : {}),
|
|
245
|
+
};
|
|
246
|
+
cache.set(cacheKey, { result, ...(freshness !== undefined ? { freshness } : {}) });
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
/** Resolves every manifest `use:` module ref (realpathed + containment-checked). */
|
|
250
|
+
function resolveUseModuleRefs(ctx) {
|
|
251
|
+
const resolvedRefs = new Map();
|
|
252
|
+
const sections = [
|
|
253
|
+
ctx.document.adapters,
|
|
254
|
+
ctx.document.handlers,
|
|
255
|
+
ctx.document.processors,
|
|
256
|
+
];
|
|
257
|
+
for (const section of sections) {
|
|
258
|
+
for (const entry of Object.values(section ?? {})) {
|
|
259
|
+
if (entry.use === undefined || !isModuleRef(entry.use))
|
|
260
|
+
continue;
|
|
261
|
+
const modulePath = entry.use.split('#')[0];
|
|
262
|
+
if (resolvedRefs.has(entry.use))
|
|
263
|
+
continue;
|
|
264
|
+
const resolved = resolve(ctx.dir, modulePath);
|
|
265
|
+
const real = realpathOrFail(resolved, `manifest module '${modulePath}' (from '${ctx.path}', resolved: ${resolved})`);
|
|
266
|
+
if (!isContainedIn(real, ctx.rootReal)) {
|
|
267
|
+
throw new Error(`Deployment manifest module '${modulePath}' resolves to '${real}', which is OUTSIDE ` +
|
|
268
|
+
`the deployment root '${ctx.rootReal}'. Manifest 'use:' modules must live within ` +
|
|
269
|
+
`the deployment root — move the module inside it, or fix the reference in '${ctx.path}'.`);
|
|
270
|
+
}
|
|
271
|
+
resolvedRefs.set(entry.use, real);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return resolvedRefs;
|
|
275
|
+
}
|
|
276
|
+
/** Parses/constructs/registers everything the manifest declares. */
|
|
277
|
+
async function applyDeploymentManifest(ctx, useModuleRefs, registry, builtinNames, claimedBy, manifest, secretMode, sentinelWarnings) {
|
|
278
|
+
const refs = collectManifestSecretRefs(ctx.document);
|
|
279
|
+
const resolved = resolveManifestSecrets(refs, ctx.document.secrets, ctx.dir, secretMode);
|
|
280
|
+
const resolveName = (name) => resolved.values[name] ?? '';
|
|
281
|
+
const secretValues = Object.values(resolved.values);
|
|
282
|
+
const useFormats = new Map();
|
|
283
|
+
const secretBearingHandlers = [];
|
|
284
|
+
const secretBearingProcessors = [];
|
|
285
|
+
const sections = [
|
|
286
|
+
{ key: 'adapters', type: 'adapter' },
|
|
287
|
+
{ key: 'handlers', type: 'handler' },
|
|
288
|
+
{ key: 'processors', type: 'processor' },
|
|
289
|
+
];
|
|
290
|
+
for (const section of sections) {
|
|
291
|
+
for (const [name, entry] of Object.entries(ctx.document[section.key] ?? {})) {
|
|
292
|
+
const site = `${section.key}.${name}`;
|
|
293
|
+
if (entry.use === undefined) {
|
|
294
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} needs 'use:' — a catalog name ` +
|
|
295
|
+
`(adapters) or a module reference './path.js#Export'.`);
|
|
296
|
+
}
|
|
297
|
+
const factory = await resolveEntryFactory(ctx, useModuleRefs, useFormats, section.type, name, entry, site);
|
|
298
|
+
const hasRefs = findSecretRefSites(entry.config, site).sites.length > 0;
|
|
299
|
+
if (section.type === 'handler' && hasRefs)
|
|
300
|
+
secretBearingHandlers.push(name);
|
|
301
|
+
if (section.type === 'processor' && hasRefs)
|
|
302
|
+
secretBearingProcessors.push(name);
|
|
303
|
+
const config = interpolateConfigTree(entry.config ?? {}, resolveName);
|
|
304
|
+
let instance;
|
|
305
|
+
try {
|
|
306
|
+
instance = factory({ id: name, config });
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
const redacted = redactSecretValues(`Deployment manifest '${ctx.path}': constructing ${site} (use: ${entry.use}) failed: ${errMsg(err)}`, secretValues);
|
|
310
|
+
if (secretMode === 'sentinel') {
|
|
311
|
+
// Sentinel degrade: warn-and-skip — never a hard failure, never silent.
|
|
312
|
+
sentinelWarnings.push(`${redacted} (sentinel mode — entry skipped)`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
throw new Error(redacted);
|
|
316
|
+
}
|
|
317
|
+
probeShape(instance, section.type, PROBE_MEMBERS[section.type], name, {
|
|
318
|
+
declared: `${ctx.path}#${site}`,
|
|
319
|
+
resolved: ctx.realPath,
|
|
320
|
+
});
|
|
321
|
+
const claimKey = `${section.type}:${name}`;
|
|
322
|
+
const previousClaim = claimedBy.get(claimKey);
|
|
323
|
+
if (previousClaim !== undefined) {
|
|
324
|
+
throw new Error(`Extension ${section.type} '${name}' is declared by both '${previousClaim}' and the ` +
|
|
325
|
+
`deployment manifest '${ctx.path}' — manifest entries and code-module exports ` +
|
|
326
|
+
`share one namespace; names must be unique.`);
|
|
327
|
+
}
|
|
328
|
+
claimedBy.set(claimKey, `manifest ${ctx.path}`);
|
|
329
|
+
if (builtinNames[section.type].has(name)) {
|
|
330
|
+
console.warn(`[realm] manifest ${section.type} '${name}' overrides the built-in ${section.type} '${name}'.`);
|
|
331
|
+
}
|
|
332
|
+
registry.register(section.type, name, instance);
|
|
333
|
+
manifest[section.key].push(name);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (secretBearingHandlers.length > 0)
|
|
337
|
+
manifest.secret_bearing_handlers = secretBearingHandlers;
|
|
338
|
+
if (secretBearingProcessors.length > 0)
|
|
339
|
+
manifest.secret_bearing_processors = secretBearingProcessors;
|
|
340
|
+
let notifiers;
|
|
341
|
+
const slackGate = ctx.document.notifiers?.slack_gate;
|
|
342
|
+
if (slackGate !== undefined) {
|
|
343
|
+
notifiers = {
|
|
344
|
+
slack_gate: interpolateConfigTree(slackGate.config, resolveName),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
...(notifiers !== undefined ? { notifiers } : {}),
|
|
349
|
+
secretNames: [...new Set(refs.map((r) => r.name))].sort(),
|
|
350
|
+
// Values-only, frozen, real-mode only: the agent redaction feed. Never persisted.
|
|
351
|
+
secretValues: resolved.sentinel
|
|
352
|
+
? Object.freeze([])
|
|
353
|
+
: Object.freeze([...new Set(Object.values(resolved.values))].filter((v) => v.length >= 4)),
|
|
354
|
+
useFormats,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/** Resolves an entry's factory: catalog name (adapters only) or module ref `#Export`. */
|
|
358
|
+
async function resolveEntryFactory(ctx, useModuleRefs, useFormats, type, name, entry, site) {
|
|
359
|
+
const use = entry.use;
|
|
360
|
+
if (!isModuleRef(use)) {
|
|
361
|
+
if (type !== 'adapter') {
|
|
362
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} — there is no built-in catalog for ` +
|
|
363
|
+
`${type}s; use a module reference './path.js#ExportName'.`);
|
|
364
|
+
}
|
|
365
|
+
const catalogEntry = BUILTIN_ADAPTER_CATALOG[use];
|
|
366
|
+
if (catalogEntry === undefined) {
|
|
367
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} — unknown catalog adapter '${use}'. ` +
|
|
368
|
+
`Valid catalog names (realm v${VERSION}): ${Object.keys(BUILTIN_ADAPTER_CATALOG).join(', ')}. ` +
|
|
369
|
+
`For a custom adapter use a module reference './path.js#ExportName'.`);
|
|
370
|
+
}
|
|
371
|
+
if (catalogEntry.configless === true && entry.config !== undefined) {
|
|
372
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} — catalog adapter '${use}' takes no ` +
|
|
373
|
+
`config; remove the 'config:' block.`);
|
|
374
|
+
}
|
|
375
|
+
return catalogEntry.factory;
|
|
376
|
+
}
|
|
377
|
+
const [modulePath, exportName] = use.split('#');
|
|
378
|
+
const real = useModuleRefs.get(use) ?? useModuleRefs.get(modulePath);
|
|
379
|
+
if (real === undefined) {
|
|
380
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} — unresolved module '${use}'.`);
|
|
381
|
+
}
|
|
382
|
+
const { namespace, format } = await importModuleNamespace(real, `${ctx.path}#${site}`);
|
|
383
|
+
useFormats.set(real, format);
|
|
384
|
+
const candidate = pickExport(namespace, exportName, use, ctx.path, site);
|
|
385
|
+
if (typeof candidate !== 'function') {
|
|
386
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} — '${use}' must export a FACTORY ` +
|
|
387
|
+
`(ctx: { id, config }) => instance; got ${typeof candidate}.`);
|
|
388
|
+
}
|
|
389
|
+
return candidate;
|
|
390
|
+
}
|
|
391
|
+
/** Picks the named (or default) export, naming the available exports on a miss. */
|
|
392
|
+
function pickExport(namespace, exportName, use, manifestPath, site) {
|
|
393
|
+
const unwrapped = unwrapCjsNamespace(namespace);
|
|
394
|
+
const wanted = exportName ?? 'default';
|
|
395
|
+
const candidate = unwrapped[wanted];
|
|
396
|
+
if (candidate !== undefined)
|
|
397
|
+
return candidate;
|
|
398
|
+
const available = Object.keys(unwrapped).filter((k) => k !== '__esModule');
|
|
399
|
+
throw new Error(`Deployment manifest '${manifestPath}': ${site} — module '${use}' has no ` +
|
|
400
|
+
`${exportName !== undefined ? `export '${exportName}'` : 'default export'}. ` +
|
|
401
|
+
`Available exports: ${available.length > 0 ? available.join(', ') : '(none)'}.`);
|
|
402
|
+
}
|
|
403
|
+
/** Flattens the tsc-CJS `{ default: { __esModule, ...exports } }` interop wrapper. */
|
|
404
|
+
function unwrapCjsNamespace(namespace) {
|
|
405
|
+
const def = namespace['default'];
|
|
406
|
+
if (typeof def === 'object' &&
|
|
407
|
+
def !== null &&
|
|
408
|
+
def['__esModule'] === true) {
|
|
409
|
+
return { ...namespace, ...def };
|
|
410
|
+
}
|
|
411
|
+
return namespace;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Resolves the list of extension CODE modules. Returns undefined when none are declared.
|
|
415
|
+
* Enforces the trust model: agent-origin refusal, re-register guidance when resolution
|
|
416
|
+
* metadata is missing, and realpath containment within trust_root.
|
|
417
|
+
*/
|
|
418
|
+
function resolveModuleRefs(definition, overrideModule) {
|
|
419
|
+
if (overrideModule !== undefined) {
|
|
420
|
+
const resolved = resolve(overrideModule);
|
|
421
|
+
const real = realpathOrFail(resolved, `--extensions-module '${overrideModule}'`);
|
|
422
|
+
// Loud by design: the override replaces whatever the workflow declared.
|
|
423
|
+
console.warn(`[realm] --extensions-module override active: loading '${overrideModule}' (resolved: ${real}). ` +
|
|
424
|
+
`Declared workflow extensions are IGNORED.`);
|
|
425
|
+
return [{ declared: overrideModule, resolved: real }];
|
|
426
|
+
}
|
|
427
|
+
const declared = normalizeDeclared(definition.extensions);
|
|
428
|
+
if (declared === undefined || declared.length === 0)
|
|
429
|
+
return undefined;
|
|
430
|
+
if (definition.origin === 'agent') {
|
|
431
|
+
throw new Error(`Workflow '${definition.id}' was created by an agent (origin: 'agent') but declares 'extensions' — ` +
|
|
432
|
+
`refusing to load extension code. Extensions are register-time and operator-only: ` +
|
|
433
|
+
`register the workflow from its YAML file with 'realm workflow register <path>'.`);
|
|
434
|
+
}
|
|
435
|
+
if (definition.source_dir === undefined || definition.trust_root === undefined) {
|
|
436
|
+
throw new Error(`Workflow '${definition.id}' declares 'extensions' but its stored definition carries no ` +
|
|
437
|
+
`source_dir/trust_root resolution metadata (registered by an older Realm version). ` +
|
|
438
|
+
`Re-register this workflow: realm workflow register <path-to-workflow>`);
|
|
439
|
+
}
|
|
440
|
+
const trustRootReal = realpathOrFail(definition.trust_root, `trust_root '${definition.trust_root}' of workflow '${definition.id}'`);
|
|
441
|
+
return declared.map((declaredPath) => {
|
|
442
|
+
const resolved = resolve(definition.source_dir, declaredPath);
|
|
443
|
+
const real = realpathOrFail(resolved, `extension module '${declaredPath}' of workflow '${definition.id}' (resolved: ${resolved})`);
|
|
444
|
+
if (!isContainedIn(real, trustRootReal)) {
|
|
445
|
+
throw new Error(`Extension module '${declaredPath}' resolves to '${real}', which is OUTSIDE the ` +
|
|
446
|
+
`workflow's trust root '${trustRootReal}'. Extension modules must live within the ` +
|
|
447
|
+
`project containing the workflow (nearest package.json/.git ancestor of the workflow ` +
|
|
448
|
+
`directory). Move the module inside the project, or re-register the workflow from ` +
|
|
449
|
+
`its real location.`);
|
|
450
|
+
}
|
|
451
|
+
return { declared: declaredPath, resolved: real };
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Imports one module (jiti-bridged for TypeScript paths) and returns its full namespace
|
|
456
|
+
* plus the load format (ts-jiti for TypeScript paths, cjs for .cjs / detected CJS-interop
|
|
457
|
+
* wrappers, esm otherwise).
|
|
458
|
+
*/
|
|
459
|
+
async function importModuleNamespace(resolvedPath, label) {
|
|
460
|
+
const isTypescript = /\.(ts|mts|cts)$/.test(resolvedPath);
|
|
461
|
+
let namespace;
|
|
462
|
+
if (isTypescript) {
|
|
463
|
+
namespace = await importViaJiti(resolvedPath, label);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
try {
|
|
467
|
+
namespace = (await import(pathToFileURL(resolvedPath).href));
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
throw new Error(`Failed to import module '${label}' (${resolvedPath}): ${errMsg(err)}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
let format = isTypescript
|
|
474
|
+
? 'ts-jiti'
|
|
475
|
+
: resolvedPath.endsWith('.cjs')
|
|
476
|
+
? 'cjs'
|
|
477
|
+
: 'esm';
|
|
478
|
+
const def = namespace['default'];
|
|
479
|
+
if (!isTypescript &&
|
|
480
|
+
typeof def === 'object' &&
|
|
481
|
+
def !== null &&
|
|
482
|
+
def['__esModule'] === true) {
|
|
483
|
+
format = 'cjs';
|
|
484
|
+
}
|
|
485
|
+
return { namespace, format };
|
|
486
|
+
}
|
|
487
|
+
/** The workflow-extensions default-export contract: unwraps tsc-CJS interop, requires default. */
|
|
488
|
+
function defaultExportOf(namespace, declared) {
|
|
489
|
+
let exported = namespace['default'];
|
|
490
|
+
// CJS-ESM interop: `import()` of a tsc-CommonJS build ("type" not "module") yields
|
|
491
|
+
// namespace.default = module.exports = { __esModule: true, default: <manifest> }.
|
|
492
|
+
if (typeof exported === 'object' &&
|
|
493
|
+
exported !== null &&
|
|
494
|
+
exported['__esModule'] === true &&
|
|
495
|
+
'default' in exported) {
|
|
496
|
+
exported = exported['default'];
|
|
497
|
+
}
|
|
498
|
+
if (exported === undefined) {
|
|
499
|
+
throw new Error(`Extension module '${declared}' has no default export — export a declarative object: ` +
|
|
500
|
+
`export default { adapters: { name: instance }, handlers: { ... }, processors: { ... } }`);
|
|
501
|
+
}
|
|
502
|
+
return exported;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* TypeScript modules load through jiti resolved from the MODULE'S OWN directory (the consumer
|
|
506
|
+
* project's node_modules) — never from the CLI install (npx/global installs carry no jiti).
|
|
507
|
+
* Compiled JS is the documented default; jiti is a consumer-side optional peer.
|
|
508
|
+
*/
|
|
509
|
+
async function importViaJiti(resolvedPath, label) {
|
|
510
|
+
const requireFromModule = createRequire(resolvedPath);
|
|
511
|
+
let jitiPath;
|
|
512
|
+
try {
|
|
513
|
+
jitiPath = requireFromModule.resolve('jiti');
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
throw new Error(`Module '${label}' (${resolvedPath}) is TypeScript, but 'jiti' is not ` +
|
|
517
|
+
`installed in your project. Install jiti in your project (npm install --save-dev jiti), ` +
|
|
518
|
+
`or compile the module to JS and declare the compiled path.`);
|
|
519
|
+
}
|
|
520
|
+
let jitiModule;
|
|
521
|
+
try {
|
|
522
|
+
jitiModule = (await import(pathToFileURL(jitiPath).href));
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
throw new Error(`Failed to load 'jiti' from '${jitiPath}': ${errMsg(err)}`);
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
// jiti v2: createJiti(parentPath).import(path) → module namespace.
|
|
529
|
+
const createJiti = jitiModule['createJiti'];
|
|
530
|
+
if (typeof createJiti === 'function') {
|
|
531
|
+
const jiti = createJiti(resolvedPath);
|
|
532
|
+
return (await jiti.import(resolvedPath));
|
|
533
|
+
}
|
|
534
|
+
// jiti v1: default export is a factory returning a require-like function.
|
|
535
|
+
const factory = jitiModule['default'];
|
|
536
|
+
if (typeof factory === 'function') {
|
|
537
|
+
const jitiRequire = factory(resolvedPath, { interopDefault: false });
|
|
538
|
+
const required = jitiRequire(resolvedPath);
|
|
539
|
+
if (typeof required === 'object' && required !== null && 'default' in required) {
|
|
540
|
+
return required;
|
|
541
|
+
}
|
|
542
|
+
return { default: required };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
throw new Error(`Failed to import TypeScript module '${label}' via jiti: ${errMsg(err)}`);
|
|
547
|
+
}
|
|
548
|
+
throw new Error(`Unrecognized 'jiti' package shape at '${jitiPath}' — upgrade jiti in your project, or ` +
|
|
549
|
+
`compile the extension module to JS.`);
|
|
550
|
+
}
|
|
551
|
+
/** Reads a property defensively — a throwing getter becomes a clean validation error. */
|
|
552
|
+
function safeGet(obj, key, context) {
|
|
553
|
+
try {
|
|
554
|
+
return obj[key];
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
throw new Error(`${context}: reading '${key}' threw: ${errMsg(err)}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Duck-validates and applies one module's declarative default export onto the registry.
|
|
562
|
+
* Collision policy: overriding a BUILT-IN name → WARN and allow; the same name claimed twice
|
|
563
|
+
* across declared modules → ERROR. Registration name = map key (a differing instance `id` WARNs).
|
|
564
|
+
*/
|
|
565
|
+
function applyModule(exported, ref, registry, builtinNames, claimedBy, manifest) {
|
|
566
|
+
const context = `Extension module '${ref.declared}'`;
|
|
567
|
+
if (typeof exported !== 'object' || exported === null || Array.isArray(exported)) {
|
|
568
|
+
throw new Error(`${context}: default export must be a plain object ({ adapters?, handlers?, processors? }), ` +
|
|
569
|
+
`got ${Array.isArray(exported) ? 'array' : typeof exported}.`);
|
|
570
|
+
}
|
|
571
|
+
// Reject registry-instance-like exports: the contract is declarative maps, not a registry.
|
|
572
|
+
if (typeof safeGet(exported, 'register', context) === 'function' ||
|
|
573
|
+
typeof safeGet(exported, 'getAdapter', context) === 'function') {
|
|
574
|
+
throw new Error(`${context}: default export looks like an ExtensionRegistry instance. Export a declarative ` +
|
|
575
|
+
`object instead: export default { adapters: { name: instance }, handlers: { ... }, ` +
|
|
576
|
+
`processors: { ... } }`);
|
|
577
|
+
}
|
|
578
|
+
const allowedKeys = new Set(EXTENSION_SURFACES.map((s) => s.mapKey));
|
|
579
|
+
for (const key of Object.keys(exported)) {
|
|
580
|
+
if (!allowedKeys.has(key)) {
|
|
581
|
+
throw new Error(`${context}: unknown key '${key}' in default export — allowed keys are ` +
|
|
582
|
+
`'adapters', 'handlers', 'processors'.`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
for (const surface of EXTENSION_SURFACES) {
|
|
586
|
+
const map = safeGet(exported, surface.mapKey, context);
|
|
587
|
+
if (map === undefined)
|
|
588
|
+
continue;
|
|
589
|
+
if (typeof map !== 'object' || map === null || Array.isArray(map)) {
|
|
590
|
+
throw new Error(`${context}: '${surface.mapKey}' must be an object map of name → instance, got ` +
|
|
591
|
+
`${Array.isArray(map) ? 'array' : typeof map}.`);
|
|
592
|
+
}
|
|
593
|
+
for (const name of Object.keys(map)) {
|
|
594
|
+
const impl = safeGet(map, name, `${context}, ${surface.type} '${name}'`);
|
|
595
|
+
probeShape(impl, surface.type, surface.probeMembers, name, ref);
|
|
596
|
+
warnOnIdMismatch(impl, surface.type, name, ref);
|
|
597
|
+
const claimKey = `${surface.type}:${name}`;
|
|
598
|
+
const previousClaim = claimedBy.get(claimKey);
|
|
599
|
+
if (previousClaim !== undefined) {
|
|
600
|
+
throw new Error(`Extension ${surface.type} '${name}' is declared by both '${previousClaim}' and ` +
|
|
601
|
+
`'${ref.declared}' — extension names must be unique across declared modules.`);
|
|
602
|
+
}
|
|
603
|
+
claimedBy.set(claimKey, ref.declared);
|
|
604
|
+
if (builtinNames[surface.type].has(name)) {
|
|
605
|
+
console.warn(`[realm] extension ${surface.type} '${name}' from '${ref.declared}' overrides the ` +
|
|
606
|
+
`built-in ${surface.type} '${name}'.`);
|
|
607
|
+
}
|
|
608
|
+
// Safe: probeShape validated the minimal distinguishing members of each interface.
|
|
609
|
+
registry.register(surface.type, name, impl);
|
|
610
|
+
manifest[surface.mapKey].push(name);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/** Probes the minimal distinguishing members of the extension interface — no instanceof. */
|
|
615
|
+
function probeShape(impl, type, members, name, ref) {
|
|
616
|
+
const context = `Extension ${type} '${name}' in '${ref.declared}'`;
|
|
617
|
+
if (typeof impl !== 'object' || impl === null) {
|
|
618
|
+
throw new Error(`${context}: expected an object instance, got ${impl === null ? 'null' : typeof impl}.`);
|
|
619
|
+
}
|
|
620
|
+
for (const member of members) {
|
|
621
|
+
const value = safeGet(impl, member, context);
|
|
622
|
+
if (typeof value !== 'function') {
|
|
623
|
+
throw new Error(`${context}: missing callable '${member}' — ${type}s must implement ` +
|
|
624
|
+
`${members.map((m) => `'${m}'`).join('/')} per the @sensigo/realm ${type === 'adapter' ? 'ServiceAdapter' : type === 'handler' ? 'StepHandler' : 'Processor'} interface.`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/** Registration name = map key; a differing instance `id` is evidence worth surfacing. */
|
|
629
|
+
function warnOnIdMismatch(impl, type, name, ref) {
|
|
630
|
+
let id;
|
|
631
|
+
try {
|
|
632
|
+
id = impl['id'];
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
return; // a throwing id getter is not worth failing the load over — probe already passed
|
|
636
|
+
}
|
|
637
|
+
if (typeof id === 'string' && id !== name) {
|
|
638
|
+
console.warn(`[realm] extension ${type} registered as '${name}' (map key) but its instance id is ` +
|
|
639
|
+
`'${id}' (module '${ref.declared}'). The registration name is the map key.`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Builds a `registryProvider` for the MCP server (serve / mcp stdio): resolves each
|
|
644
|
+
* definition through the process-lifetime project loader. `--extensions-module` overrides
|
|
645
|
+
* CODE modules; `projectDir` anchors the manifest for definitions without trust_root
|
|
646
|
+
* (`realm mcp` passes it ONLY when --project was given — no cwd default there).
|
|
647
|
+
*/
|
|
648
|
+
export function makeRegistryProvider(overrideModule, projectDir) {
|
|
649
|
+
return async (definition) => (await loadProjectExtensions(definition, {
|
|
650
|
+
...(overrideModule !== undefined ? { overrideModule } : {}),
|
|
651
|
+
...(projectDir !== undefined ? { projectDir } : {}),
|
|
652
|
+
})).registry;
|
|
653
|
+
}
|
|
654
|
+
//# sourceMappingURL=load-project-extensions.js.map
|