@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.
- 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 +5 -6
- package/dist/agent/run-attach.d.ts.map +1 -1
- package/dist/agent/run-attach.js +7 -3
- package/dist/agent/run-attach.js.map +1 -1
- package/dist/commands/agent.d.ts +12 -11
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +64 -105
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +55 -29
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +11 -0
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +6 -2
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +21 -6
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +8 -2
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +3 -2
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +15 -4
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +6 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/extensions/extension-identity.d.ts +19 -0
- package/dist/extensions/extension-identity.d.ts.map +1 -1
- package/dist/extensions/extension-identity.js +24 -0
- package/dist/extensions/extension-identity.js.map +1 -1
- package/dist/extensions/load-project-extensions.d.ts +38 -15
- package/dist/extensions/load-project-extensions.d.ts.map +1 -1
- package/dist/extensions/load-project-extensions.js +377 -81
- package/dist/extensions/load-project-extensions.js.map +1 -1
- 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
|
@@ -1,31 +1,59 @@
|
|
|
1
|
-
// load-project-extensions.ts — the ONE loader
|
|
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,
|
|
4
|
-
// test, validate, register, watch) resolves
|
|
5
|
-
// stores PATHS only
|
|
6
|
-
//
|
|
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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 {
|
|
17
|
-
import {
|
|
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
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
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(
|
|
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 = {
|
|
126
|
-
|
|
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
|
|
131
|
-
* Enforces the trust model
|
|
132
|
-
*
|
|
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
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
|
176
|
-
const isTypescript = /\.(ts|mts|cts)$/.test(
|
|
177
|
-
let
|
|
459
|
+
async function importModuleNamespace(resolvedPath, label) {
|
|
460
|
+
const isTypescript = /\.(ts|mts|cts)$/.test(resolvedPath);
|
|
461
|
+
let namespace;
|
|
178
462
|
if (isTypescript) {
|
|
179
|
-
|
|
463
|
+
namespace = await importViaJiti(resolvedPath, label);
|
|
180
464
|
}
|
|
181
465
|
else {
|
|
182
466
|
try {
|
|
183
|
-
|
|
467
|
+
namespace = (await import(pathToFileURL(resolvedPath).href));
|
|
184
468
|
}
|
|
185
469
|
catch (err) {
|
|
186
|
-
throw new Error(`Failed to import
|
|
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
|
-
:
|
|
475
|
+
: resolvedPath.endsWith('.cjs')
|
|
192
476
|
? 'cjs'
|
|
193
477
|
: 'esm';
|
|
194
|
-
|
|
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 '${
|
|
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
|
|
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(
|
|
218
|
-
const requireFromModule = createRequire(
|
|
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(`
|
|
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(
|
|
240
|
-
return (await jiti.import(
|
|
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(
|
|
246
|
-
const required = jitiRequire(
|
|
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
|
|
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
|
|
353
|
-
*
|
|
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,
|
|
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
|