@sensigo/realm-cli 0.13.0 → 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.
Files changed (66) hide show
  1. package/dist/agent/gate/slack-gate-notifier.d.ts +1 -1
  2. package/dist/agent/gate/slack-gate-notifier.d.ts.map +1 -1
  3. package/dist/agent/gate/slack-gate-notifier.js +1 -1
  4. package/dist/agent/gate/slack-gate-notifier.js.map +1 -1
  5. package/dist/agent/gate/slack-gate-server.d.ts +1 -1
  6. package/dist/agent/gate/slack-gate-server.d.ts.map +1 -1
  7. package/dist/agent/gate/slack-socket-client.d.ts +1 -1
  8. package/dist/agent/gate/slack-socket-client.d.ts.map +1 -1
  9. package/dist/agent/providers/agent-utils.d.ts +7 -2
  10. package/dist/agent/providers/agent-utils.d.ts.map +1 -1
  11. package/dist/agent/providers/agent-utils.js +20 -6
  12. package/dist/agent/providers/agent-utils.js.map +1 -1
  13. package/dist/agent/run-agent.d.ts +6 -0
  14. package/dist/agent/run-agent.d.ts.map +1 -1
  15. package/dist/agent/run-agent.js +5 -0
  16. package/dist/agent/run-agent.js.map +1 -1
  17. package/dist/agent/run-attach.d.ts +5 -6
  18. package/dist/agent/run-attach.d.ts.map +1 -1
  19. package/dist/agent/run-attach.js +7 -3
  20. package/dist/agent/run-attach.js.map +1 -1
  21. package/dist/commands/agent.d.ts +12 -11
  22. package/dist/commands/agent.d.ts.map +1 -1
  23. package/dist/commands/agent.js +64 -105
  24. package/dist/commands/agent.js.map +1 -1
  25. package/dist/commands/init.d.ts.map +1 -1
  26. package/dist/commands/init.js +55 -29
  27. package/dist/commands/init.js.map +1 -1
  28. package/dist/commands/inspect.d.ts.map +1 -1
  29. package/dist/commands/inspect.js +11 -0
  30. package/dist/commands/inspect.js.map +1 -1
  31. package/dist/commands/mcp.d.ts.map +1 -1
  32. package/dist/commands/mcp.js +6 -2
  33. package/dist/commands/mcp.js.map +1 -1
  34. package/dist/commands/register.d.ts.map +1 -1
  35. package/dist/commands/register.js +21 -6
  36. package/dist/commands/register.js.map +1 -1
  37. package/dist/commands/run.d.ts.map +1 -1
  38. package/dist/commands/run.js +8 -2
  39. package/dist/commands/run.js.map +1 -1
  40. package/dist/commands/serve.d.ts.map +1 -1
  41. package/dist/commands/serve.js +3 -2
  42. package/dist/commands/serve.js.map +1 -1
  43. package/dist/commands/test.d.ts.map +1 -1
  44. package/dist/commands/test.js +15 -4
  45. package/dist/commands/test.js.map +1 -1
  46. package/dist/commands/validate.d.ts.map +1 -1
  47. package/dist/commands/validate.js +6 -1
  48. package/dist/commands/validate.js.map +1 -1
  49. package/dist/extensions/extension-identity.d.ts +19 -0
  50. package/dist/extensions/extension-identity.d.ts.map +1 -1
  51. package/dist/extensions/extension-identity.js +24 -0
  52. package/dist/extensions/extension-identity.js.map +1 -1
  53. package/dist/extensions/load-project-extensions.d.ts +38 -15
  54. package/dist/extensions/load-project-extensions.d.ts.map +1 -1
  55. package/dist/extensions/load-project-extensions.js +377 -81
  56. package/dist/extensions/load-project-extensions.js.map +1 -1
  57. package/dist/extensions/manifest-secrets.d.ts +36 -0
  58. package/dist/extensions/manifest-secrets.d.ts.map +1 -0
  59. package/dist/extensions/manifest-secrets.js +126 -0
  60. package/dist/extensions/manifest-secrets.js.map +1 -0
  61. package/dist/index.js +1 -1
  62. package/package.json +4 -4
  63. package/dist/agent/preflight.d.ts +0 -36
  64. package/dist/agent/preflight.d.ts.map +0 -1
  65. package/dist/agent/preflight.js +0 -81
  66. package/dist/agent/preflight.js.map +0 -1
@@ -1,31 +1,59 @@
1
- // load-project-extensions.ts — the ONE loader for workflow-declared project extension modules.
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.
2
5
  //
3
- // Every step-executing or config-validating CLI entry point (run, agent, listen, serve, mcp,
4
- // test, validate, register, watch) resolves extensions through this function. Core resolves and
5
- // stores PATHS only (source_dir / trust_root / declared relative paths on the definition) — the
6
- // dynamic import lives here, in the CLI composition layer.
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.
7
10
  //
8
- // Trust model: extension module paths originate ONLY from operator-registered workflow
9
- // definitions or from the operator-typed --extensions-module flag never from request data.
10
- // Declared paths resolve against the definition's source_dir and must realpath-contain within
11
- // its trust_root (nearest package.json/.git ancestor, derived at registration time).
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.
12
20
  import { createRequire } from 'node:module';
13
- import { realpathSync } from 'node:fs';
14
- import { resolve, sep } from 'node:path';
21
+ import { readFileSync, realpathSync } from 'node:fs';
22
+ import { dirname, join, resolve, sep } from 'node:path';
15
23
  import { pathToFileURL } from 'node:url';
16
- import { createDefaultRegistry } from '@sensigo/realm';
17
- import { computeExtensionIdentity, errorExtensionIdentityEntry } from './extension-identity.js';
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';
18
28
  /** Registration surfaces of a declarative extension module, keyed by export map name. */
19
29
  const EXTENSION_SURFACES = [
20
30
  { mapKey: 'adapters', type: 'adapter', probeMembers: ['fetch', 'create', 'update'] },
21
31
  { mapKey: 'handlers', type: 'handler', probeMembers: ['execute'] },
22
32
  { mapKey: 'processors', type: 'processor', probeMembers: ['process'] },
23
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
+ };
24
56
  const DEFAULT_REGISTRY_CACHE_KEY = '\u0000default-registry\u0000';
25
- // Process-lifetime cache keyed by the ORDERED list of resolved module paths (plus a sentinel
26
- // for the extension-less default registry). Restart-required semantics: module CONTENT changes
27
- // in long-lived processes (listen parent, serve, mcp) need a process restart — there is no
28
- // cache-busting re-import (ESM module cache would defeat it anyway).
29
57
  const cache = new Map();
30
58
  /** @internal Test-only: clears the process-lifetime registry cache. */
31
59
  export function clearProjectExtensionsCache() {
@@ -55,81 +83,337 @@ function normalizeDeclared(extensions) {
55
83
  return typeof extensions === 'string' ? [extensions] : extensions;
56
84
  }
57
85
  /**
58
- * Loads the project extension modules declared by a workflow definition (or an operator
59
- * override) and returns a registry (defaults + extensions applied) plus a manifest of what
60
- * was loaded. No `extensions` on the definition and no override `createDefaultRegistry()`
61
- * plus an empty manifest — the byte-identical fallback, cached process-wide.
62
- *
63
- * Precedence: defaults < legacy env-gated built-ins (applied by `realm agent`, see agent.ts)
64
- * < declared extensions < --extensions-module override.
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.
65
145
  *
66
146
  * CONTRACT: returned registries are shared process-lifetime cache entries. The only
67
- * permitted mutation is `getOrCreateRateLimiter` (serve bucket persistence, by design);
68
- * any tier composition on top requires `ExtensionRegistry.clone()`.
147
+ * permitted mutation is `getOrCreateRateLimiter` (serve bucket persistence, by design).
69
148
  */
70
149
  export async function loadProjectExtensions(definition, opts) {
150
+ const secretMode = opts?.secretMode ?? 'real';
71
151
  const moduleRefs = resolveModuleRefs(definition, opts?.overrideModule);
72
- if (moduleRefs === undefined) {
73
- // Extension-free fallback: byte-identical default registry, process-lifetime cached
74
- // (the sentinel keeps rate-limiter buckets stable across requests in serve/mcp too).
152
+ const manifestCtx = loadManifestContext(definition, opts?.projectDir);
153
+ if (moduleRefs === undefined && manifestCtx === undefined) {
154
+ // Extension-free, manifest-free fallback: process-stable default registry.
75
155
  let entry = cache.get(DEFAULT_REGISTRY_CACHE_KEY);
76
156
  if (entry === undefined) {
77
- entry = { registry: createDefaultRegistry(), manifest: emptyManifest() };
157
+ entry = { result: { registry: createDefaultRegistry(), manifest: emptyManifest() } };
78
158
  cache.set(DEFAULT_REGISTRY_CACHE_KEY, entry);
79
159
  }
80
- return entry;
160
+ return entry.result;
81
161
  }
82
- const cacheKey = moduleRefs.map((m) => m.resolved).join('\n');
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;
83
174
  const cached = cache.get(cacheKey);
84
- if (cached !== undefined)
85
- return cached;
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).
86
179
  const registry = createDefaultRegistry();
87
180
  const builtinNames = {
88
181
  adapter: new Set(registry.names('adapter')),
89
182
  handler: new Set(registry.names('handler')),
90
183
  processor: new Set(registry.names('processor')),
91
184
  };
92
- // `${type}:${name}` → declared path of the module that first claimed it.
93
185
  const claimedBy = new Map();
94
186
  const manifest = {
95
- modules: moduleRefs,
187
+ modules: moduleRefs ?? [],
96
188
  adapters: [],
97
189
  handlers: [],
98
190
  processors: [],
99
191
  };
100
192
  const moduleFormats = new Map();
101
- for (const ref of moduleRefs) {
102
- const { exported, format } = await importExtensionModule(ref);
193
+ for (const ref of moduleRefs ?? []) {
194
+ const { namespace, format } = await importModuleNamespace(ref.resolved, ref.declared);
103
195
  moduleFormats.set(ref.resolved, format);
104
- applyModule(exported, ref, registry, builtinNames, claimedBy, manifest);
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
+ }
105
219
  }
106
- // Drift evidence (issue #119): capture the identity of the loaded code ONCE per cache
107
- // entry, at module-LOAD time (what is actually in memory). Capture failure NEVER fails
108
- // loading an error entry is attached and logged instead. The extension-less sentinel
109
- // above gets NO identity: extension-free runs record nothing.
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.
110
224
  const overrideActive = opts?.overrideModule !== undefined;
111
225
  try {
112
- registry.setIdentity(computeExtensionIdentity(moduleRefs.map((ref) => ({
113
- declared: ref.declared,
114
- resolved: ref.resolved,
115
- format: moduleFormats.get(ref.resolved) ?? 'esm',
116
- })), {
226
+ registry.setIdentity(computeExtensionIdentity(identityModules, {
117
227
  ...(definition.trust_root !== undefined ? { trustRoot: definition.trust_root } : {}),
118
228
  ...(overrideActive ? { overrideActive: true } : {}),
229
+ ...(manifestCtx !== undefined
230
+ ? { manifest: { path: manifestCtx.path, content_hash: manifestCtx.contentHash } }
231
+ : {}),
232
+ secretNames,
119
233
  }));
120
234
  }
121
235
  catch (err) {
122
236
  console.error(`[realm] extension identity capture failed: ${errMsg(err)} — drift evidence will carry an error entry for this load.`);
123
237
  registry.setIdentity(errorExtensionIdentityEntry(`identity capture failed: ${errMsg(err)}`, overrideActive ? { overrideActive: true } : {}));
124
238
  }
125
- const result = { registry, manifest };
126
- cache.set(cacheKey, result);
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 } : {}) });
127
247
  return result;
128
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
+ }
129
413
  /**
130
- * Resolves the list of modules to load. Returns undefined for the extension-free fallback.
131
- * Enforces the trust model for declared modules: agent-origin refusal, re-register guidance
132
- * when resolution metadata is missing, and realpath containment within trust_root.
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.
133
417
  */
134
418
  function resolveModuleRefs(definition, overrideModule) {
135
419
  if (overrideModule !== undefined) {
@@ -168,60 +452,68 @@ function resolveModuleRefs(definition, overrideModule) {
168
452
  });
169
453
  }
170
454
  /**
171
- * Imports one extension module (jiti-bridged for TypeScript paths) and returns its default
172
- * export plus the load format (descriptive metadata for the drift-evidence identity record:
173
- * ts-jiti for TypeScript paths, cjs for .cjs / detected CJS-interop wrappers, esm otherwise).
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).
174
458
  */
175
- async function importExtensionModule(ref) {
176
- const isTypescript = /\.(ts|mts|cts)$/.test(ref.resolved);
177
- let mod;
459
+ async function importModuleNamespace(resolvedPath, label) {
460
+ const isTypescript = /\.(ts|mts|cts)$/.test(resolvedPath);
461
+ let namespace;
178
462
  if (isTypescript) {
179
- mod = await importViaJiti(ref);
463
+ namespace = await importViaJiti(resolvedPath, label);
180
464
  }
181
465
  else {
182
466
  try {
183
- mod = (await import(pathToFileURL(ref.resolved).href));
467
+ namespace = (await import(pathToFileURL(resolvedPath).href));
184
468
  }
185
469
  catch (err) {
186
- throw new Error(`Failed to import extension module '${ref.declared}' (${ref.resolved}): ${errMsg(err)}`);
470
+ throw new Error(`Failed to import module '${label}' (${resolvedPath}): ${errMsg(err)}`);
187
471
  }
188
472
  }
189
473
  let format = isTypescript
190
474
  ? 'ts-jiti'
191
- : ref.resolved.endsWith('.cjs')
475
+ : resolvedPath.endsWith('.cjs')
192
476
  ? 'cjs'
193
477
  : 'esm';
194
- let exported = mod['default'];
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'];
195
490
  // CJS-ESM interop: `import()` of a tsc-CommonJS build ("type" not "module") yields
196
491
  // namespace.default = module.exports = { __esModule: true, default: <manifest> }.
197
- // Unwrap that wrapper so CJS consumers get the same contract as ESM ones.
198
492
  if (typeof exported === 'object' &&
199
493
  exported !== null &&
200
494
  exported['__esModule'] === true &&
201
495
  'default' in exported) {
202
496
  exported = exported['default'];
203
- if (!isTypescript)
204
- format = 'cjs';
205
497
  }
206
498
  if (exported === undefined) {
207
- throw new Error(`Extension module '${ref.declared}' has no default export — export a declarative object: ` +
499
+ throw new Error(`Extension module '${declared}' has no default export — export a declarative object: ` +
208
500
  `export default { adapters: { name: instance }, handlers: { ... }, processors: { ... } }`);
209
501
  }
210
- return { exported, format };
502
+ return exported;
211
503
  }
212
504
  /**
213
505
  * TypeScript modules load through jiti resolved from the MODULE'S OWN directory (the consumer
214
506
  * project's node_modules) — never from the CLI install (npx/global installs carry no jiti).
215
507
  * Compiled JS is the documented default; jiti is a consumer-side optional peer.
216
508
  */
217
- async function importViaJiti(ref) {
218
- const requireFromModule = createRequire(ref.resolved);
509
+ async function importViaJiti(resolvedPath, label) {
510
+ const requireFromModule = createRequire(resolvedPath);
219
511
  let jitiPath;
220
512
  try {
221
513
  jitiPath = requireFromModule.resolve('jiti');
222
514
  }
223
515
  catch {
224
- throw new Error(`Extension module '${ref.declared}' (${ref.resolved}) is TypeScript, but 'jiti' is not ` +
516
+ throw new Error(`Module '${label}' (${resolvedPath}) is TypeScript, but 'jiti' is not ` +
225
517
  `installed in your project. Install jiti in your project (npm install --save-dev jiti), ` +
226
518
  `or compile the module to JS and declare the compiled path.`);
227
519
  }
@@ -236,14 +528,14 @@ async function importViaJiti(ref) {
236
528
  // jiti v2: createJiti(parentPath).import(path) → module namespace.
237
529
  const createJiti = jitiModule['createJiti'];
238
530
  if (typeof createJiti === 'function') {
239
- const jiti = createJiti(ref.resolved);
240
- return (await jiti.import(ref.resolved));
531
+ const jiti = createJiti(resolvedPath);
532
+ return (await jiti.import(resolvedPath));
241
533
  }
242
534
  // jiti v1: default export is a factory returning a require-like function.
243
535
  const factory = jitiModule['default'];
244
536
  if (typeof factory === 'function') {
245
- const jitiRequire = factory(ref.resolved, { interopDefault: false });
246
- const required = jitiRequire(ref.resolved);
537
+ const jitiRequire = factory(resolvedPath, { interopDefault: false });
538
+ const required = jitiRequire(resolvedPath);
247
539
  if (typeof required === 'object' && required !== null && 'default' in required) {
248
540
  return required;
249
541
  }
@@ -251,7 +543,7 @@ async function importViaJiti(ref) {
251
543
  }
252
544
  }
253
545
  catch (err) {
254
- throw new Error(`Failed to import TypeScript extension module '${ref.declared}' via jiti: ${errMsg(err)}`);
546
+ throw new Error(`Failed to import TypeScript module '${label}' via jiti: ${errMsg(err)}`);
255
547
  }
256
548
  throw new Error(`Unrecognized 'jiti' package shape at '${jitiPath}' — upgrade jiti in your project, or ` +
257
549
  `compile the extension module to JS.`);
@@ -349,10 +641,14 @@ function warnOnIdMismatch(impl, type, name, ref) {
349
641
  }
350
642
  /**
351
643
  * Builds a `registryProvider` for the MCP server (serve / mcp stdio): resolves each
352
- * definition's extensions through the process-lifetime loader cache. An operator override
353
- * replaces declared modules for EVERY definition served by this process.
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).
354
647
  */
355
- export function makeRegistryProvider(overrideModule) {
356
- return async (definition) => (await loadProjectExtensions(definition, overrideModule !== undefined ? { overrideModule } : {})).registry;
648
+ export function makeRegistryProvider(overrideModule, projectDir) {
649
+ return async (definition) => (await loadProjectExtensions(definition, {
650
+ ...(overrideModule !== undefined ? { overrideModule } : {}),
651
+ ...(projectDir !== undefined ? { projectDir } : {}),
652
+ })).registry;
357
653
  }
358
654
  //# sourceMappingURL=load-project-extensions.js.map