@telorun/kernel 0.2.4 → 0.2.6
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/base-definition.d.ts +14 -0
- package/dist/base-definition.d.ts.map +1 -0
- package/dist/base-definition.js +17 -0
- package/dist/base-definition.js.map +1 -0
- package/dist/boot-context-registry.d.ts.map +1 -1
- package/dist/boot-context-registry.js +6 -6
- package/dist/boot-context-registry.js.map +1 -1
- package/dist/capabilities/capabilities/component.yaml +2 -1
- package/dist/capabilities/capabilities/executable.yaml +2 -1
- package/dist/capabilities/capabilities/handler.yaml +2 -1
- package/dist/capabilities/capabilities/listener.yaml +2 -1
- package/dist/capabilities/capabilities/provider.yaml +2 -1
- package/dist/capabilities/capabilities/template.yaml +2 -1
- package/dist/capabilities/capabilities/type.yaml +2 -1
- package/dist/capabilities/component.d.ts +3 -0
- package/dist/capabilities/component.d.ts.map +1 -0
- package/dist/capabilities/component.js +4 -0
- package/dist/capabilities/component.js.map +1 -0
- package/dist/capabilities/component.yaml +1 -1
- package/dist/capabilities/executable.d.ts +3 -0
- package/dist/capabilities/executable.d.ts.map +1 -0
- package/dist/capabilities/executable.js +5 -0
- package/dist/capabilities/executable.js.map +1 -0
- package/dist/capabilities/executable.yaml +1 -1
- package/dist/capabilities/handler.d.ts +3 -0
- package/dist/capabilities/handler.d.ts.map +1 -0
- package/dist/capabilities/handler.js +4 -0
- package/dist/capabilities/handler.js.map +1 -0
- package/dist/capabilities/handler.yaml +1 -1
- package/dist/capabilities/invokable.d.ts +3 -0
- package/dist/capabilities/invokable.d.ts.map +1 -0
- package/dist/capabilities/invokable.js +5 -0
- package/dist/capabilities/invokable.js.map +1 -0
- package/dist/capabilities/listener.d.ts +3 -0
- package/dist/capabilities/listener.d.ts.map +1 -0
- package/dist/capabilities/listener.js +5 -0
- package/dist/capabilities/listener.js.map +1 -0
- package/dist/capabilities/listener.yaml +1 -1
- package/dist/capabilities/mount.d.ts +3 -0
- package/dist/capabilities/mount.d.ts.map +1 -0
- package/dist/capabilities/mount.js +5 -0
- package/dist/capabilities/mount.js.map +1 -0
- package/dist/capabilities/provider.d.ts +3 -0
- package/dist/capabilities/provider.d.ts.map +1 -0
- package/dist/capabilities/provider.js +8 -0
- package/dist/capabilities/provider.js.map +1 -0
- package/dist/capabilities/provider.yaml +1 -1
- package/dist/capabilities/runnable.d.ts +3 -0
- package/dist/capabilities/runnable.d.ts.map +1 -0
- package/dist/capabilities/runnable.js +5 -0
- package/dist/capabilities/runnable.js.map +1 -0
- package/dist/capabilities/service.d.ts +3 -0
- package/dist/capabilities/service.d.ts.map +1 -0
- package/dist/capabilities/service.js +5 -0
- package/dist/capabilities/service.js.map +1 -0
- package/dist/capabilities/template.d.ts +3 -0
- package/dist/capabilities/template.d.ts.map +1 -0
- package/dist/capabilities/template.js +5 -0
- package/dist/capabilities/template.js.map +1 -0
- package/dist/capabilities/template.yaml +1 -1
- package/dist/capabilities/type.d.ts +3 -0
- package/dist/capabilities/type.d.ts.map +1 -0
- package/dist/capabilities/type.js +5 -0
- package/dist/capabilities/type.js.map +1 -0
- package/dist/capabilities/type.yaml +1 -1
- package/dist/controller-loader.d.ts +1 -1
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +6 -3
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-registry.d.ts +1 -6
- package/dist/controller-registry.d.ts.map +1 -1
- package/dist/controller-registry.js +11 -22
- package/dist/controller-registry.js.map +1 -1
- package/dist/controllers/capability/capability-controller.d.ts +0 -5
- package/dist/controllers/capability/capability-controller.d.ts.map +1 -1
- package/dist/controllers/capability/capability-controller.js +1 -5
- package/dist/controllers/capability/capability-controller.js.map +1 -1
- package/dist/controllers/module/import-controller.d.ts +35 -0
- package/dist/controllers/module/import-controller.d.ts.map +1 -0
- package/dist/controllers/module/import-controller.js +113 -0
- package/dist/controllers/module/import-controller.js.map +1 -0
- package/dist/controllers/module/module-controller.d.ts +2 -15
- package/dist/controllers/module/module-controller.d.ts.map +1 -1
- package/dist/controllers/module/module-controller.js +9 -90
- package/dist/controllers/module/module-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts +3 -4
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +17 -11
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts +12 -0
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -0
- package/dist/controllers/resource-definition/resource-template-controller.js +112 -0
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -0
- package/dist/evaluation-context.d.ts +91 -0
- package/dist/evaluation-context.d.ts.map +1 -0
- package/dist/evaluation-context.js +220 -0
- package/dist/evaluation-context.js.map +1 -0
- package/dist/execution-context.d.ts +13 -0
- package/dist/execution-context.d.ts.map +1 -0
- package/dist/execution-context.js +14 -0
- package/dist/execution-context.js.map +1 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +31 -42
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +315 -371
- package/dist/kernel.js.map +1 -1
- package/dist/loader.d.ts +11 -10
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +50 -112
- package/dist/loader.js.map +1 -1
- package/dist/manifest-adapters/http-adapter.d.ts.map +1 -1
- package/dist/manifest-adapters/http-adapter.js +3 -1
- package/dist/manifest-adapters/http-adapter.js.map +1 -1
- package/dist/manifest-adapters/local-file-adapter.d.ts +7 -5
- package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -1
- package/dist/manifest-adapters/local-file-adapter.js +28 -24
- package/dist/manifest-adapters/local-file-adapter.js.map +1 -1
- package/dist/manifest-adapters/manifest-adapter.d.ts +2 -0
- package/dist/manifest-adapters/manifest-adapter.d.ts.map +1 -1
- package/dist/manifest-adapters/registry-adapter.d.ts.map +1 -1
- package/dist/manifest-adapters/registry-adapter.js +3 -1
- package/dist/manifest-adapters/registry-adapter.js.map +1 -1
- package/dist/manifest-schemas.d.ts +61 -49
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +58 -37
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context-registry.d.ts +48 -0
- package/dist/module-context-registry.d.ts.map +1 -0
- package/dist/module-context-registry.js +91 -0
- package/dist/module-context-registry.js.map +1 -0
- package/dist/module-context.d.ts +31 -0
- package/dist/module-context.d.ts.map +1 -0
- package/dist/module-context.js +67 -0
- package/dist/module-context.js.map +1 -0
- package/dist/registry.d.ts +1 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +3 -3
- package/dist/registry.js.map +1 -1
- package/dist/resource-context.d.ts +27 -10
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +100 -44
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-valiator.d.ts.map +1 -1
- package/dist/schema-valiator.js +16 -3
- package/dist/schema-valiator.js.map +1 -1
- package/dist/snapshot-serializer.d.ts +1 -2
- package/dist/snapshot-serializer.d.ts.map +1 -1
- package/dist/snapshot-serializer.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +11 -9
- package/src/boot-context-registry.ts +169 -0
- package/src/controller-loader.ts +299 -0
- package/src/controller-registry.ts +191 -0
- package/src/controllers/module/import-controller.ts +143 -0
- package/src/controllers/module/module-controller.ts +16 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +86 -0
- package/src/controllers/resource-definition/resource-definition.json +18 -0
- package/src/controllers/resource-definition/resource-template-controller.ts +138 -0
- package/src/event-stream.ts +121 -0
- package/src/events.ts +99 -0
- package/src/index.ts +7 -0
- package/src/kernel.ts +647 -0
- package/src/loader.ts +134 -0
- package/src/manifest-adapters/local-file-adapter.ts +62 -0
- package/src/manifest-adapters/manifest-adapter.ts +35 -0
- package/src/manifest-schemas.ts +85 -0
- package/src/registry.ts +137 -0
- package/src/resource-context.ts +267 -0
- package/src/resource-uri.ts +200 -0
- package/src/schema-valiator.ts +68 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -109
- package/dist/cli.js.map +0 -1
- package/dist/expressions.d.ts +0 -20
- package/dist/expressions.d.ts.map +0 -1
- package/dist/expressions.js +0 -253
- package/dist/expressions.js.map +0 -1
- package/dist/src/controllers/module/module.yaml +0 -32
- package/dist/template-definition.d.ts +0 -38
- package/dist/template-definition.d.ts.map +0 -1
- package/dist/template-definition.js +0 -26
- package/dist/template-definition.js.map +0 -1
- package/dist/template-expander.d.ts +0 -19
- package/dist/template-expander.d.ts.map +0 -1
- package/dist/template-expander.js +0 -425
- package/dist/template-expander.js.map +0 -1
package/src/kernel.ts
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { AnalysisRegistry, StaticAnalyzer } from "@telorun/analyzer";
|
|
2
|
+
import {
|
|
3
|
+
ControllerContext,
|
|
4
|
+
Kernel as IKernel,
|
|
5
|
+
ModuleContext,
|
|
6
|
+
ResourceContext,
|
|
7
|
+
ResourceDefinition,
|
|
8
|
+
ResourceInstance,
|
|
9
|
+
ResourceManifest,
|
|
10
|
+
RuntimeError,
|
|
11
|
+
RuntimeEvent,
|
|
12
|
+
isCompiledValue,
|
|
13
|
+
} from "@telorun/sdk";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import { ControllerRegistry } from "./controller-registry.js";
|
|
16
|
+
import { EventStream } from "./event-stream.js";
|
|
17
|
+
import { EventBus } from "./events.js";
|
|
18
|
+
import { Loader } from "./loader.js";
|
|
19
|
+
import { ResourceContextImpl } from "./resource-context.js";
|
|
20
|
+
import { SchemaValidator } from "./schema-valiator.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Kernel: Central orchestrator managing lifecycle and message bus
|
|
24
|
+
* Handles resource loading, initialization, and execution through controllers
|
|
25
|
+
*/
|
|
26
|
+
export class Kernel implements IKernel {
|
|
27
|
+
private readonly loader = new Loader();
|
|
28
|
+
private readonly analyzer = new StaticAnalyzer();
|
|
29
|
+
private readonly registry = new AnalysisRegistry();
|
|
30
|
+
// private manifests: ManifestRegistry = new ManifestRegistry();
|
|
31
|
+
private controllers: ControllerRegistry = new ControllerRegistry();
|
|
32
|
+
private eventBus: EventBus = new EventBus();
|
|
33
|
+
private eventStream: EventStream = new EventStream();
|
|
34
|
+
// private snapshotSerializer: SnapshotSerializer = new SnapshotSerializer();
|
|
35
|
+
// private runtimeManifests: ResourceManifest[] | null = null;
|
|
36
|
+
// private resourceInstances: Map<
|
|
37
|
+
// string,
|
|
38
|
+
// { resource: ResourceManifest; instance: ResourceInstance }
|
|
39
|
+
// > = new Map();
|
|
40
|
+
|
|
41
|
+
private holdCount = 0;
|
|
42
|
+
private idleResolvers: Array<() => void> = [];
|
|
43
|
+
private _exitCode = 0;
|
|
44
|
+
// private bootContextRegistry = new BootContextRegistry();
|
|
45
|
+
private readonly sharedSchemaValidator = new SchemaValidator();
|
|
46
|
+
private rootContext!: ModuleContext;
|
|
47
|
+
private staticManifests: ResourceManifest[] = [];
|
|
48
|
+
|
|
49
|
+
constructor() {
|
|
50
|
+
this.setupEventStreaming();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async registerController(
|
|
54
|
+
moduleName: string,
|
|
55
|
+
kindName: string,
|
|
56
|
+
controllerInstance: any,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
this.controllers.registerController(`${moduleName}.${kindName}`, controllerInstance);
|
|
59
|
+
await controllerInstance.register?.(this.createControllerContext(`${moduleName}.${kindName}`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register a resource definition with the controller registry
|
|
64
|
+
*/
|
|
65
|
+
registerResourceDefinition(definition: ResourceDefinition): void {
|
|
66
|
+
this.controllers.registerDefinition(definition);
|
|
67
|
+
this.registry.registerDefinition(definition);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getModuleContext(_moduleName: string): ModuleContext {
|
|
71
|
+
return this.rootContext;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
resolveModuleAlias(_declaringModule: string, alias: string): string | undefined {
|
|
75
|
+
return this.rootContext.importAliases.get(alias);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
registerModuleImport(
|
|
79
|
+
_declaringModule: string,
|
|
80
|
+
alias: string,
|
|
81
|
+
targetModule: string,
|
|
82
|
+
kinds: string[],
|
|
83
|
+
): void {
|
|
84
|
+
this.rootContext.registerImport(alias, targetModule, kinds);
|
|
85
|
+
this.registry.registerImport(alias, targetModule, kinds);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Returns the live analysis registry backed by this kernel's known definitions and aliases.
|
|
89
|
+
* Pass to StaticAnalyzer.analyze() for incremental validation of new manifests against
|
|
90
|
+
* already-registered types (e.g. front-end editor validating a manifest before submitting). */
|
|
91
|
+
getAnalysisRegistry(): AnalysisRegistry {
|
|
92
|
+
return this.registry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Load built-in Runtime definitions (e.g., Kernel.Module)
|
|
97
|
+
* Also declares all known module namespaces upfront so that resources can be
|
|
98
|
+
* registered to them. User-defined modules are declared explicitly by Kernel.Module
|
|
99
|
+
* resources during the initialization phase.
|
|
100
|
+
*/
|
|
101
|
+
private async loadBuiltinDefinitions(): Promise<void> {
|
|
102
|
+
// Declare built-in module namespaces upfront so getContext() can distinguish
|
|
103
|
+
// "not yet populated" from a completely unknown module name.
|
|
104
|
+
this.rootContext.registerImport("Kernel", "Kernel", []); // built-ins, unrestricted
|
|
105
|
+
|
|
106
|
+
// Register built-in definitions with the controller registry.
|
|
107
|
+
// AnalysisRegistry's underlying DefinitionRegistry already seeds KERNEL_BUILTINS on construction.
|
|
108
|
+
for (const def of this.registry.builtinDefinitions()) this.controllers.registerDefinition(def);
|
|
109
|
+
|
|
110
|
+
this.controllers.registerController(
|
|
111
|
+
"Kernel.Definition",
|
|
112
|
+
await import("./controllers/resource-definition/resource-definition-controller.js"),
|
|
113
|
+
);
|
|
114
|
+
this.controllers.registerController(
|
|
115
|
+
"Kernel.Module",
|
|
116
|
+
await import("./controllers/module/module-controller.js"),
|
|
117
|
+
);
|
|
118
|
+
this.controllers.registerController(
|
|
119
|
+
"Kernel.Import",
|
|
120
|
+
await import("./controllers/module/import-controller.js"),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Load from runtime configuration file
|
|
126
|
+
*/
|
|
127
|
+
async loadFromConfig(runtimeYamlPath: string): Promise<void> {
|
|
128
|
+
// Resolve directory paths to their module.yaml so that relative imports
|
|
129
|
+
// (e.g. ../../modules/foo) use the correct base directory.
|
|
130
|
+
const sourceUrl = await this.loader.resolveEntryPoint(
|
|
131
|
+
new URL(runtimeYamlPath, `file://${process.cwd()}/`).href,
|
|
132
|
+
);
|
|
133
|
+
this.rootContext = new ModuleContext(
|
|
134
|
+
sourceUrl,
|
|
135
|
+
{},
|
|
136
|
+
{},
|
|
137
|
+
{},
|
|
138
|
+
[],
|
|
139
|
+
this._createInstance.bind(this),
|
|
140
|
+
(event, payload) => this.eventBus.emit(event, payload),
|
|
141
|
+
process.env,
|
|
142
|
+
);
|
|
143
|
+
// Initialize built-in Runtime definitions first
|
|
144
|
+
await this.loadBuiltinDefinitions();
|
|
145
|
+
|
|
146
|
+
// Phase 5: attach injection hook — fires between create() and init() for every resource
|
|
147
|
+
this.rootContext.preInitHook = (resource, getInstance) =>
|
|
148
|
+
this._injectDependencies(resource, getInstance);
|
|
149
|
+
|
|
150
|
+
// Static analysis pre-flight: validates schemas and invocation context compatibility.
|
|
151
|
+
// All errors are fatal — kernel does not start if analysis fails.
|
|
152
|
+
const staticManifests = await this.loader.loadManifests(sourceUrl);
|
|
153
|
+
this.staticManifests = staticManifests;
|
|
154
|
+
|
|
155
|
+
// Register module identities for x-telo-ref resolution (Phase 3 prerequisite).
|
|
156
|
+
// Kernel built-ins ("kernel" → "Kernel") are auto-registered when Kernel.Abstract
|
|
157
|
+
// definitions are registered in loadBuiltinDefinitions() above.
|
|
158
|
+
for (const m of staticManifests) {
|
|
159
|
+
if (m.kind === "Kernel.Module" && m.metadata?.name && m.metadata?.namespace) {
|
|
160
|
+
this.registry.registerModuleIdentity(
|
|
161
|
+
m.metadata.namespace as string,
|
|
162
|
+
m.metadata.name as string,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const errors = this.analyzer.analyzeErrors(staticManifests, {}, this.registry);
|
|
168
|
+
if (errors.length > 0) {
|
|
169
|
+
throw new RuntimeError(
|
|
170
|
+
"ERR_MANIFEST_VALIDATION_FAILED",
|
|
171
|
+
"Manifest validation failed",
|
|
172
|
+
errors.map((d) => ({
|
|
173
|
+
severity: "error" as const,
|
|
174
|
+
message: d.message,
|
|
175
|
+
code: d.code !== undefined ? String(d.code) : undefined,
|
|
176
|
+
resource: (d.data as any)?.resource
|
|
177
|
+
? `${(d.data as any).resource.kind}.${(d.data as any).resource.name}`
|
|
178
|
+
: undefined,
|
|
179
|
+
})),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Load runtime configuration — root module gets access to host env
|
|
184
|
+
const allManifests = await this.loader.loadModule(sourceUrl, { compile: true });
|
|
185
|
+
|
|
186
|
+
// Phase 2: normalize inline resources — extract inline values from x-telo-ref slots
|
|
187
|
+
// into first-class named manifests and replace them in-place with {kind, name} references.
|
|
188
|
+
// Update staticManifests so Phase 3 (validateReferences) and Phase 4 (DAG) see
|
|
189
|
+
// the same normalized structure.
|
|
190
|
+
const normalizedManifests = this.analyzer.normalize(allManifests, this.registry);
|
|
191
|
+
this.staticManifests = normalizedManifests;
|
|
192
|
+
|
|
193
|
+
for (const manifest of normalizedManifests) {
|
|
194
|
+
if (manifest.kind === "Kernel.Module") {
|
|
195
|
+
this.rootContext.setSecrets(manifest.secrets ?? {});
|
|
196
|
+
this.rootContext.setVariables(manifest.variables ?? {});
|
|
197
|
+
this.rootContext.setTargets(manifest.targets ?? []);
|
|
198
|
+
}
|
|
199
|
+
this.rootContext.registerManifest(manifest);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Phase 1: Load - Ingest files from directory and load runtime config
|
|
205
|
+
* @deprecated Use loadFromConfig instead
|
|
206
|
+
*/
|
|
207
|
+
async loadDirectory(dirPath: string): Promise<void> {
|
|
208
|
+
const configYamlPath = path.join(dirPath, "module.yaml");
|
|
209
|
+
|
|
210
|
+
await this.loadFromConfig(configYamlPath);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Phase 2: Start - Initialize resources
|
|
215
|
+
*/
|
|
216
|
+
async start(): Promise<void> {
|
|
217
|
+
// Call controller register hooks first (before any initialization)
|
|
218
|
+
for (const kind of this.controllers.getKinds()) {
|
|
219
|
+
const controller = this.controllers.getController(kind);
|
|
220
|
+
if (controller?.register) {
|
|
221
|
+
await controller.register(this.createControllerContext(`controller:${kind}`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Phase 3+4: reference validation, cycle detection, and topo sort
|
|
226
|
+
const {
|
|
227
|
+
diagnostics: refErrors,
|
|
228
|
+
order,
|
|
229
|
+
cycleError,
|
|
230
|
+
} = this.analyzer.prepare(this.staticManifests, this.registry);
|
|
231
|
+
if (refErrors.length > 0) {
|
|
232
|
+
throw new RuntimeError(
|
|
233
|
+
"ERR_MANIFEST_VALIDATION_FAILED",
|
|
234
|
+
"Manifest validation failed",
|
|
235
|
+
refErrors.map((d) => ({
|
|
236
|
+
severity: "error" as const,
|
|
237
|
+
message: d.message,
|
|
238
|
+
code: d.code !== undefined ? String(d.code) : undefined,
|
|
239
|
+
resource: (d.data as any)?.resource
|
|
240
|
+
? `${(d.data as any).resource.kind}.${(d.data as any).resource.name}`
|
|
241
|
+
: undefined,
|
|
242
|
+
})),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (cycleError) {
|
|
246
|
+
throw new RuntimeError("ERR_CIRCULAR_DEPENDENCY", cycleError);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Phase 5: sort pending resources into topo order so injection always finds
|
|
250
|
+
// initialized dependencies, then run the init loop.
|
|
251
|
+
if (order) {
|
|
252
|
+
this.rootContext.setInitOrder(order);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Initialize resources
|
|
256
|
+
try {
|
|
257
|
+
await this.rootContext.initializeResources();
|
|
258
|
+
await this.eventBus.emit("Kernel.Initialized", {});
|
|
259
|
+
await this.eventBus.emit("Kernel.Starting", {});
|
|
260
|
+
await this.rootContext.runTargets();
|
|
261
|
+
await this.eventBus.emit("Kernel.Started", {});
|
|
262
|
+
await this.waitForIdle();
|
|
263
|
+
} finally {
|
|
264
|
+
await this.eventBus.emit("Kernel.Stopping", {});
|
|
265
|
+
await this.rootContext.teardownResources();
|
|
266
|
+
await this.eventBus.emit("Kernel.Stopped", { exitCode: this._exitCode });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async emitRuntimeEvent(event: string, payload?: any): Promise<void> {
|
|
271
|
+
await this.eventBus.emit(event, payload);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
get exitCode(): number {
|
|
275
|
+
return this._exitCode;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
requestExit(code: number): void {
|
|
279
|
+
this._exitCode = Math.max(this._exitCode, code);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
acquireHold(reason?: string): () => void {
|
|
283
|
+
this.holdCount += 1;
|
|
284
|
+
if (this.holdCount === 1) {
|
|
285
|
+
void this.eventBus.emit("Kernel.Blocked", {
|
|
286
|
+
reason,
|
|
287
|
+
count: this.holdCount,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
let released = false;
|
|
291
|
+
return () => {
|
|
292
|
+
if (released) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
released = true;
|
|
296
|
+
this.holdCount = Math.max(0, this.holdCount - 1);
|
|
297
|
+
if (this.holdCount === 0) {
|
|
298
|
+
const resolvers = this.idleResolvers.splice(0);
|
|
299
|
+
for (const resolve of resolvers) {
|
|
300
|
+
resolve();
|
|
301
|
+
}
|
|
302
|
+
void this.eventBus.emit("Kernel.Unblocked", { count: this.holdCount });
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
waitForIdle(): Promise<void> {
|
|
308
|
+
if (this.holdCount === 0) {
|
|
309
|
+
return Promise.resolve();
|
|
310
|
+
}
|
|
311
|
+
return new Promise((resolve) => {
|
|
312
|
+
this.idleResolvers.push(resolve);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Force-resolve waitForIdle() regardless of active holds.
|
|
318
|
+
* Used for graceful shutdown when external signals (e.g. SIGINT) should
|
|
319
|
+
* bypass resource holds and proceed directly to teardown.
|
|
320
|
+
*/
|
|
321
|
+
shutdown(): void {
|
|
322
|
+
const resolvers = this.idleResolvers.splice(0);
|
|
323
|
+
for (const resolve of resolvers) resolve();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
hasEventHandlers(event: string): boolean {
|
|
327
|
+
return this.eventBus.hasHandlers(event);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
on(event: string, handler: (event: RuntimeEvent) => void | Promise<void>): void {
|
|
331
|
+
this.eventBus.on(event, handler);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private createControllerContext(kind: string): ControllerContext {
|
|
335
|
+
return {
|
|
336
|
+
on: (event: string, handler: (event: RuntimeEvent) => void | Promise<void>) =>
|
|
337
|
+
this.eventBus.on(event, handler),
|
|
338
|
+
once: (event: string, handler: (event: RuntimeEvent) => void | Promise<void>) =>
|
|
339
|
+
this.eventBus.once(event, handler),
|
|
340
|
+
off: (event: string, handler: (event: RuntimeEvent) => void | Promise<void>) =>
|
|
341
|
+
this.eventBus.off(event, handler),
|
|
342
|
+
emit: (event: string, payload?: any) => {
|
|
343
|
+
const namespaced = event.includes(".") ? event : `${kind}.${event}`;
|
|
344
|
+
void this.eventBus.emit(namespaced, payload);
|
|
345
|
+
},
|
|
346
|
+
acquireHold: (reason?: string) => this.acquireHold(reason),
|
|
347
|
+
expandValue: (value: any, context: Record<string, any>) =>
|
|
348
|
+
this.rootContext.expandWith(value, context),
|
|
349
|
+
requestExit: (code: number) => this.requestExit(code),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private createResourceContext(
|
|
354
|
+
moduleContext: ModuleContext,
|
|
355
|
+
resource: ResourceManifest,
|
|
356
|
+
): ResourceContext {
|
|
357
|
+
return new ResourceContextImpl(
|
|
358
|
+
this,
|
|
359
|
+
moduleContext,
|
|
360
|
+
resource.metadata,
|
|
361
|
+
this.sharedSchemaValidator,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Create phase only: resolves the controller, validates the schema, and calls
|
|
367
|
+
* controller.create(). Returns { instance, ctx } so initializeResources can
|
|
368
|
+
* run init() separately in its second phase. Returns null when the controller
|
|
369
|
+
* is not yet registered (retry signal).
|
|
370
|
+
*/
|
|
371
|
+
private async _createInstance(
|
|
372
|
+
evalContext: ModuleContext,
|
|
373
|
+
resource: ResourceManifest,
|
|
374
|
+
): Promise<{ instance: ResourceInstance; ctx: ResourceContext } | null> {
|
|
375
|
+
const kind = resource.kind;
|
|
376
|
+
|
|
377
|
+
// Resolve the alias-prefixed kind to its real fully-qualified kind.
|
|
378
|
+
// resolveKind() throws with a clear message if the alias or kind is not found.
|
|
379
|
+
const resolvedKind = this.rootContext.resolveKind(kind);
|
|
380
|
+
|
|
381
|
+
const controller = this.controllers.getControllerOrUndefined(resolvedKind);
|
|
382
|
+
if (!controller) {
|
|
383
|
+
const kindInfo =
|
|
384
|
+
resolvedKind !== kind ? `'${kind}' (resolved to '${resolvedKind}')` : `'${kind}'`;
|
|
385
|
+
throw new Error(
|
|
386
|
+
`No controller registered for kind ${kindInfo}, known controllers are: ${this.controllers.getKinds().join(", ")}`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!controller.create) {
|
|
391
|
+
throw new RuntimeError(
|
|
392
|
+
"ERR_CONTROLLER_INVALID",
|
|
393
|
+
`Controller for ${kind} does not implement create method`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (!controller.schema?.type) {
|
|
397
|
+
throw new Error(`No schema defined for ${kind} controller`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Resolve eval paths from x-telo-eval annotations in the parent and own schema
|
|
401
|
+
const definition = this.controllers.getDefinition(resolvedKind);
|
|
402
|
+
const parentDef = definition?.capability
|
|
403
|
+
? this.controllers.getDefinition(definition.capability)
|
|
404
|
+
: undefined;
|
|
405
|
+
const parentEval = parentDef?.schema
|
|
406
|
+
? buildEvalPaths(parentDef.schema)
|
|
407
|
+
: { compile: [], runtime: [] };
|
|
408
|
+
const ownEval = definition?.schema
|
|
409
|
+
? buildEvalPaths(definition.schema)
|
|
410
|
+
: { compile: [], runtime: [] };
|
|
411
|
+
const compile = [...parentEval.compile, ...ownEval.compile];
|
|
412
|
+
const runtime = [...parentEval.runtime, ...ownEval.runtime];
|
|
413
|
+
|
|
414
|
+
// Schema validation runs before CEL evaluation so it sees the original manifest
|
|
415
|
+
// shape. CompiledValue wrappers (from load-time precompilation) are stripped,
|
|
416
|
+
// restoring the pre-CEL string view that the schema expects.
|
|
417
|
+
try {
|
|
418
|
+
this.sharedSchemaValidator
|
|
419
|
+
.compile(controller.schema)
|
|
420
|
+
.validate(stripCompiledValues(resource, controller.schema as Record<string, unknown>));
|
|
421
|
+
} catch (error) {
|
|
422
|
+
throw new RuntimeError(
|
|
423
|
+
"ERR_RESOURCE_SCHEMA_VALIDATION_FAILED",
|
|
424
|
+
`Resource does not match schema for kind ${kind}: ${error instanceof Error ? error.message : String(error)}`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Expand compile-time CEL fields before passing to the controller.
|
|
429
|
+
const processedResource = compile.length
|
|
430
|
+
? (evalContext.expandPaths(
|
|
431
|
+
resource as Record<string, unknown>,
|
|
432
|
+
compile,
|
|
433
|
+
runtime,
|
|
434
|
+
) as ResourceManifest)
|
|
435
|
+
: resource;
|
|
436
|
+
|
|
437
|
+
const ctx = this.createResourceContext(evalContext, processedResource);
|
|
438
|
+
const instance = await controller.create(processedResource, ctx);
|
|
439
|
+
if (!instance) return null;
|
|
440
|
+
|
|
441
|
+
if (!runtime.length) return { instance, ctx };
|
|
442
|
+
|
|
443
|
+
const wrapped: ResourceInstance = {
|
|
444
|
+
...instance,
|
|
445
|
+
invoke: async (inputs: any) => {
|
|
446
|
+
const expanded = evalContext.expandPaths(inputs as Record<string, unknown>, runtime);
|
|
447
|
+
return instance.invoke!(expanded);
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
return { instance: wrapped, ctx };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Phase 5 — Inject live instances into reference fields of a resource config.
|
|
455
|
+
*
|
|
456
|
+
* Called between create() and init() for every resource. Walks the definition's
|
|
457
|
+
* field map and replaces each {kind, name} reference value (outside scope visibility
|
|
458
|
+
* paths) with the live ResourceInstance returned by getInstance(name). Fields within
|
|
459
|
+
* scope paths are left as {kind, name} — the controller resolves them at runtime.
|
|
460
|
+
*/
|
|
461
|
+
private _injectDependencies(
|
|
462
|
+
resource: ResourceManifest,
|
|
463
|
+
getInstance: (name: string) => ResourceInstance | undefined,
|
|
464
|
+
): void {
|
|
465
|
+
this.registry.iterateFieldEntries(
|
|
466
|
+
resource,
|
|
467
|
+
(fieldPath) => injectAtPath(resource, fieldPath, getInstance),
|
|
468
|
+
(fieldPath) => {
|
|
469
|
+
const val = (resource as Record<string, unknown>)[fieldPath];
|
|
470
|
+
if (Array.isArray(val)) {
|
|
471
|
+
(resource as Record<string, unknown>)[fieldPath] = this.rootContext.createScopeHandle(
|
|
472
|
+
val as ResourceManifest[],
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Enable event streaming to a file (JSONL format)
|
|
481
|
+
*/
|
|
482
|
+
async enableEventStream(filePath: string): Promise<void> {
|
|
483
|
+
await this.eventStream.enable(filePath);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Disable event streaming
|
|
488
|
+
*/
|
|
489
|
+
disableEventStream(): void {
|
|
490
|
+
this.eventStream.disable();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Get the event stream for testing and inspection
|
|
495
|
+
*/
|
|
496
|
+
getEventStream(): EventStream {
|
|
497
|
+
return this.eventStream;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Setup event streaming hook to capture all events
|
|
502
|
+
*/
|
|
503
|
+
private setupEventStreaming(): void {
|
|
504
|
+
const originalEmit = this.eventBus.emit.bind(this.eventBus);
|
|
505
|
+
this.eventBus.emit = async (event: string, payload?: any) => {
|
|
506
|
+
if (this.eventStream.isEnabledStream()) {
|
|
507
|
+
await this.eventStream.log(event, payload);
|
|
508
|
+
}
|
|
509
|
+
return originalEmit(event, payload);
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Returns a schema-appropriate placeholder value for a CompiledValue field. */
|
|
515
|
+
function placeholderForSchema(schema: Record<string, unknown>): unknown {
|
|
516
|
+
if (schema.default !== undefined) return schema.default;
|
|
517
|
+
switch (schema.type) {
|
|
518
|
+
case "integer":
|
|
519
|
+
case "number":
|
|
520
|
+
return (schema.minimum as number | undefined) ?? 0;
|
|
521
|
+
case "boolean":
|
|
522
|
+
return false;
|
|
523
|
+
case "array":
|
|
524
|
+
return [];
|
|
525
|
+
case "object":
|
|
526
|
+
return {};
|
|
527
|
+
default:
|
|
528
|
+
return "";
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Replaces CompiledValue wrappers with schema-appropriate placeholders for schema validation.
|
|
533
|
+
* Template strings were compiled from YAML at load time; this restores a shape
|
|
534
|
+
* that AJV can validate without evaluating expressions. */
|
|
535
|
+
function stripCompiledValues(v: unknown, schema: Record<string, unknown> = {}): unknown {
|
|
536
|
+
if (isCompiledValue(v)) return placeholderForSchema(schema);
|
|
537
|
+
if (Array.isArray(v)) {
|
|
538
|
+
const itemSchema = (schema.items ?? {}) as Record<string, unknown>;
|
|
539
|
+
return v.map((item) => stripCompiledValues(item, itemSchema));
|
|
540
|
+
}
|
|
541
|
+
if (v !== null && typeof v === "object") {
|
|
542
|
+
const props = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
|
|
543
|
+
const out: Record<string, unknown> = {};
|
|
544
|
+
for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
|
|
545
|
+
out[k] = stripCompiledValues(val, props[k] ?? {});
|
|
546
|
+
}
|
|
547
|
+
return out;
|
|
548
|
+
}
|
|
549
|
+
return v;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Walks `resource` following `fieldPath` (dot notation, `[]` = array traversal).
|
|
554
|
+
* For each leaf value that looks like a {kind, name} reference, calls getInstance(name)
|
|
555
|
+
* and replaces the value in-place with the returned live ResourceInstance.
|
|
556
|
+
* Values where getInstance returns undefined are left unchanged.
|
|
557
|
+
*/
|
|
558
|
+
/**
|
|
559
|
+
* Traverses a definition schema and collects all paths annotated with `x-telo-eval`.
|
|
560
|
+
* Root-level `x-telo-eval` produces the `"**"` wildcard (expand all fields).
|
|
561
|
+
* Property-level annotations produce the dot-notation path to that property.
|
|
562
|
+
*/
|
|
563
|
+
function buildEvalPaths(schema: Record<string, any>): { compile: string[]; runtime: string[] } {
|
|
564
|
+
const compile: string[] = [];
|
|
565
|
+
const runtime: string[] = [];
|
|
566
|
+
|
|
567
|
+
if (schema["x-telo-eval"] === "compile") compile.push("**");
|
|
568
|
+
else if (schema["x-telo-eval"] === "runtime") runtime.push("**");
|
|
569
|
+
|
|
570
|
+
if (schema.properties) {
|
|
571
|
+
for (const [key, propSchema] of Object.entries(schema.properties as Record<string, any>)) {
|
|
572
|
+
collectEvalPathsNode(propSchema, key, compile, runtime);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return { compile, runtime };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function collectEvalPathsNode(
|
|
580
|
+
node: Record<string, any>,
|
|
581
|
+
path: string,
|
|
582
|
+
compile: string[],
|
|
583
|
+
runtime: string[],
|
|
584
|
+
): void {
|
|
585
|
+
if (node["x-telo-eval"] === "compile") {
|
|
586
|
+
compile.push(path);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (node["x-telo-eval"] === "runtime") {
|
|
590
|
+
runtime.push(path);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (node.properties) {
|
|
594
|
+
for (const [key, propSchema] of Object.entries(node.properties as Record<string, any>)) {
|
|
595
|
+
collectEvalPathsNode(propSchema, `${path}.${key}`, compile, runtime);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function injectAtPath(
|
|
601
|
+
resource: ResourceManifest,
|
|
602
|
+
fieldPath: string,
|
|
603
|
+
getInstance: (name: string) => ResourceInstance | undefined,
|
|
604
|
+
): void {
|
|
605
|
+
const parts = fieldPath.split(".");
|
|
606
|
+
|
|
607
|
+
function traverse(obj: unknown, partsLeft: string[]): void {
|
|
608
|
+
if (!obj || typeof obj !== "object" || partsLeft.length === 0) return;
|
|
609
|
+
const [head, ...rest] = partsLeft;
|
|
610
|
+
const isArr = head.endsWith("[]");
|
|
611
|
+
const key = isArr ? head.slice(0, -2) : head;
|
|
612
|
+
const container = obj as Record<string, unknown>;
|
|
613
|
+
const val = container[key];
|
|
614
|
+
if (val == null) return;
|
|
615
|
+
|
|
616
|
+
if (isArr) {
|
|
617
|
+
if (!Array.isArray(val)) return;
|
|
618
|
+
for (let i = 0; i < val.length; i++) {
|
|
619
|
+
const elem = val[i];
|
|
620
|
+
if (!elem || typeof elem !== "object") continue;
|
|
621
|
+
if (rest.length === 0) {
|
|
622
|
+
const ref = elem as Record<string, unknown>;
|
|
623
|
+
if (typeof ref.kind === "string" && typeof ref.name === "string") {
|
|
624
|
+
const instance = getInstance(ref.name);
|
|
625
|
+
if (instance) val[i] = instance;
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
traverse(elem, rest);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
if (rest.length === 0) {
|
|
633
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
634
|
+
const ref = val as Record<string, unknown>;
|
|
635
|
+
if (typeof ref.kind === "string" && typeof ref.name === "string") {
|
|
636
|
+
const instance = getInstance(ref.name);
|
|
637
|
+
if (instance) container[key] = instance;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
traverse(val, rest);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
traverse(resource, parts);
|
|
647
|
+
}
|