@sensigo/realm-cli 0.13.0 → 0.14.1
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 +18 -5
- 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 +53 -14
- package/dist/extensions/load-project-extensions.d.ts.map +1 -1
- package/dist/extensions/load-project-extensions.js +445 -80
- 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, existsSync } 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, WorkflowError, 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,406 @@ function normalizeDeclared(extensions) {
|
|
|
55
83
|
return typeof extensions === 'string' ? [extensions] : extensions;
|
|
56
84
|
}
|
|
57
85
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
86
|
+
* Orphaned-manifest guard (#123): throws when a `realm.yaml` sits anywhere in the span
|
|
87
|
+
* `[sourceDir, trustRoot)` — from the workflow directory walking UP to, but NOT including,
|
|
88
|
+
* the trust root. Such a manifest is silently ignored by the strict-single-location loader,
|
|
89
|
+
* so surfacing it loudly turns a confusing downstream `adapter 'X' not registered` (or total
|
|
90
|
+
* silence under validate/sentinel/fixture paths) into an actionable placement error.
|
|
62
91
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
92
|
+
* Path-keyed (takes the two directories, NOT a definition) so callers that don't stamp a
|
|
93
|
+
* definition — `realm workflow validate` on an extension-free workflow — can reuse it with a
|
|
94
|
+
* `findTrustRoot(workflowDir)` result. Callers guarantee both paths are known: the loader
|
|
95
|
+
* calls it only when a definition carries both source_dir and trust_root (agent-origin /
|
|
96
|
+
* from-string definitions have no source_dir → never scanned). A workflow dir that is its
|
|
97
|
+
* own trust root has an empty span (no-op). Never scans at or above trust_root — a stray at
|
|
98
|
+
* trust_root IS the loaded manifest; Case C (above trust_root) is deliberately out of #123.
|
|
99
|
+
*/
|
|
100
|
+
export function checkForOrphanedManifests(sourceDir, trustRoot) {
|
|
101
|
+
if (sourceDir === trustRoot)
|
|
102
|
+
return; // empty span
|
|
103
|
+
const orphans = [];
|
|
104
|
+
let current = sourceDir;
|
|
105
|
+
// Walk [source_dir, trust_root): stop BEFORE trust_root; guard against a source_dir that
|
|
106
|
+
// is not actually under trust_root (never terminates otherwise).
|
|
107
|
+
while (current !== trustRoot) {
|
|
108
|
+
const candidate = join(current, 'realm.yaml');
|
|
109
|
+
if (existsSync(candidate))
|
|
110
|
+
orphans.push(candidate);
|
|
111
|
+
const parent = dirname(current);
|
|
112
|
+
if (parent === current)
|
|
113
|
+
break; // hit the filesystem root without meeting trust_root
|
|
114
|
+
current = parent;
|
|
115
|
+
}
|
|
116
|
+
if (orphans.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
const found = orphans.length === 1
|
|
119
|
+
? `Deployment manifest at '${orphans[0]}'`
|
|
120
|
+
: `Deployment manifests at ${orphans.map((o) => `'${o}'`).join(', ')}`;
|
|
121
|
+
const nearest = orphans[0];
|
|
122
|
+
// Throw WorkflowError (VALIDATION class — the same class the yaml-loader raises for
|
|
123
|
+
// structural validation failures) so `realm workflow validate`'s existing
|
|
124
|
+
// `if (err instanceof WorkflowError)` handler renders it as `Invalid:` rather than a stack
|
|
125
|
+
// trace, and every execution/registration path treats it as a load failure.
|
|
126
|
+
throw new WorkflowError(`${found} will NOT be loaded — manifests are read only from the deployment root ` +
|
|
127
|
+
`'${join(trustRoot, 'realm.yaml')}'. Move it to '${join(trustRoot, 'realm.yaml')}' ` +
|
|
128
|
+
`(the nearest package.json/.git ancestor of the workflow), or add a package.json/.git ` +
|
|
129
|
+
`at '${dirname(nearest)}' to make that the deployment root.`, {
|
|
130
|
+
code: 'VALIDATION_WORKFLOW_SCHEMA',
|
|
131
|
+
category: 'VALIDATION',
|
|
132
|
+
agentAction: 'report_to_user',
|
|
133
|
+
retryable: false,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolves the deployment root and loads+validates the manifest document when present.
|
|
138
|
+
* Definitions WITH trust_root anchor there; definitions WITHOUT it anchor at the daemon's
|
|
139
|
+
* `--project` root (absent → no manifest, defaults only — NO walking, NO error).
|
|
140
|
+
*/
|
|
141
|
+
function loadManifestContext(definition, projectDir) {
|
|
142
|
+
const root = definition.trust_root ?? projectDir;
|
|
143
|
+
if (root === undefined)
|
|
144
|
+
return undefined;
|
|
145
|
+
// Orphaned-manifest guard (#123): manifests load ONLY from <trust_root>/realm.yaml.
|
|
146
|
+
// A realm.yaml placed anywhere in the span [source_dir, trust_root) — the workflow dir
|
|
147
|
+
// or an intermediate dir below the resolved trust_root — is silently ignored, which is
|
|
148
|
+
// the common misplacement (workflow in a repo subdir whose package.json/.git is an
|
|
149
|
+
// ancestor). Detect it and fail loud BEFORE loading, consistent with the invalid-YAML /
|
|
150
|
+
// schema-error throws below. Runs before the trust_root load: a correctly-placed
|
|
151
|
+
// manifest does NOT excuse a stray one below it (the stray is still ignored, still
|
|
152
|
+
// operator error). Detection-only — LOADING semantics stay strict-single-location.
|
|
153
|
+
// Case C (realm.yaml ABOVE trust_root) is deliberately NOT scanned (see #123).
|
|
154
|
+
//
|
|
155
|
+
// Scan only when the definition carries both source_dir and trust_root: agent-origin /
|
|
156
|
+
// from-string definitions have no source_dir (and reach the manifest via `--project`),
|
|
157
|
+
// and `root === trust_root` here whenever trust_root is defined (root = trust_root ??
|
|
158
|
+
// projectDir), so passing definition.trust_root preserves the previous trust_root===root
|
|
159
|
+
// gate — the `--project`-fallback path never scans.
|
|
160
|
+
if (definition.source_dir !== undefined && definition.trust_root !== undefined) {
|
|
161
|
+
checkForOrphanedManifests(definition.source_dir, definition.trust_root);
|
|
162
|
+
}
|
|
163
|
+
const manifestPath = join(root, 'realm.yaml');
|
|
164
|
+
let bytes;
|
|
165
|
+
try {
|
|
166
|
+
bytes = readFileSync(manifestPath);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return undefined; // absent manifest = defaults only, valid for config-free projects
|
|
170
|
+
}
|
|
171
|
+
let raw;
|
|
172
|
+
try {
|
|
173
|
+
raw = loadYaml(bytes.toString('utf8'));
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
throw new Error(`Deployment manifest '${manifestPath}' is not valid YAML: ${errMsg(err)}`);
|
|
177
|
+
}
|
|
178
|
+
const errors = validateDeploymentManifest(raw);
|
|
179
|
+
if (errors.length > 0) {
|
|
180
|
+
throw new Error(`Deployment manifest '${manifestPath}' is invalid:\n ${errors.join('\n ')}`);
|
|
181
|
+
}
|
|
182
|
+
const document = raw;
|
|
183
|
+
// dotenv bytes participate in the freshness hash ONLY (rotation detection) — never in
|
|
184
|
+
// drift evidence.
|
|
185
|
+
const dotenvPath = join(dirname(manifestPath), document.secrets?.dotenv ?? '.env');
|
|
186
|
+
let dotenvBytes;
|
|
187
|
+
try {
|
|
188
|
+
dotenvBytes = readFileSync(dotenvPath);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
dotenvBytes = Buffer.alloc(0);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
document,
|
|
195
|
+
path: manifestPath,
|
|
196
|
+
realPath: realpathOrFail(manifestPath, `deployment manifest '${manifestPath}'`),
|
|
197
|
+
dir: dirname(manifestPath),
|
|
198
|
+
rootReal: realpathOrFail(root, `deployment root '${root}'`),
|
|
199
|
+
bytes,
|
|
200
|
+
contentHash: sha256HexOf(bytes),
|
|
201
|
+
dotenvBytes,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/** `use:` disambiguation — contains `/` or a module file extension → module ref. */
|
|
205
|
+
function isModuleRef(use) {
|
|
206
|
+
const beforeHash = use.split('#')[0];
|
|
207
|
+
return beforeHash.includes('/') || /\.(js|mjs|cjs|ts|mts|cts)$/.test(beforeHash);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Loads the project extensions and deployment configuration for a definition:
|
|
211
|
+
* workflow-declared CODE modules + the deployment manifest's constructed instances,
|
|
212
|
+
* one registry, one namespace, one collision map. No extensions, no manifest → the
|
|
213
|
+
* process-stable filesystem-only default registry.
|
|
65
214
|
*
|
|
66
215
|
* 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()`.
|
|
216
|
+
* permitted mutation is `getOrCreateRateLimiter` (serve bucket persistence, by design).
|
|
69
217
|
*/
|
|
70
218
|
export async function loadProjectExtensions(definition, opts) {
|
|
219
|
+
const secretMode = opts?.secretMode ?? 'real';
|
|
71
220
|
const moduleRefs = resolveModuleRefs(definition, opts?.overrideModule);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
221
|
+
const manifestCtx = loadManifestContext(definition, opts?.projectDir);
|
|
222
|
+
if (moduleRefs === undefined && manifestCtx === undefined) {
|
|
223
|
+
// Extension-free, manifest-free fallback: process-stable default registry.
|
|
75
224
|
let entry = cache.get(DEFAULT_REGISTRY_CACHE_KEY);
|
|
76
225
|
if (entry === undefined) {
|
|
77
|
-
entry = { registry: createDefaultRegistry(), manifest: emptyManifest() };
|
|
226
|
+
entry = { result: { registry: createDefaultRegistry(), manifest: emptyManifest() } };
|
|
78
227
|
cache.set(DEFAULT_REGISTRY_CACHE_KEY, entry);
|
|
79
228
|
}
|
|
80
|
-
return entry;
|
|
229
|
+
return entry.result;
|
|
81
230
|
}
|
|
82
|
-
|
|
231
|
+
// Resolve manifest `use:` module refs up front — they are part of the cache key.
|
|
232
|
+
const useModuleRefs = manifestCtx !== undefined ? resolveUseModuleRefs(manifestCtx) : new Map();
|
|
233
|
+
const cacheKey = [
|
|
234
|
+
`mode:${secretMode}`,
|
|
235
|
+
`manifest:${manifestCtx?.realPath ?? '\u0000none'}`,
|
|
236
|
+
`root:${manifestCtx?.rootReal ?? '\u0000none'}`,
|
|
237
|
+
...(moduleRefs ?? []).map((m) => `ext:${m.resolved}`),
|
|
238
|
+
...[...useModuleRefs.values()].sort().map((p) => `use:${p}`),
|
|
239
|
+
].join('\n');
|
|
240
|
+
const freshness = manifestCtx !== undefined
|
|
241
|
+
? sha256HexOf(Buffer.concat([manifestCtx.bytes, Buffer.from('\u0000'), manifestCtx.dotenvBytes]))
|
|
242
|
+
: undefined;
|
|
83
243
|
const cached = cache.get(cacheKey);
|
|
84
|
-
if (cached !== undefined)
|
|
85
|
-
return cached;
|
|
244
|
+
if (cached !== undefined && cached.freshness === freshness)
|
|
245
|
+
return cached.result;
|
|
246
|
+
// freshness mismatch → rebuild-and-REPLACE (dotenv rotation / manifest edit reaches
|
|
247
|
+
// the next run without restart; rate-limiter buckets reset with the new registry).
|
|
86
248
|
const registry = createDefaultRegistry();
|
|
87
249
|
const builtinNames = {
|
|
88
250
|
adapter: new Set(registry.names('adapter')),
|
|
89
251
|
handler: new Set(registry.names('handler')),
|
|
90
252
|
processor: new Set(registry.names('processor')),
|
|
91
253
|
};
|
|
92
|
-
// `${type}:${name}` → declared path of the module that first claimed it.
|
|
93
254
|
const claimedBy = new Map();
|
|
94
255
|
const manifest = {
|
|
95
|
-
modules: moduleRefs,
|
|
256
|
+
modules: moduleRefs ?? [],
|
|
96
257
|
adapters: [],
|
|
97
258
|
handlers: [],
|
|
98
259
|
processors: [],
|
|
99
260
|
};
|
|
100
261
|
const moduleFormats = new Map();
|
|
101
|
-
for (const ref of moduleRefs) {
|
|
102
|
-
const {
|
|
262
|
+
for (const ref of moduleRefs ?? []) {
|
|
263
|
+
const { namespace, format } = await importModuleNamespace(ref.resolved, ref.declared);
|
|
103
264
|
moduleFormats.set(ref.resolved, format);
|
|
104
|
-
applyModule(
|
|
265
|
+
applyModule(defaultExportOf(namespace, ref.declared), ref, registry, builtinNames, claimedBy, manifest);
|
|
266
|
+
}
|
|
267
|
+
const sentinelWarnings = [];
|
|
268
|
+
let notifiers;
|
|
269
|
+
let secretValues;
|
|
270
|
+
const identityModules = (moduleRefs ?? []).map((ref) => ({
|
|
271
|
+
declared: ref.declared,
|
|
272
|
+
resolved: ref.resolved,
|
|
273
|
+
format: moduleFormats.get(ref.resolved) ?? 'esm',
|
|
274
|
+
}));
|
|
275
|
+
let secretNames = [];
|
|
276
|
+
if (manifestCtx !== undefined) {
|
|
277
|
+
const applied = await applyDeploymentManifest(manifestCtx, useModuleRefs, registry, builtinNames, claimedBy, manifest, secretMode, sentinelWarnings);
|
|
278
|
+
notifiers = applied.notifiers;
|
|
279
|
+
secretNames = applied.secretNames;
|
|
280
|
+
secretValues = applied.secretValues;
|
|
281
|
+
for (const [use, real] of useModuleRefs) {
|
|
282
|
+
identityModules.push({
|
|
283
|
+
declared: use,
|
|
284
|
+
resolved: real,
|
|
285
|
+
format: moduleFormats.get(real) ?? applied.useFormats.get(real) ?? 'esm',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
105
288
|
}
|
|
106
|
-
// Drift evidence
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
289
|
+
// Drift evidence: identity captured ONCE per cache entry at module-LOAD time. The
|
|
290
|
+
// manifest contributes its content hash (never a sweep root); `use:`-resolved module
|
|
291
|
+
// FILES contribute sweep roots exactly like extension-list modules; secret NAMES are
|
|
292
|
+
// recorded, never compared; the freshness hash is NEVER recorded.
|
|
110
293
|
const overrideActive = opts?.overrideModule !== undefined;
|
|
111
294
|
try {
|
|
112
|
-
registry.setIdentity(computeExtensionIdentity(
|
|
113
|
-
declared: ref.declared,
|
|
114
|
-
resolved: ref.resolved,
|
|
115
|
-
format: moduleFormats.get(ref.resolved) ?? 'esm',
|
|
116
|
-
})), {
|
|
295
|
+
registry.setIdentity(computeExtensionIdentity(identityModules, {
|
|
117
296
|
...(definition.trust_root !== undefined ? { trustRoot: definition.trust_root } : {}),
|
|
118
297
|
...(overrideActive ? { overrideActive: true } : {}),
|
|
298
|
+
...(manifestCtx !== undefined
|
|
299
|
+
? { manifest: { path: manifestCtx.path, content_hash: manifestCtx.contentHash } }
|
|
300
|
+
: {}),
|
|
301
|
+
secretNames,
|
|
119
302
|
}));
|
|
120
303
|
}
|
|
121
304
|
catch (err) {
|
|
122
305
|
console.error(`[realm] extension identity capture failed: ${errMsg(err)} — drift evidence will carry an error entry for this load.`);
|
|
123
306
|
registry.setIdentity(errorExtensionIdentityEntry(`identity capture failed: ${errMsg(err)}`, overrideActive ? { overrideActive: true } : {}));
|
|
124
307
|
}
|
|
125
|
-
const result = {
|
|
126
|
-
|
|
308
|
+
const result = {
|
|
309
|
+
registry,
|
|
310
|
+
manifest,
|
|
311
|
+
...(notifiers !== undefined ? { notifiers } : {}),
|
|
312
|
+
...(sentinelWarnings.length > 0 ? { sentinelWarnings } : {}),
|
|
313
|
+
...(secretValues !== undefined && secretValues.length > 0 ? { secretValues } : {}),
|
|
314
|
+
};
|
|
315
|
+
cache.set(cacheKey, { result, ...(freshness !== undefined ? { freshness } : {}) });
|
|
127
316
|
return result;
|
|
128
317
|
}
|
|
318
|
+
/** Resolves every manifest `use:` module ref (realpathed + containment-checked). */
|
|
319
|
+
function resolveUseModuleRefs(ctx) {
|
|
320
|
+
const resolvedRefs = new Map();
|
|
321
|
+
const sections = [
|
|
322
|
+
ctx.document.adapters,
|
|
323
|
+
ctx.document.handlers,
|
|
324
|
+
ctx.document.processors,
|
|
325
|
+
];
|
|
326
|
+
for (const section of sections) {
|
|
327
|
+
for (const entry of Object.values(section ?? {})) {
|
|
328
|
+
if (entry.use === undefined || !isModuleRef(entry.use))
|
|
329
|
+
continue;
|
|
330
|
+
const modulePath = entry.use.split('#')[0];
|
|
331
|
+
if (resolvedRefs.has(entry.use))
|
|
332
|
+
continue;
|
|
333
|
+
const resolved = resolve(ctx.dir, modulePath);
|
|
334
|
+
const real = realpathOrFail(resolved, `manifest module '${modulePath}' (from '${ctx.path}', resolved: ${resolved})`);
|
|
335
|
+
if (!isContainedIn(real, ctx.rootReal)) {
|
|
336
|
+
throw new Error(`Deployment manifest module '${modulePath}' resolves to '${real}', which is OUTSIDE ` +
|
|
337
|
+
`the deployment root '${ctx.rootReal}'. Manifest 'use:' modules must live within ` +
|
|
338
|
+
`the deployment root — move the module inside it, or fix the reference in '${ctx.path}'.`);
|
|
339
|
+
}
|
|
340
|
+
resolvedRefs.set(entry.use, real);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return resolvedRefs;
|
|
344
|
+
}
|
|
345
|
+
/** Parses/constructs/registers everything the manifest declares. */
|
|
346
|
+
async function applyDeploymentManifest(ctx, useModuleRefs, registry, builtinNames, claimedBy, manifest, secretMode, sentinelWarnings) {
|
|
347
|
+
const refs = collectManifestSecretRefs(ctx.document);
|
|
348
|
+
const resolved = resolveManifestSecrets(refs, ctx.document.secrets, ctx.dir, secretMode);
|
|
349
|
+
const resolveName = (name) => resolved.values[name] ?? '';
|
|
350
|
+
const secretValues = Object.values(resolved.values);
|
|
351
|
+
const useFormats = new Map();
|
|
352
|
+
const secretBearingHandlers = [];
|
|
353
|
+
const secretBearingProcessors = [];
|
|
354
|
+
const sections = [
|
|
355
|
+
{ key: 'adapters', type: 'adapter' },
|
|
356
|
+
{ key: 'handlers', type: 'handler' },
|
|
357
|
+
{ key: 'processors', type: 'processor' },
|
|
358
|
+
];
|
|
359
|
+
for (const section of sections) {
|
|
360
|
+
for (const [name, entry] of Object.entries(ctx.document[section.key] ?? {})) {
|
|
361
|
+
const site = `${section.key}.${name}`;
|
|
362
|
+
if (entry.use === undefined) {
|
|
363
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} needs 'use:' — a catalog name ` +
|
|
364
|
+
`(adapters) or a module reference './path.js#Export'.`);
|
|
365
|
+
}
|
|
366
|
+
const factory = await resolveEntryFactory(ctx, useModuleRefs, useFormats, section.type, name, entry, site);
|
|
367
|
+
const hasRefs = findSecretRefSites(entry.config, site).sites.length > 0;
|
|
368
|
+
if (section.type === 'handler' && hasRefs)
|
|
369
|
+
secretBearingHandlers.push(name);
|
|
370
|
+
if (section.type === 'processor' && hasRefs)
|
|
371
|
+
secretBearingProcessors.push(name);
|
|
372
|
+
const config = interpolateConfigTree(entry.config ?? {}, resolveName);
|
|
373
|
+
let instance;
|
|
374
|
+
try {
|
|
375
|
+
instance = factory({ id: name, config });
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
const redacted = redactSecretValues(`Deployment manifest '${ctx.path}': constructing ${site} (use: ${entry.use}) failed: ${errMsg(err)}`, secretValues);
|
|
379
|
+
if (secretMode === 'sentinel') {
|
|
380
|
+
// Sentinel degrade: warn-and-skip — never a hard failure, never silent.
|
|
381
|
+
sentinelWarnings.push(`${redacted} (sentinel mode — entry skipped)`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
throw new Error(redacted);
|
|
385
|
+
}
|
|
386
|
+
probeShape(instance, section.type, PROBE_MEMBERS[section.type], name, {
|
|
387
|
+
declared: `${ctx.path}#${site}`,
|
|
388
|
+
resolved: ctx.realPath,
|
|
389
|
+
});
|
|
390
|
+
const claimKey = `${section.type}:${name}`;
|
|
391
|
+
const previousClaim = claimedBy.get(claimKey);
|
|
392
|
+
if (previousClaim !== undefined) {
|
|
393
|
+
throw new Error(`Extension ${section.type} '${name}' is declared by both '${previousClaim}' and the ` +
|
|
394
|
+
`deployment manifest '${ctx.path}' — manifest entries and code-module exports ` +
|
|
395
|
+
`share one namespace; names must be unique.`);
|
|
396
|
+
}
|
|
397
|
+
claimedBy.set(claimKey, `manifest ${ctx.path}`);
|
|
398
|
+
if (builtinNames[section.type].has(name)) {
|
|
399
|
+
console.warn(`[realm] manifest ${section.type} '${name}' overrides the built-in ${section.type} '${name}'.`);
|
|
400
|
+
}
|
|
401
|
+
registry.register(section.type, name, instance);
|
|
402
|
+
manifest[section.key].push(name);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (secretBearingHandlers.length > 0)
|
|
406
|
+
manifest.secret_bearing_handlers = secretBearingHandlers;
|
|
407
|
+
if (secretBearingProcessors.length > 0)
|
|
408
|
+
manifest.secret_bearing_processors = secretBearingProcessors;
|
|
409
|
+
let notifiers;
|
|
410
|
+
const slackGate = ctx.document.notifiers?.slack_gate;
|
|
411
|
+
if (slackGate !== undefined) {
|
|
412
|
+
notifiers = {
|
|
413
|
+
slack_gate: interpolateConfigTree(slackGate.config, resolveName),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
...(notifiers !== undefined ? { notifiers } : {}),
|
|
418
|
+
secretNames: [...new Set(refs.map((r) => r.name))].sort(),
|
|
419
|
+
// Values-only, frozen, real-mode only: the agent redaction feed. Never persisted.
|
|
420
|
+
secretValues: resolved.sentinel
|
|
421
|
+
? Object.freeze([])
|
|
422
|
+
: Object.freeze([...new Set(Object.values(resolved.values))].filter((v) => v.length >= 4)),
|
|
423
|
+
useFormats,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/** Resolves an entry's factory: catalog name (adapters only) or module ref `#Export`. */
|
|
427
|
+
async function resolveEntryFactory(ctx, useModuleRefs, useFormats, type, name, entry, site) {
|
|
428
|
+
const use = entry.use;
|
|
429
|
+
if (!isModuleRef(use)) {
|
|
430
|
+
if (type !== 'adapter') {
|
|
431
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} — there is no built-in catalog for ` +
|
|
432
|
+
`${type}s; use a module reference './path.js#ExportName'.`);
|
|
433
|
+
}
|
|
434
|
+
const catalogEntry = BUILTIN_ADAPTER_CATALOG[use];
|
|
435
|
+
if (catalogEntry === undefined) {
|
|
436
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} — unknown catalog adapter '${use}'. ` +
|
|
437
|
+
`Valid catalog names (realm v${VERSION}): ${Object.keys(BUILTIN_ADAPTER_CATALOG).join(', ')}. ` +
|
|
438
|
+
`For a custom adapter use a module reference './path.js#ExportName'.`);
|
|
439
|
+
}
|
|
440
|
+
if (catalogEntry.configless === true && entry.config !== undefined) {
|
|
441
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} — catalog adapter '${use}' takes no ` +
|
|
442
|
+
`config; remove the 'config:' block.`);
|
|
443
|
+
}
|
|
444
|
+
return catalogEntry.factory;
|
|
445
|
+
}
|
|
446
|
+
const [modulePath, exportName] = use.split('#');
|
|
447
|
+
const real = useModuleRefs.get(use) ?? useModuleRefs.get(modulePath);
|
|
448
|
+
if (real === undefined) {
|
|
449
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} — unresolved module '${use}'.`);
|
|
450
|
+
}
|
|
451
|
+
const { namespace, format } = await importModuleNamespace(real, `${ctx.path}#${site}`);
|
|
452
|
+
useFormats.set(real, format);
|
|
453
|
+
const candidate = pickExport(namespace, exportName, use, ctx.path, site);
|
|
454
|
+
if (typeof candidate !== 'function') {
|
|
455
|
+
throw new Error(`Deployment manifest '${ctx.path}': ${site} — '${use}' must export a FACTORY ` +
|
|
456
|
+
`(ctx: { id, config }) => instance; got ${typeof candidate}.`);
|
|
457
|
+
}
|
|
458
|
+
return candidate;
|
|
459
|
+
}
|
|
460
|
+
/** Picks the named (or default) export, naming the available exports on a miss. */
|
|
461
|
+
function pickExport(namespace, exportName, use, manifestPath, site) {
|
|
462
|
+
const unwrapped = unwrapCjsNamespace(namespace);
|
|
463
|
+
const wanted = exportName ?? 'default';
|
|
464
|
+
const candidate = unwrapped[wanted];
|
|
465
|
+
if (candidate !== undefined)
|
|
466
|
+
return candidate;
|
|
467
|
+
const available = Object.keys(unwrapped).filter((k) => k !== '__esModule');
|
|
468
|
+
throw new Error(`Deployment manifest '${manifestPath}': ${site} — module '${use}' has no ` +
|
|
469
|
+
`${exportName !== undefined ? `export '${exportName}'` : 'default export'}. ` +
|
|
470
|
+
`Available exports: ${available.length > 0 ? available.join(', ') : '(none)'}.`);
|
|
471
|
+
}
|
|
472
|
+
/** Flattens the tsc-CJS `{ default: { __esModule, ...exports } }` interop wrapper. */
|
|
473
|
+
function unwrapCjsNamespace(namespace) {
|
|
474
|
+
const def = namespace['default'];
|
|
475
|
+
if (typeof def === 'object' &&
|
|
476
|
+
def !== null &&
|
|
477
|
+
def['__esModule'] === true) {
|
|
478
|
+
return { ...namespace, ...def };
|
|
479
|
+
}
|
|
480
|
+
return namespace;
|
|
481
|
+
}
|
|
129
482
|
/**
|
|
130
|
-
* Resolves the list of
|
|
131
|
-
* Enforces the trust model
|
|
132
|
-
*
|
|
483
|
+
* Resolves the list of extension CODE modules. Returns undefined when none are declared.
|
|
484
|
+
* Enforces the trust model: agent-origin refusal, re-register guidance when resolution
|
|
485
|
+
* metadata is missing, and realpath containment within trust_root.
|
|
133
486
|
*/
|
|
134
487
|
function resolveModuleRefs(definition, overrideModule) {
|
|
135
488
|
if (overrideModule !== undefined) {
|
|
@@ -168,60 +521,68 @@ function resolveModuleRefs(definition, overrideModule) {
|
|
|
168
521
|
});
|
|
169
522
|
}
|
|
170
523
|
/**
|
|
171
|
-
* Imports one
|
|
172
|
-
*
|
|
173
|
-
*
|
|
524
|
+
* Imports one module (jiti-bridged for TypeScript paths) and returns its full namespace
|
|
525
|
+
* plus the load format (ts-jiti for TypeScript paths, cjs for .cjs / detected CJS-interop
|
|
526
|
+
* wrappers, esm otherwise).
|
|
174
527
|
*/
|
|
175
|
-
async function
|
|
176
|
-
const isTypescript = /\.(ts|mts|cts)$/.test(
|
|
177
|
-
let
|
|
528
|
+
async function importModuleNamespace(resolvedPath, label) {
|
|
529
|
+
const isTypescript = /\.(ts|mts|cts)$/.test(resolvedPath);
|
|
530
|
+
let namespace;
|
|
178
531
|
if (isTypescript) {
|
|
179
|
-
|
|
532
|
+
namespace = await importViaJiti(resolvedPath, label);
|
|
180
533
|
}
|
|
181
534
|
else {
|
|
182
535
|
try {
|
|
183
|
-
|
|
536
|
+
namespace = (await import(pathToFileURL(resolvedPath).href));
|
|
184
537
|
}
|
|
185
538
|
catch (err) {
|
|
186
|
-
throw new Error(`Failed to import
|
|
539
|
+
throw new Error(`Failed to import module '${label}' (${resolvedPath}): ${errMsg(err)}`);
|
|
187
540
|
}
|
|
188
541
|
}
|
|
189
542
|
let format = isTypescript
|
|
190
543
|
? 'ts-jiti'
|
|
191
|
-
:
|
|
544
|
+
: resolvedPath.endsWith('.cjs')
|
|
192
545
|
? 'cjs'
|
|
193
546
|
: 'esm';
|
|
194
|
-
|
|
547
|
+
const def = namespace['default'];
|
|
548
|
+
if (!isTypescript &&
|
|
549
|
+
typeof def === 'object' &&
|
|
550
|
+
def !== null &&
|
|
551
|
+
def['__esModule'] === true) {
|
|
552
|
+
format = 'cjs';
|
|
553
|
+
}
|
|
554
|
+
return { namespace, format };
|
|
555
|
+
}
|
|
556
|
+
/** The workflow-extensions default-export contract: unwraps tsc-CJS interop, requires default. */
|
|
557
|
+
function defaultExportOf(namespace, declared) {
|
|
558
|
+
let exported = namespace['default'];
|
|
195
559
|
// CJS-ESM interop: `import()` of a tsc-CommonJS build ("type" not "module") yields
|
|
196
560
|
// namespace.default = module.exports = { __esModule: true, default: <manifest> }.
|
|
197
|
-
// Unwrap that wrapper so CJS consumers get the same contract as ESM ones.
|
|
198
561
|
if (typeof exported === 'object' &&
|
|
199
562
|
exported !== null &&
|
|
200
563
|
exported['__esModule'] === true &&
|
|
201
564
|
'default' in exported) {
|
|
202
565
|
exported = exported['default'];
|
|
203
|
-
if (!isTypescript)
|
|
204
|
-
format = 'cjs';
|
|
205
566
|
}
|
|
206
567
|
if (exported === undefined) {
|
|
207
|
-
throw new Error(`Extension module '${
|
|
568
|
+
throw new Error(`Extension module '${declared}' has no default export — export a declarative object: ` +
|
|
208
569
|
`export default { adapters: { name: instance }, handlers: { ... }, processors: { ... } }`);
|
|
209
570
|
}
|
|
210
|
-
return
|
|
571
|
+
return exported;
|
|
211
572
|
}
|
|
212
573
|
/**
|
|
213
574
|
* TypeScript modules load through jiti resolved from the MODULE'S OWN directory (the consumer
|
|
214
575
|
* project's node_modules) — never from the CLI install (npx/global installs carry no jiti).
|
|
215
576
|
* Compiled JS is the documented default; jiti is a consumer-side optional peer.
|
|
216
577
|
*/
|
|
217
|
-
async function importViaJiti(
|
|
218
|
-
const requireFromModule = createRequire(
|
|
578
|
+
async function importViaJiti(resolvedPath, label) {
|
|
579
|
+
const requireFromModule = createRequire(resolvedPath);
|
|
219
580
|
let jitiPath;
|
|
220
581
|
try {
|
|
221
582
|
jitiPath = requireFromModule.resolve('jiti');
|
|
222
583
|
}
|
|
223
584
|
catch {
|
|
224
|
-
throw new Error(`
|
|
585
|
+
throw new Error(`Module '${label}' (${resolvedPath}) is TypeScript, but 'jiti' is not ` +
|
|
225
586
|
`installed in your project. Install jiti in your project (npm install --save-dev jiti), ` +
|
|
226
587
|
`or compile the module to JS and declare the compiled path.`);
|
|
227
588
|
}
|
|
@@ -236,14 +597,14 @@ async function importViaJiti(ref) {
|
|
|
236
597
|
// jiti v2: createJiti(parentPath).import(path) → module namespace.
|
|
237
598
|
const createJiti = jitiModule['createJiti'];
|
|
238
599
|
if (typeof createJiti === 'function') {
|
|
239
|
-
const jiti = createJiti(
|
|
240
|
-
return (await jiti.import(
|
|
600
|
+
const jiti = createJiti(resolvedPath);
|
|
601
|
+
return (await jiti.import(resolvedPath));
|
|
241
602
|
}
|
|
242
603
|
// jiti v1: default export is a factory returning a require-like function.
|
|
243
604
|
const factory = jitiModule['default'];
|
|
244
605
|
if (typeof factory === 'function') {
|
|
245
|
-
const jitiRequire = factory(
|
|
246
|
-
const required = jitiRequire(
|
|
606
|
+
const jitiRequire = factory(resolvedPath, { interopDefault: false });
|
|
607
|
+
const required = jitiRequire(resolvedPath);
|
|
247
608
|
if (typeof required === 'object' && required !== null && 'default' in required) {
|
|
248
609
|
return required;
|
|
249
610
|
}
|
|
@@ -251,7 +612,7 @@ async function importViaJiti(ref) {
|
|
|
251
612
|
}
|
|
252
613
|
}
|
|
253
614
|
catch (err) {
|
|
254
|
-
throw new Error(`Failed to import TypeScript
|
|
615
|
+
throw new Error(`Failed to import TypeScript module '${label}' via jiti: ${errMsg(err)}`);
|
|
255
616
|
}
|
|
256
617
|
throw new Error(`Unrecognized 'jiti' package shape at '${jitiPath}' — upgrade jiti in your project, or ` +
|
|
257
618
|
`compile the extension module to JS.`);
|
|
@@ -349,10 +710,14 @@ function warnOnIdMismatch(impl, type, name, ref) {
|
|
|
349
710
|
}
|
|
350
711
|
/**
|
|
351
712
|
* Builds a `registryProvider` for the MCP server (serve / mcp stdio): resolves each
|
|
352
|
-
* definition
|
|
353
|
-
*
|
|
713
|
+
* definition through the process-lifetime project loader. `--extensions-module` overrides
|
|
714
|
+
* CODE modules; `projectDir` anchors the manifest for definitions without trust_root
|
|
715
|
+
* (`realm mcp` passes it ONLY when --project was given — no cwd default there).
|
|
354
716
|
*/
|
|
355
|
-
export function makeRegistryProvider(overrideModule) {
|
|
356
|
-
return async (definition) => (await loadProjectExtensions(definition,
|
|
717
|
+
export function makeRegistryProvider(overrideModule, projectDir) {
|
|
718
|
+
return async (definition) => (await loadProjectExtensions(definition, {
|
|
719
|
+
...(overrideModule !== undefined ? { overrideModule } : {}),
|
|
720
|
+
...(projectDir !== undefined ? { projectDir } : {}),
|
|
721
|
+
})).registry;
|
|
357
722
|
}
|
|
358
723
|
//# sourceMappingURL=load-project-extensions.js.map
|