apcore-js 0.19.0 → 0.21.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/README.md +110 -9
- package/dist/acl.d.ts +18 -0
- package/dist/acl.d.ts.map +1 -1
- package/dist/acl.js +54 -17
- package/dist/acl.js.map +1 -1
- package/dist/async-task.d.ts +70 -16
- package/dist/async-task.d.ts.map +1 -1
- package/dist/async-task.js +212 -72
- package/dist/async-task.js.map +1 -1
- package/dist/builtin-steps.d.ts +16 -5
- package/dist/builtin-steps.d.ts.map +1 -1
- package/dist/builtin-steps.js +45 -28
- package/dist/builtin-steps.js.map +1 -1
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +163 -33
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +12 -1
- package/dist/context.js.map +1 -1
- package/dist/errors.d.ts +32 -10
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +55 -16
- package/dist/errors.js.map +1 -1
- package/dist/events/circuit-breaker.d.ts +45 -0
- package/dist/events/circuit-breaker.d.ts.map +1 -0
- package/dist/events/circuit-breaker.js +115 -0
- package/dist/events/circuit-breaker.js.map +1 -0
- package/dist/events/emitter.d.ts +22 -1
- package/dist/events/emitter.d.ts.map +1 -1
- package/dist/events/emitter.js +66 -2
- package/dist/events/emitter.js.map +1 -1
- package/dist/events/index.d.ts +4 -2
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +3 -2
- package/dist/events/index.js.map +1 -1
- package/dist/events/subscribers.d.ts +33 -1
- package/dist/events/subscribers.d.ts.map +1 -1
- package/dist/events/subscribers.js +124 -1
- package/dist/events/subscribers.js.map +1 -1
- package/dist/executor.d.ts +10 -2
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +173 -52
- package/dist/executor.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.d.ts +35 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -17
- package/dist/index.js.map +1 -1
- package/dist/middleware/base.d.ts +25 -3
- package/dist/middleware/base.d.ts.map +1 -1
- package/dist/middleware/base.js +24 -0
- package/dist/middleware/base.js.map +1 -1
- package/dist/middleware/circuit-breaker.d.ts +54 -0
- package/dist/middleware/circuit-breaker.d.ts.map +1 -0
- package/dist/middleware/circuit-breaker.js +168 -0
- package/dist/middleware/circuit-breaker.js.map +1 -0
- package/dist/middleware/context-namespace.d.ts +30 -0
- package/dist/middleware/context-namespace.d.ts.map +1 -0
- package/dist/middleware/context-namespace.js +38 -0
- package/dist/middleware/context-namespace.js.map +1 -0
- package/dist/middleware/index.d.ts +7 -1
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +4 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/manager.d.ts +11 -4
- package/dist/middleware/manager.d.ts.map +1 -1
- package/dist/middleware/manager.js +25 -9
- package/dist/middleware/manager.js.map +1 -1
- package/dist/middleware/platform-notify.d.ts +8 -4
- package/dist/middleware/platform-notify.d.ts.map +1 -1
- package/dist/middleware/platform-notify.js +11 -7
- package/dist/middleware/platform-notify.js.map +1 -1
- package/dist/middleware/tracing.d.ts +50 -0
- package/dist/middleware/tracing.d.ts.map +1 -0
- package/dist/middleware/tracing.js +89 -0
- package/dist/middleware/tracing.js.map +1 -0
- package/dist/module.d.ts +94 -2
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +99 -2
- package/dist/module.js.map +1 -1
- package/dist/observability/batch-span-processor.d.ts +48 -0
- package/dist/observability/batch-span-processor.d.ts.map +1 -0
- package/dist/observability/batch-span-processor.js +89 -0
- package/dist/observability/batch-span-processor.js.map +1 -0
- package/dist/observability/context-logger.d.ts +54 -1
- package/dist/observability/context-logger.d.ts.map +1 -1
- package/dist/observability/context-logger.js +270 -6
- package/dist/observability/context-logger.js.map +1 -1
- package/dist/observability/error-history.d.ts +36 -7
- package/dist/observability/error-history.d.ts.map +1 -1
- package/dist/observability/error-history.js +169 -50
- package/dist/observability/error-history.js.map +1 -1
- package/dist/observability/index.d.ts +16 -5
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +8 -3
- package/dist/observability/index.js.map +1 -1
- package/dist/observability/metrics.d.ts +14 -1
- package/dist/observability/metrics.d.ts.map +1 -1
- package/dist/observability/metrics.js +23 -2
- package/dist/observability/metrics.js.map +1 -1
- package/dist/observability/prometheus-exporter.d.ts +37 -0
- package/dist/observability/prometheus-exporter.d.ts.map +1 -0
- package/dist/observability/prometheus-exporter.js +135 -0
- package/dist/observability/prometheus-exporter.js.map +1 -0
- package/dist/observability/storage.d.ts +43 -0
- package/dist/observability/storage.d.ts.map +1 -0
- package/dist/observability/storage.js +58 -0
- package/dist/observability/storage.js.map +1 -0
- package/dist/observability/store.d.ts +29 -0
- package/dist/observability/store.d.ts.map +1 -0
- package/dist/observability/store.js +36 -0
- package/dist/observability/store.js.map +1 -0
- package/dist/observability/usage-exporter.d.ts +58 -0
- package/dist/observability/usage-exporter.d.ts.map +1 -0
- package/dist/observability/usage-exporter.js +86 -0
- package/dist/observability/usage-exporter.js.map +1 -0
- package/dist/observability/usage.d.ts +18 -1
- package/dist/observability/usage.d.ts.map +1 -1
- package/dist/observability/usage.js +25 -3
- package/dist/observability/usage.js.map +1 -1
- package/dist/pipeline-config.d.ts +11 -0
- package/dist/pipeline-config.d.ts.map +1 -1
- package/dist/pipeline-config.js +36 -10
- package/dist/pipeline-config.js.map +1 -1
- package/dist/pipeline.d.ts +123 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +249 -50
- package/dist/pipeline.js.map +1 -1
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -0
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/multi-class.d.ts +71 -0
- package/dist/registry/multi-class.d.ts.map +1 -0
- package/dist/registry/multi-class.js +120 -0
- package/dist/registry/multi-class.js.map +1 -0
- package/dist/registry/registry.d.ts +246 -5
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +485 -16
- package/dist/registry/registry.js.map +1 -1
- package/dist/schema/annotations.d.ts.map +1 -1
- package/dist/schema/annotations.js +1 -0
- package/dist/schema/annotations.js.map +1 -1
- package/dist/schema/constants.d.ts +9 -0
- package/dist/schema/constants.d.ts.map +1 -0
- package/dist/schema/constants.js +9 -0
- package/dist/schema/constants.js.map +1 -0
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/loader.d.ts +27 -3
- package/dist/schema/loader.d.ts.map +1 -1
- package/dist/schema/loader.js +137 -32
- package/dist/schema/loader.js.map +1 -1
- package/dist/schema/types.d.ts +4 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js.map +1 -1
- package/dist/schema/validator.d.ts +9 -0
- package/dist/schema/validator.d.ts.map +1 -1
- package/dist/schema/validator.js +153 -4
- package/dist/schema/validator.js.map +1 -1
- package/dist/sys-modules/audit.d.ts +50 -0
- package/dist/sys-modules/audit.d.ts.map +1 -0
- package/dist/sys-modules/audit.js +89 -0
- package/dist/sys-modules/audit.js.map +1 -0
- package/dist/sys-modules/control.d.ts +32 -4
- package/dist/sys-modules/control.d.ts.map +1 -1
- package/dist/sys-modules/control.js +196 -25
- package/dist/sys-modules/control.js.map +1 -1
- package/dist/sys-modules/index.d.ts +7 -2
- package/dist/sys-modules/index.d.ts.map +1 -1
- package/dist/sys-modules/index.js +3 -1
- package/dist/sys-modules/index.js.map +1 -1
- package/dist/sys-modules/overrides.d.ts +58 -0
- package/dist/sys-modules/overrides.d.ts.map +1 -0
- package/dist/sys-modules/overrides.js +106 -0
- package/dist/sys-modules/overrides.js.map +1 -0
- package/dist/sys-modules/registration.d.ts +17 -12
- package/dist/sys-modules/registration.d.ts.map +1 -1
- package/dist/sys-modules/registration.js +134 -23
- package/dist/sys-modules/registration.js.map +1 -1
- package/dist/sys-modules/toggle.d.ts +7 -2
- package/dist/sys-modules/toggle.d.ts.map +1 -1
- package/dist/sys-modules/toggle.js +61 -5
- package/dist/sys-modules/toggle.js.map +1 -1
- package/dist/trace-context.d.ts +47 -9
- package/dist/trace-context.d.ts.map +1 -1
- package/dist/trace-context.js +139 -16
- package/dist/trace-context.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { detectIdConflicts } from './conflicts.js';
|
|
|
7
7
|
import { resolveDependencies } from './dependencies.js';
|
|
8
8
|
import { resolveEntryPoint } from './entry-point.js';
|
|
9
9
|
import { mergeModuleMetadata, parseDependencies } from './metadata.js';
|
|
10
|
+
import { _discoverMultiClass } from './multi-class.js';
|
|
10
11
|
import { getSchema } from './schema-export.js';
|
|
11
12
|
import { toStrictSchema } from '../schema/strict.js';
|
|
12
13
|
import { deepCopy } from '../utils/index.js';
|
|
@@ -43,6 +44,35 @@ async function lazyScanMultiRoot(roots, maxDepth, followSymlinks) {
|
|
|
43
44
|
const { scanMultiRoot } = await import('./scanner.js');
|
|
44
45
|
return scanMultiRoot(roots, maxDepth, followSymlinks);
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* One-shot deprecation flag for the legacy 4-arg
|
|
49
|
+
* `Registry.discoverMultiClass(filePath, classes, extensionsRoot, multiClassEnabled)`
|
|
50
|
+
* call shape. Cleared per process so the warning fires at most once even
|
|
51
|
+
* across many invocations from the same caller. See apcore decision-log
|
|
52
|
+
* D-06 (apcore commit 973410b).
|
|
53
|
+
*
|
|
54
|
+
* @internal exported for tests so they can reset between cases.
|
|
55
|
+
*/
|
|
56
|
+
let _multiClassEnabledDeprecationWarned = false;
|
|
57
|
+
/**
|
|
58
|
+
* Reset the one-shot deprecation flag for the legacy 4-arg
|
|
59
|
+
* `Registry.discoverMultiClass` overload. Test-only — production callers
|
|
60
|
+
* never need this.
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
export function _resetMultiClassEnabledDeprecationWarned() {
|
|
65
|
+
_multiClassEnabledDeprecationWarned = false;
|
|
66
|
+
}
|
|
67
|
+
function warnMultiClassEnabledDeprecated() {
|
|
68
|
+
if (_multiClassEnabledDeprecationWarned)
|
|
69
|
+
return;
|
|
70
|
+
_multiClassEnabledDeprecationWarned = true;
|
|
71
|
+
console.warn('[apcore:registry] DEPRECATION: the `multiClassEnabled` argument to ' +
|
|
72
|
+
'`Registry.discoverMultiClass()` is deprecated and ignored under apcore ' +
|
|
73
|
+
'decision-log D-06. Mark each `ClassDescriptor` with `multiClass: true` ' +
|
|
74
|
+
'instead. The 4-arg overload will be removed in v0.22.0.');
|
|
75
|
+
}
|
|
46
76
|
/**
|
|
47
77
|
* Standard registry event names.
|
|
48
78
|
*/
|
|
@@ -68,6 +98,25 @@ export const MAX_MODULE_ID_LENGTH = 192;
|
|
|
68
98
|
* Reserved words that cannot appear as the first segment of a module ID.
|
|
69
99
|
*/
|
|
70
100
|
export const RESERVED_WORDS = new Set(['system', 'internal', 'core', 'apcore', 'plugin', 'schema', 'acl']);
|
|
101
|
+
/**
|
|
102
|
+
* Namespace prefix reserved for programmatically-registered modules
|
|
103
|
+
* synthesized at runtime (PROTOCOL_SPEC §2.5; RFC
|
|
104
|
+
* `apcore/docs/spec/rfc-ephemeral-modules.md`). IDs in this namespace MUST
|
|
105
|
+
* be registered through {@link Registry.register} only — the filesystem
|
|
106
|
+
* discoverer rejects matching IDs because the namespace has no
|
|
107
|
+
* directory-rooted source of truth, and {@link Registry.registerInternal}
|
|
108
|
+
* rejects them too so the audit-trail provenance distinction between
|
|
109
|
+
* framework-emitted (`system.*`) and caller-emitted (`ephemeral.*`)
|
|
110
|
+
* modules stays clean.
|
|
111
|
+
*
|
|
112
|
+
* The trailing dot is required so module IDs whose first segment merely
|
|
113
|
+
* *starts with* `ephemeral` (e.g. `ephemerals`) are not falsely classified.
|
|
114
|
+
*/
|
|
115
|
+
export const EPHEMERAL_NAMESPACE_PREFIX = 'ephemeral.';
|
|
116
|
+
/** Return true when `moduleId` belongs to the reserved `ephemeral.*` namespace. */
|
|
117
|
+
export function isEphemeralModuleId(moduleId) {
|
|
118
|
+
return moduleId === 'ephemeral' || moduleId.startsWith(EPHEMERAL_NAMESPACE_PREFIX);
|
|
119
|
+
}
|
|
71
120
|
/**
|
|
72
121
|
* Validate a module ID against PROTOCOL_SPEC §2.7 in canonical order.
|
|
73
122
|
*
|
|
@@ -129,6 +178,16 @@ export class Registry {
|
|
|
129
178
|
_refCounts = new Map();
|
|
130
179
|
_draining = new Set();
|
|
131
180
|
_drainResolvers = new Map();
|
|
181
|
+
/**
|
|
182
|
+
* Optional EventEmitter for the ephemeral.* registry-side audit emits
|
|
183
|
+
* (RFC `apcore/docs/spec/rfc-ephemeral-modules.md` "Audit-event single-emit
|
|
184
|
+
* rule"). Set via {@link setEventEmitter}; when null, ephemeral audit
|
|
185
|
+
* events are warn-logged so they never silently disappear.
|
|
186
|
+
*
|
|
187
|
+
* Mirrors apcore-python `Registry._event_emitter` /
|
|
188
|
+
* `Registry.set_event_emitter`.
|
|
189
|
+
*/
|
|
190
|
+
_eventEmitter = null;
|
|
132
191
|
constructor(options) {
|
|
133
192
|
const config = options?.config ?? null;
|
|
134
193
|
const extensionsDir = options?.extensionsDir ?? null;
|
|
@@ -197,6 +256,27 @@ export class Registry {
|
|
|
197
256
|
continue;
|
|
198
257
|
}
|
|
199
258
|
}
|
|
259
|
+
// PROTOCOL_SPEC §2.7 ID validation — sync finding A-D-102.
|
|
260
|
+
// Mirrors apcore-python `Registry._discover_custom` which calls
|
|
261
|
+
// `_validate_module_id` before registration. Invalid IDs are skipped
|
|
262
|
+
// with a warning rather than aborting the whole discover run.
|
|
263
|
+
try {
|
|
264
|
+
validateModuleId(moduleId, false);
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
console.warn(`[apcore:registry] Skipping custom-discovered module with invalid ID '${moduleId}': ${e.message}`);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// RFC `apcore/docs/spec/rfc-ephemeral-modules.md`: filesystem /
|
|
271
|
+
// discoverer paths MUST reject `ephemeral.*` IDs. The custom
|
|
272
|
+
// discoverer is functionally a discovery path too, so the same
|
|
273
|
+
// contract applies — ephemeral.* must land via Registry.register().
|
|
274
|
+
if (isEphemeralModuleId(moduleId)) {
|
|
275
|
+
console.warn(`[apcore:registry] Skipping custom-discovered module '${moduleId}': ` +
|
|
276
|
+
`'ephemeral.*' is reserved for programmatic registration via ` +
|
|
277
|
+
`Registry.register(); see docs/spec/rfc-ephemeral-modules.md.`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
200
280
|
try {
|
|
201
281
|
this._registerImpl(moduleId, mod);
|
|
202
282
|
count++;
|
|
@@ -207,15 +287,31 @@ export class Registry {
|
|
|
207
287
|
}
|
|
208
288
|
return count;
|
|
209
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Default discovery pipeline (D-32 — 8 canonical stages, mirroring
|
|
292
|
+
* apcore-rust `default_discoverer.rs`):
|
|
293
|
+
*
|
|
294
|
+
* 1. _ensureIdMap — lazy-load the optional id_map.json
|
|
295
|
+
* 2. _scanRoots — walk extension roots
|
|
296
|
+
* 3. _applyIdMapOverrides — rewrite canonical IDs from the map
|
|
297
|
+
* 4. _loadAllMetadata — load each module's `module.yaml`
|
|
298
|
+
* 5. _resolveAllEntryPoints — import the JS/TS entry point
|
|
299
|
+
* 6. _validateAll — run module/custom validators
|
|
300
|
+
* 7. _filterIdConflicts — batch-drop conflicting / invalid IDs
|
|
301
|
+
* 8. _resolveLoadOrder + _registerInOrder
|
|
302
|
+
* — topological sort then register.
|
|
303
|
+
*/
|
|
210
304
|
async _discoverDefault() {
|
|
211
305
|
await this._ensureIdMap();
|
|
212
306
|
const discovered = await this._scanRoots();
|
|
213
307
|
await this._applyIdMapOverrides(discovered);
|
|
308
|
+
this._rejectEphemeralDiscoveries(discovered);
|
|
214
309
|
const rawMetadata = await this._loadAllMetadata(discovered);
|
|
215
310
|
const resolvedModules = await this._resolveAllEntryPoints(discovered, rawMetadata);
|
|
216
311
|
const validModules = await this._validateAll(resolvedModules);
|
|
217
|
-
const
|
|
218
|
-
|
|
312
|
+
const filteredModules = this._filterIdConflicts(validModules, rawMetadata);
|
|
313
|
+
const loadOrder = this._resolveLoadOrder(filteredModules, rawMetadata);
|
|
314
|
+
return this._registerInOrder(loadOrder, filteredModules, rawMetadata);
|
|
219
315
|
}
|
|
220
316
|
async _scanRoots() {
|
|
221
317
|
let maxDepth = 8;
|
|
@@ -230,6 +326,31 @@ export class Registry {
|
|
|
230
326
|
}
|
|
231
327
|
return lazyScanExtensions(this._extensionRoots[0]['root'], maxDepth, followSymlinks);
|
|
232
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Reject filesystem-derived IDs that fall in the reserved `ephemeral.*`
|
|
331
|
+
* namespace.
|
|
332
|
+
*
|
|
333
|
+
* Per the apcore ephemeral-modules RFC pilot (`apcore/docs/spec/
|
|
334
|
+
* rfc-ephemeral-modules.md`), `ephemeral.*` is reserved for
|
|
335
|
+
* programmatically-registered modules synthesized at runtime. Any
|
|
336
|
+
* filesystem layout that produces such an ID is a configuration error —
|
|
337
|
+
* either the directory is misnamed or the namespace prefix is being
|
|
338
|
+
* misused. We surface this as an `InvalidInputError` so callers cannot
|
|
339
|
+
* accidentally pollute the registry with filesystem-rooted ephemerals.
|
|
340
|
+
*
|
|
341
|
+
* Mirrors apcore-python `Registry._reject_ephemeral_discoveries`.
|
|
342
|
+
*/
|
|
343
|
+
_rejectEphemeralDiscoveries(discovered) {
|
|
344
|
+
const offenders = discovered.filter((dm) => isEphemeralModuleId(dm.canonicalId));
|
|
345
|
+
if (offenders.length === 0)
|
|
346
|
+
return;
|
|
347
|
+
const ids = [...new Set(offenders.map((dm) => dm.canonicalId))].sort();
|
|
348
|
+
throw new InvalidInputError(`Filesystem discovery produced module ID(s) in the reserved ` +
|
|
349
|
+
`'ephemeral.*' namespace: ${JSON.stringify(ids)}. The ephemeral.* ` +
|
|
350
|
+
`namespace is reserved for programmatically-registered modules and ` +
|
|
351
|
+
`may only be used via Registry.register(). Rename the offending ` +
|
|
352
|
+
`directory or extension namespace.`);
|
|
353
|
+
}
|
|
233
354
|
async _applyIdMapOverrides(discovered) {
|
|
234
355
|
if (Object.keys(this._idMap).length === 0)
|
|
235
356
|
return;
|
|
@@ -295,6 +416,56 @@ export class Registry {
|
|
|
295
416
|
}
|
|
296
417
|
return validModules;
|
|
297
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Stage 7 (D-32) — batch-drop modules with invalid or conflicting IDs.
|
|
421
|
+
*
|
|
422
|
+
* Mirrors apcore-python `_filter_id_conflicts` and apcore-rust
|
|
423
|
+
* `default_discoverer::filter_id_conflicts`. Two failure modes drop a
|
|
424
|
+
* module here (warn + skip) rather than aborting the whole batch:
|
|
425
|
+
* - PROTOCOL_SPEC §2.7 ID validation (empty / pattern / length /
|
|
426
|
+
* reserved-word first segment), and
|
|
427
|
+
* - Algorithm A03 conflict detection (duplicate against an existing
|
|
428
|
+
* registration, lowercase collision, reserved-word collision).
|
|
429
|
+
*
|
|
430
|
+
* Soft-severity conflicts (e.g. case-insensitive match against an
|
|
431
|
+
* already-registered ID at `warn` level) are NOT dropped here — the
|
|
432
|
+
* warning is logged and the module flows through to registration so
|
|
433
|
+
* existing behaviour is preserved. Only `error`-severity conflicts and
|
|
434
|
+
* invalid IDs are filtered out.
|
|
435
|
+
*
|
|
436
|
+
* `rawMetadata` is accepted for cross-language signature parity with
|
|
437
|
+
* the Rust/Python helpers (which may inspect metadata for additional
|
|
438
|
+
* checks); the TS implementation reads only the module ID.
|
|
439
|
+
*/
|
|
440
|
+
_filterIdConflicts(validModules, _rawMetadata) {
|
|
441
|
+
const filtered = new Map();
|
|
442
|
+
// Track within-batch IDs (case-insensitive) so two newly-discovered
|
|
443
|
+
// modules whose IDs collide on lowercase don't both slip through.
|
|
444
|
+
const batchLowercase = new Map(this._lowercaseMap);
|
|
445
|
+
const batchIds = new Set(this._modules.keys());
|
|
446
|
+
for (const [modId, mod] of validModules.entries()) {
|
|
447
|
+
try {
|
|
448
|
+
validateModuleId(modId, false);
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
console.warn(`[apcore:registry] Skipping discovered module with invalid ID '${modId}': ${e.message}`);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const conflict = detectIdConflicts(modId, batchIds, RESERVED_WORDS, batchLowercase);
|
|
455
|
+
if (conflict !== null) {
|
|
456
|
+
if (conflict.severity === 'error') {
|
|
457
|
+
console.warn(`[apcore:registry] Skipping discovered module '${modId}' due to ID conflict: ${conflict.message}`);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
// Soft severity — log but keep the module.
|
|
461
|
+
console.warn(`[apcore:registry] ID conflict: ${conflict.message}`);
|
|
462
|
+
}
|
|
463
|
+
filtered.set(modId, mod);
|
|
464
|
+
batchIds.add(modId);
|
|
465
|
+
batchLowercase.set(modId.toLowerCase(), modId);
|
|
466
|
+
}
|
|
467
|
+
return filtered;
|
|
468
|
+
}
|
|
298
469
|
_resolveLoadOrder(validModules, rawMetadata) {
|
|
299
470
|
const modulesWithDeps = [];
|
|
300
471
|
const moduleVersions = new Map();
|
|
@@ -326,9 +497,13 @@ export class Registry {
|
|
|
326
497
|
return resolveDependencies(modulesWithDeps, knownIds, moduleVersions);
|
|
327
498
|
}
|
|
328
499
|
_registerInOrder(loadOrder, validModules, rawMetadata) {
|
|
500
|
+
// Stage 8 (D-32). Conflict detection / ID validation already happened in
|
|
501
|
+
// `_filterIdConflicts` (stage 7), so this loop is purely a register pass.
|
|
329
502
|
let count = 0;
|
|
330
503
|
for (const modId of loadOrder) {
|
|
331
504
|
const mod = validModules.get(modId);
|
|
505
|
+
if (mod === undefined)
|
|
506
|
+
continue;
|
|
332
507
|
const modObj = mod;
|
|
333
508
|
const mergedMeta = mergeModuleMetadata(modObj, rawMetadata.get(modId) ?? {});
|
|
334
509
|
this._modules.set(modId, mod);
|
|
@@ -357,9 +532,25 @@ export class Registry {
|
|
|
357
532
|
* Validation order (PROTOCOL_SPEC §2.7, aligned with apcore-python and
|
|
358
533
|
* apcore-rust): empty → pattern → length → reserved (per-segment) →
|
|
359
534
|
* duplicate.
|
|
535
|
+
*
|
|
536
|
+
* The optional `version` and `metadata` parameters mirror apcore-python's
|
|
537
|
+
* `Registry.register(module_id, module, version=None, metadata=None)` for
|
|
538
|
+
* cross-language signature parity (sync finding A-001). Multi-version
|
|
539
|
+
* coexistence is not yet implemented in this SDK — when supplied, both
|
|
540
|
+
* fields are merged into the module's metadata so callers can read them
|
|
541
|
+
* back via `getDefinition()` and `list({tags})`. See PROTOCOL_SPEC §5.4.
|
|
360
542
|
*/
|
|
361
|
-
register(moduleId, module) {
|
|
543
|
+
register(moduleId, module, version, metadata, options) {
|
|
362
544
|
validateModuleId(moduleId, false);
|
|
545
|
+
// ephemeral.* registrations only land via this programmatic path —
|
|
546
|
+
// the filesystem discoverer rejects matching IDs upstream (see
|
|
547
|
+
// `_rejectEphemeralDiscoveries`). When invoked here the RFC pilot
|
|
548
|
+
// recommends `requires_approval=true` so a human gates execution of
|
|
549
|
+
// agent-synthesized code; we soft-warn but never refuse.
|
|
550
|
+
const ephemeral = isEphemeralModuleId(moduleId);
|
|
551
|
+
if (ephemeral) {
|
|
552
|
+
this._warnIfMissingApproval(moduleId, module);
|
|
553
|
+
}
|
|
363
554
|
if (this._customValidator !== null) {
|
|
364
555
|
const result = this._customValidator.validate(module);
|
|
365
556
|
if (result instanceof Promise) {
|
|
@@ -369,10 +560,17 @@ export class Registry {
|
|
|
369
560
|
throw new InvalidInputError(`Custom validator rejected module '${moduleId}': ${result.join('; ')}`);
|
|
370
561
|
}
|
|
371
562
|
}
|
|
372
|
-
|
|
563
|
+
const overrides = { ...(metadata ?? {}) };
|
|
564
|
+
if (version !== undefined && version !== null) {
|
|
565
|
+
overrides['version'] = version;
|
|
566
|
+
}
|
|
567
|
+
this._registerImpl(moduleId, module, overrides);
|
|
568
|
+
if (ephemeral) {
|
|
569
|
+
this._emitEphemeralAudit('apcore.registry.module_registered', moduleId, options?.context ?? null);
|
|
570
|
+
}
|
|
373
571
|
}
|
|
374
572
|
/** Inner registration — no validator, no ID validation. Used by discover() paths that run their own checks. */
|
|
375
|
-
_registerImpl(moduleId, module) {
|
|
573
|
+
_registerImpl(moduleId, module, metadataOverrides = {}) {
|
|
376
574
|
// Algorithm A03: detect ID conflicts (exact duplicate, reserved word, case collision)
|
|
377
575
|
const conflict = detectIdConflicts(moduleId, new Set(this._modules.keys()), RESERVED_WORDS, this._lowercaseMap);
|
|
378
576
|
if (conflict !== null) {
|
|
@@ -385,9 +583,10 @@ export class Registry {
|
|
|
385
583
|
}
|
|
386
584
|
this._modules.set(moduleId, module);
|
|
387
585
|
this._lowercaseMap.set(moduleId.toLowerCase(), moduleId);
|
|
388
|
-
// Populate metadata from the module object
|
|
586
|
+
// Populate metadata from the module object, layering any explicit overrides
|
|
587
|
+
// (e.g. the `version` / `metadata` args passed to `register()`) on top.
|
|
389
588
|
const modObj = module;
|
|
390
|
-
this._moduleMeta.set(moduleId, mergeModuleMetadata(modObj,
|
|
589
|
+
this._moduleMeta.set(moduleId, mergeModuleMetadata(modObj, metadataOverrides));
|
|
391
590
|
// Call onLoad if available
|
|
392
591
|
if (typeof modObj['onLoad'] === 'function') {
|
|
393
592
|
try {
|
|
@@ -402,7 +601,7 @@ export class Registry {
|
|
|
402
601
|
}
|
|
403
602
|
this._triggerEvent(REGISTRY_EVENTS.REGISTER, moduleId, module);
|
|
404
603
|
}
|
|
405
|
-
unregister(moduleId) {
|
|
604
|
+
unregister(moduleId, options) {
|
|
406
605
|
if (!this._modules.has(moduleId))
|
|
407
606
|
return false;
|
|
408
607
|
const module = this._modules.get(moduleId);
|
|
@@ -421,9 +620,24 @@ export class Registry {
|
|
|
421
620
|
}
|
|
422
621
|
}
|
|
423
622
|
this._triggerEvent(REGISTRY_EVENTS.UNREGISTER, moduleId, module);
|
|
623
|
+
if (isEphemeralModuleId(moduleId)) {
|
|
624
|
+
this._emitEphemeralAudit('apcore.registry.module_unregistered', moduleId, options?.context ?? null);
|
|
625
|
+
}
|
|
424
626
|
return true;
|
|
425
627
|
}
|
|
426
|
-
|
|
628
|
+
/**
|
|
629
|
+
* Look up a registered module by ID.
|
|
630
|
+
*
|
|
631
|
+
* @param moduleId - Module identifier (must be non-empty).
|
|
632
|
+
* @param _versionHint - Optional semver range for multi-version coexistence
|
|
633
|
+
* (PROTOCOL_SPEC §5.4). This SDK currently exposes a single-version
|
|
634
|
+
* registry, so the hint is accepted for cross-language API parity with
|
|
635
|
+
* apcore-python (sync finding A-002) but does NOT participate in
|
|
636
|
+
* resolution: the latest registered module for `moduleId` is returned
|
|
637
|
+
* regardless of the hint. When multi-version registration lands, this
|
|
638
|
+
* parameter will gate semver-range matching.
|
|
639
|
+
*/
|
|
640
|
+
get(moduleId, _versionHint) {
|
|
427
641
|
if (moduleId === '') {
|
|
428
642
|
throw new ModuleNotFoundError('');
|
|
429
643
|
}
|
|
@@ -432,8 +646,21 @@ export class Registry {
|
|
|
432
646
|
has(moduleId) {
|
|
433
647
|
return this._modules.has(moduleId);
|
|
434
648
|
}
|
|
649
|
+
/**
|
|
650
|
+
* Return sorted list of registered module IDs, optionally filtered by
|
|
651
|
+
* `prefix` / `tags`.
|
|
652
|
+
*
|
|
653
|
+
* Modules whose `ModuleAnnotations.discoverable === false` are excluded
|
|
654
|
+
* by default per PROTOCOL_SPEC §4.4 (RFC
|
|
655
|
+
* `apcore/docs/spec/rfc-ephemeral-modules.md`). Pass
|
|
656
|
+
* `includeHidden: true` to enumerate every registered module — useful
|
|
657
|
+
* for introspection tools, debug consoles, and tests.
|
|
658
|
+
*/
|
|
435
659
|
list(options) {
|
|
436
660
|
let ids = [...this._modules.keys()];
|
|
661
|
+
if (options?.includeHidden !== true) {
|
|
662
|
+
ids = ids.filter((id) => this._isDiscoverable(id));
|
|
663
|
+
}
|
|
437
664
|
if (options?.prefix != null) {
|
|
438
665
|
ids = ids.filter((id) => id.startsWith(options.prefix));
|
|
439
666
|
}
|
|
@@ -456,16 +683,92 @@ export class Registry {
|
|
|
456
683
|
}
|
|
457
684
|
return ids.sort();
|
|
458
685
|
}
|
|
459
|
-
|
|
460
|
-
|
|
686
|
+
/**
|
|
687
|
+
* Iterate `[moduleId, module]` pairs. Hides modules annotated
|
|
688
|
+
* `discoverable: false` by default; pass `includeHidden: true` to walk
|
|
689
|
+
* every registered module.
|
|
690
|
+
*/
|
|
691
|
+
iter(options) {
|
|
692
|
+
if (options?.includeHidden === true) {
|
|
693
|
+
return this._modules.entries();
|
|
694
|
+
}
|
|
695
|
+
const visible = [];
|
|
696
|
+
for (const [id, mod] of this._modules.entries()) {
|
|
697
|
+
if (this._isDiscoverable(id))
|
|
698
|
+
visible.push([id, mod]);
|
|
699
|
+
}
|
|
700
|
+
return visible[Symbol.iterator]();
|
|
461
701
|
}
|
|
702
|
+
/** Total number of registered modules (includes `discoverable: false`). */
|
|
462
703
|
get count() {
|
|
463
704
|
return this._modules.size;
|
|
464
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Sorted list of registered module IDs (excludes
|
|
708
|
+
* `ModuleAnnotations.discoverable === false` modules). Use
|
|
709
|
+
* {@link list} with `includeHidden: true` when the full set is required.
|
|
710
|
+
*/
|
|
465
711
|
get moduleIds() {
|
|
466
|
-
return [...this._modules.keys()]
|
|
712
|
+
return [...this._modules.keys()]
|
|
713
|
+
.filter((id) => this._isDiscoverable(id))
|
|
714
|
+
.sort();
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Return false when the module's annotations declare
|
|
718
|
+
* `discoverable: false`. Anything else (including missing annotations)
|
|
719
|
+
* keeps the module visible — the default `true` preserves backward
|
|
720
|
+
* compatibility.
|
|
721
|
+
*
|
|
722
|
+
* Resolution order matches `mergeModuleMetadata`:
|
|
723
|
+
* 1. The merged `annotations` slot in `_moduleMeta` (which already
|
|
724
|
+
* accounts for YAML > code precedence).
|
|
725
|
+
* 2. The module instance's `annotations` attribute (covers paths that
|
|
726
|
+
* bypassed the merge, e.g. `registerInternal`).
|
|
727
|
+
*/
|
|
728
|
+
_isDiscoverable(moduleId) {
|
|
729
|
+
const meta = this._moduleMeta.get(moduleId);
|
|
730
|
+
if (meta != null) {
|
|
731
|
+
const ann = meta['annotations'];
|
|
732
|
+
if (ann != null) {
|
|
733
|
+
const dv = this._readDiscoverable(ann);
|
|
734
|
+
if (dv === false)
|
|
735
|
+
return false;
|
|
736
|
+
if (dv === true)
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const mod = this._modules.get(moduleId);
|
|
741
|
+
if (mod == null)
|
|
742
|
+
return true;
|
|
743
|
+
const ann = mod['annotations'];
|
|
744
|
+
if (ann == null)
|
|
745
|
+
return true;
|
|
746
|
+
return this._readDiscoverable(ann) !== false;
|
|
747
|
+
}
|
|
748
|
+
_readDiscoverable(ann) {
|
|
749
|
+
if (ann == null)
|
|
750
|
+
return undefined;
|
|
751
|
+
if (typeof ann === 'object') {
|
|
752
|
+
// Accept both ModuleAnnotations instances and YAML-style dicts.
|
|
753
|
+
const rec = ann;
|
|
754
|
+
const v = rec['discoverable'];
|
|
755
|
+
if (v === true || v === false)
|
|
756
|
+
return v;
|
|
757
|
+
}
|
|
758
|
+
return undefined;
|
|
467
759
|
}
|
|
468
|
-
getDefinition(moduleId) {
|
|
760
|
+
getDefinition(moduleId, _versionHint) {
|
|
761
|
+
// `_versionHint` accepted for cross-language API parity with apcore-python
|
|
762
|
+
// (sync finding A-002 / §5.4). Ignored under the single-version registry;
|
|
763
|
+
// see `get()` for the rationale.
|
|
764
|
+
//
|
|
765
|
+
// D10-011: spec registry-system.md:382 says any error that `get(module_id)`
|
|
766
|
+
// raises is propagated. The empty-string guard mirrors `get()` (line 669)
|
|
767
|
+
// so callers using getDefinition see the same ModuleNotFoundError as
|
|
768
|
+
// get(), matching apcore-python where getDefinition routes through get().
|
|
769
|
+
if (moduleId === '') {
|
|
770
|
+
throw new ModuleNotFoundError('');
|
|
771
|
+
}
|
|
469
772
|
const module = this._modules.get(moduleId);
|
|
470
773
|
if (module == null)
|
|
471
774
|
return null;
|
|
@@ -564,6 +867,29 @@ export class Registry {
|
|
|
564
867
|
}
|
|
565
868
|
}
|
|
566
869
|
}
|
|
870
|
+
/**
|
|
871
|
+
* Watch the configured extension roots for filesystem changes and
|
|
872
|
+
* unregister any module whose source file is modified or deleted.
|
|
873
|
+
*
|
|
874
|
+
* **Cross-language divergence (sync finding A-D-104):** unlike apcore-python
|
|
875
|
+
* (which re-imports the file via `importlib.reload`) and apcore-rust (which
|
|
876
|
+
* triggers full rediscovery), the TypeScript SDK is **event-only**. On a
|
|
877
|
+
* file change the registry:
|
|
878
|
+
* 1. unregisters the previously-loaded module (calling its `onUnload`),
|
|
879
|
+
* 2. emits a `'file_changed'` event with `{ filePath }` payload.
|
|
880
|
+
*
|
|
881
|
+
* Consumers are expected to subscribe and re-register the module
|
|
882
|
+
* themselves (e.g. by calling `registry.discover()` or registering a fresh
|
|
883
|
+
* import). ES module specifiers are immutable in Node — there is no
|
|
884
|
+
* portable "reload from disk" primitive — so a transparent dynamic
|
|
885
|
+
* `import()` would silently return the cached old module on every
|
|
886
|
+
* invocation. A workaround using a cache-busting query (`?v=Date.now()`)
|
|
887
|
+
* leaks the old module each reload and breaks browser bundlers, so it is
|
|
888
|
+
* intentionally **not** offered here.
|
|
889
|
+
*
|
|
890
|
+
* If your application needs Python-style hot-reload semantics, listen for
|
|
891
|
+
* `'file_changed'` and re-discover or re-import explicitly.
|
|
892
|
+
*/
|
|
567
893
|
async watch() {
|
|
568
894
|
if (this._watchers && this._watchers.length > 0) {
|
|
569
895
|
return; // Already watching
|
|
@@ -684,13 +1010,42 @@ export class Registry {
|
|
|
684
1010
|
* `Registry::register_internal`.
|
|
685
1011
|
*/
|
|
686
1012
|
registerInternal(moduleId, module) {
|
|
1013
|
+
// RFC `apcore/docs/spec/rfc-ephemeral-modules.md` "register_internal()
|
|
1014
|
+
// interaction": ephemeral.* IDs MUST be rejected here. Namespace →
|
|
1015
|
+
// registration-mechanism is a 1:1 mapping; mixing blurs the audit-trail
|
|
1016
|
+
// distinction between framework-emitted (`system.*`) and caller-emitted
|
|
1017
|
+
// (`ephemeral.*`) modules.
|
|
1018
|
+
if (isEphemeralModuleId(moduleId)) {
|
|
1019
|
+
throw new InvalidInputError(`ephemeral.* module IDs must be registered via Registry.register(), ` +
|
|
1020
|
+
`not registerInternal() (got: '${moduleId}'). See apcore ` +
|
|
1021
|
+
`docs/spec/rfc-ephemeral-modules.md "register_internal() ` +
|
|
1022
|
+
`interaction" for rationale.`);
|
|
1023
|
+
}
|
|
687
1024
|
validateModuleId(moduleId, true);
|
|
688
|
-
|
|
689
|
-
|
|
1025
|
+
// D11-007: route duplicate detection through detectIdConflicts (with an
|
|
1026
|
+
// empty reserved-words set so the bypass for system.* prefixes is
|
|
1027
|
+
// preserved). This restores the case-collision branch present in
|
|
1028
|
+
// apcore-python (registry.py:1674) and apcore-rust (registry.rs:727).
|
|
1029
|
+
// The lowercase-only EBNF in validateModuleId makes case collisions
|
|
1030
|
+
// unreachable today, but the contract surface stays aligned across SDKs.
|
|
1031
|
+
const conflict = detectIdConflicts(moduleId, new Set(this._modules.keys()), new Set(), this._lowercaseMap);
|
|
1032
|
+
if (conflict !== null) {
|
|
1033
|
+
if (conflict.severity === 'error') {
|
|
1034
|
+
throw new InvalidInputError(conflict.message);
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
console.warn(`[apcore:registry] ID conflict: ${conflict.message}`);
|
|
1038
|
+
}
|
|
690
1039
|
}
|
|
691
1040
|
this._modules.set(moduleId, module);
|
|
692
1041
|
const modObj = module;
|
|
693
1042
|
this._moduleMeta.set(moduleId, mergeModuleMetadata(modObj, {}));
|
|
1043
|
+
// Mirror apcore-python register_internal and apcore-rust register_core:
|
|
1044
|
+
// every registration site (including sys/internal) populates the lowercase
|
|
1045
|
+
// index. The lowercase-only EBNF pattern enforced by validateModuleId makes
|
|
1046
|
+
// case collisions unreachable today, but keeping _lowercaseMap consistent
|
|
1047
|
+
// with _modules preserves the invariant for downstream conflict detection.
|
|
1048
|
+
this._lowercaseMap.set(moduleId.toLowerCase(), moduleId);
|
|
694
1049
|
if (typeof modObj['onLoad'] === 'function') {
|
|
695
1050
|
try {
|
|
696
1051
|
modObj['onLoad']();
|
|
@@ -698,6 +1053,7 @@ export class Registry {
|
|
|
698
1053
|
catch (e) {
|
|
699
1054
|
this._modules.delete(moduleId);
|
|
700
1055
|
this._moduleMeta.delete(moduleId);
|
|
1056
|
+
this._lowercaseMap.delete(moduleId.toLowerCase());
|
|
701
1057
|
throw e;
|
|
702
1058
|
}
|
|
703
1059
|
}
|
|
@@ -727,7 +1083,16 @@ export class Registry {
|
|
|
727
1083
|
clearCache() {
|
|
728
1084
|
this._schemaCache.clear();
|
|
729
1085
|
}
|
|
730
|
-
|
|
1086
|
+
discoverMultiClass(filePath, classes, extensionsRoot = 'extensions', multiClassEnabled) {
|
|
1087
|
+
if (multiClassEnabled !== undefined) {
|
|
1088
|
+
warnMultiClassEnabledDeprecated();
|
|
1089
|
+
// The argument is functionally inert under D-06: the per-class
|
|
1090
|
+
// `multiClass` field is the source of truth. We intentionally ignore
|
|
1091
|
+
// `multiClassEnabled` and recompute from the descriptors below.
|
|
1092
|
+
}
|
|
1093
|
+
const enabled = classes.some((c) => c.implementsModule && c.multiClass === true);
|
|
1094
|
+
return _discoverMultiClass(filePath, classes, extensionsRoot, enabled);
|
|
1095
|
+
}
|
|
731
1096
|
/**
|
|
732
1097
|
* Number of in-flight executions per module.
|
|
733
1098
|
*/
|
|
@@ -841,6 +1206,110 @@ export class Registry {
|
|
|
841
1206
|
*
|
|
842
1207
|
* Returns true if drained cleanly, false if force-unloaded after timeout.
|
|
843
1208
|
*/
|
|
1209
|
+
// ── Ephemeral namespace pilot (RFC: rfc-ephemeral-modules) ──────
|
|
1210
|
+
/**
|
|
1211
|
+
* Wire an EventEmitter for registry-side audit emits.
|
|
1212
|
+
*
|
|
1213
|
+
* Per the apcore RFC `apcore/docs/spec/rfc-ephemeral-modules.md`
|
|
1214
|
+
* "Audit-event single-emit rule", `ephemeral.*` registrations and
|
|
1215
|
+
* unregistrations emit a single `apcore.registry.module_registered` /
|
|
1216
|
+
* `apcore.registry.module_unregistered` event with the D-35 contextual
|
|
1217
|
+
* payload. When no emitter is wired, the event is logged at INFO so it
|
|
1218
|
+
* never silently disappears — useful for the v1 pilot where most
|
|
1219
|
+
* callers will not have an emitter attached.
|
|
1220
|
+
*
|
|
1221
|
+
* Pilot scope: only `ephemeral.*` registrations trigger registry-side
|
|
1222
|
+
* emits. The pre-existing bridge in
|
|
1223
|
+
* `sys-modules/registration.ts` continues to emit empty-payload events
|
|
1224
|
+
* for non-ephemeral modules and short-circuits for ephemerals so the
|
|
1225
|
+
* single-emit rule holds.
|
|
1226
|
+
*/
|
|
1227
|
+
setEventEmitter(emitter) {
|
|
1228
|
+
this._eventEmitter = emitter;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Soft-warn when an `ephemeral.*` module is registered without
|
|
1232
|
+
* `requiresApproval: true`. Per the ephemeral-modules RFC pilot,
|
|
1233
|
+
* agent-synthesized modules SHOULD declare `requiresApproval` so a
|
|
1234
|
+
* human gates execution. The registry only warns; it does not refuse
|
|
1235
|
+
* the registration.
|
|
1236
|
+
*/
|
|
1237
|
+
_warnIfMissingApproval(moduleId, module) {
|
|
1238
|
+
const ann = module?.annotations;
|
|
1239
|
+
let requiresApproval = false;
|
|
1240
|
+
if (ann != null && typeof ann === 'object') {
|
|
1241
|
+
const rec = ann;
|
|
1242
|
+
requiresApproval = rec['requiresApproval'] === true || rec['requires_approval'] === true;
|
|
1243
|
+
}
|
|
1244
|
+
if (!requiresApproval) {
|
|
1245
|
+
console.warn(`[apcore:registry] ephemeral.* module '${moduleId}' registered without ` +
|
|
1246
|
+
`requiresApproval=true. The apcore RFC docs/spec/rfc-ephemeral-modules.md ` +
|
|
1247
|
+
`recommends setting ModuleAnnotations.requiresApproval=true so ` +
|
|
1248
|
+
`agent-synthesized code does not run unattended.`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Build the D-35 audit payload (`caller_id` plus optional `identity`
|
|
1253
|
+
* snapshot) for an ephemeral.* register/unregister. Mirrors the shape
|
|
1254
|
+
* `extractAuditIdentity` produces for `system.control.*` events so
|
|
1255
|
+
* subscribers can apply identical redaction rules.
|
|
1256
|
+
*/
|
|
1257
|
+
_buildEphemeralAuditPayload(context) {
|
|
1258
|
+
const callerIdRaw = context?.callerId;
|
|
1259
|
+
const callerId = callerIdRaw == null || callerIdRaw === '' ? '@external' : callerIdRaw;
|
|
1260
|
+
const ident = context?.identity ?? null;
|
|
1261
|
+
if (!ident) {
|
|
1262
|
+
return { caller_id: callerId, identity: null, namespace_class: 'ephemeral' };
|
|
1263
|
+
}
|
|
1264
|
+
const snapshot = {
|
|
1265
|
+
id: ident.id,
|
|
1266
|
+
type: ident.type,
|
|
1267
|
+
roles: [...ident.roles],
|
|
1268
|
+
};
|
|
1269
|
+
const displayName = ident.attrs['display_name'];
|
|
1270
|
+
if (typeof displayName === 'string' && displayName.length > 0) {
|
|
1271
|
+
snapshot['display_name'] = displayName;
|
|
1272
|
+
}
|
|
1273
|
+
return { caller_id: callerId, identity: snapshot, namespace_class: 'ephemeral' };
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Emit the canonical ephemeral.* audit event to the wired EventEmitter
|
|
1277
|
+
* (or warn-log if none is wired). The bridge in
|
|
1278
|
+
* `sys-modules/registration.ts` short-circuits on ephemeral.* IDs so
|
|
1279
|
+
* exactly one event lands per registration / unregistration.
|
|
1280
|
+
*/
|
|
1281
|
+
_emitEphemeralAudit(eventType, moduleId, context) {
|
|
1282
|
+
let payload;
|
|
1283
|
+
try {
|
|
1284
|
+
payload = this._buildEphemeralAuditPayload(context);
|
|
1285
|
+
}
|
|
1286
|
+
catch (e) {
|
|
1287
|
+
console.warn(`[apcore:registry] Failed to extract audit payload for ephemeral '${moduleId}': ${e.message}. ` +
|
|
1288
|
+
`Falling back to caller_id='@external'.`);
|
|
1289
|
+
payload = { caller_id: '@external', identity: null, namespace_class: 'ephemeral' };
|
|
1290
|
+
}
|
|
1291
|
+
const emitter = this._eventEmitter;
|
|
1292
|
+
if (emitter === null) {
|
|
1293
|
+
console.info(`[apcore:registry] ephemeral audit event ${eventType} module_id=${moduleId} ` +
|
|
1294
|
+
`payload=${JSON.stringify(payload)} (no EventEmitter wired; call ` +
|
|
1295
|
+
`Registry.setEventEmitter to capture)`);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const event = {
|
|
1299
|
+
eventType,
|
|
1300
|
+
moduleId,
|
|
1301
|
+
timestamp: new Date().toISOString(),
|
|
1302
|
+
severity: 'info',
|
|
1303
|
+
data: payload,
|
|
1304
|
+
};
|
|
1305
|
+
try {
|
|
1306
|
+
emitter.emit(event);
|
|
1307
|
+
}
|
|
1308
|
+
catch (e) {
|
|
1309
|
+
console.error(`[apcore:registry] EventEmitter.emit failed for ephemeral audit event ` +
|
|
1310
|
+
`${eventType} on '${moduleId}':`, e);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
844
1313
|
async safeUnregister(moduleId, timeoutMs = 5000) {
|
|
845
1314
|
if (!this._modules.has(moduleId))
|
|
846
1315
|
return false;
|