@telorun/kernel 0.12.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application-env.d.ts +24 -0
- package/dist/application-env.d.ts.map +1 -0
- package/dist/application-env.js +156 -0
- package/dist/application-env.js.map +1 -0
- package/dist/controller-loaders/npm-loader.d.ts +1 -6
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +21 -62
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts +1 -0
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +3 -0
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts +5 -0
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.js +67 -6
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +19 -3
- package/dist/kernel.js.map +1 -1
- package/package.json +6 -5
- package/src/application-env.ts +216 -0
- package/src/controller-loaders/npm-loader.ts +21 -62
- package/src/controllers/resource-definition/resource-definition-controller.ts +6 -0
- package/src/controllers/resource-definition/resource-template-controller.ts +95 -7
- package/src/kernel.ts +24 -3
- package/dist/generated/runtime-deps.json +0 -6
|
@@ -201,25 +201,22 @@ export class NpmControllerLoader {
|
|
|
201
201
|
const entryDir = path.dirname(path.resolve(entryPath));
|
|
202
202
|
const installRoot = path.join(entryDir, ".telo", "npm");
|
|
203
203
|
|
|
204
|
-
// Build the install-root package.json: kernel-runtime deps as `file:` refs
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
const runtimeDeps = await loadRuntimeDeps();
|
|
204
|
+
// Build the install-root package.json: kernel-runtime deps as `file:` refs
|
|
205
|
+
// pointing at the kernel-side realpath. Modules declare these names as
|
|
206
|
+
// `peerDependencies`, so npm/pnpm resolve each controller's `import` to the
|
|
207
|
+
// single copy provided here — the realm-collapse mechanism that gives
|
|
208
|
+
// class-identity-sensitive types (today: `Stream`) one constructor across
|
|
209
|
+
// the kernel/controller boundary.
|
|
211
210
|
const dependencies: Record<string, string> = {};
|
|
212
|
-
const
|
|
213
|
-
for (const name of runtimeDeps) {
|
|
211
|
+
for (const name of REALM_COLLAPSE_NAMES) {
|
|
214
212
|
const resolvedPkgRoot = await resolveKernelPackageRoot(name);
|
|
215
213
|
if (!resolvedPkgRoot) {
|
|
216
214
|
// A kernel runtime dep that can't be resolved at boot is unusual but
|
|
217
|
-
// not fatal — the realm-collapse story degrades to "rely on
|
|
218
|
-
//
|
|
215
|
+
// not fatal — the realm-collapse story degrades to "rely on whatever
|
|
216
|
+
// the package manager picks" for that name. Don't crash the loader.
|
|
219
217
|
continue;
|
|
220
218
|
}
|
|
221
219
|
dependencies[name] = `file:${resolvedPkgRoot}`;
|
|
222
|
-
overrides[name] = `$${name}`;
|
|
223
220
|
}
|
|
224
221
|
|
|
225
222
|
const packageJson = {
|
|
@@ -227,8 +224,6 @@ export class NpmControllerLoader {
|
|
|
227
224
|
private: true,
|
|
228
225
|
version: "0.0.0",
|
|
229
226
|
dependencies,
|
|
230
|
-
overrides,
|
|
231
|
-
pnpm: { overrides },
|
|
232
227
|
};
|
|
233
228
|
const packageJsonPath = path.join(installRoot, "package.json");
|
|
234
229
|
const stateFile = path.join(installRoot, ".telo-state.json");
|
|
@@ -405,54 +400,18 @@ export class NpmControllerLoader {
|
|
|
405
400
|
}
|
|
406
401
|
|
|
407
402
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
// Walk up from this file's location to the kernel-package root (the dir
|
|
419
|
-
// that contains package.json). In dev: `kernel/nodejs/`. In published:
|
|
420
|
-
// the installed package root inside `node_modules/@telorun/kernel/`.
|
|
421
|
-
// Walk-until-root rather than a fixed depth — the directory tree depth
|
|
422
|
-
// depends on whether we're in a workspace or installed tree.
|
|
423
|
-
const pkgDir = await walkUpToPackageRoot(path.dirname(here));
|
|
424
|
-
if (!pkgDir) return [];
|
|
425
|
-
|
|
426
|
-
const generated = path.join(pkgDir, "dist", "generated", "runtime-deps.json");
|
|
427
|
-
if (!(await pathExists(generated))) return [];
|
|
428
|
-
// The file is generated by `scripts/generate-runtime-deps.mjs`; if it
|
|
429
|
-
// exists but is malformed, that's a kernel-build bug. Don't swallow —
|
|
430
|
-
// surface so the cause is debuggable. Realm collapse is the whole point
|
|
431
|
-
// of this code path; quietly degrading to "no realm collapse" would
|
|
432
|
-
// silently re-introduce the very bug the file fixes.
|
|
433
|
-
const data = JSON.parse(await fs.readFile(generated, "utf8"));
|
|
434
|
-
if (!Array.isArray(data?.names)) {
|
|
435
|
-
throw new Error(
|
|
436
|
-
`[telo] ${generated} is malformed: expected { names: string[] } at top level. ` +
|
|
437
|
-
`Re-run \`node scripts/generate-runtime-deps.mjs <pkg-dir>\` to regenerate.`,
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
return data.names as string[];
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Walk up from `from` until the directory contains a `package.json`.
|
|
445
|
-
* Returns null at filesystem root if none found.
|
|
403
|
+
* Names of packages whose realpath must be shared between the kernel and every
|
|
404
|
+
* loaded controller. Each name here becomes a `file:` dep in the install-root
|
|
405
|
+
* `package.json`, pinned at the kernel's own resolution; controllers declare
|
|
406
|
+
* these names as `peerDependencies` so npm/pnpm resolves them to that single
|
|
407
|
+
* copy instead of nesting their own.
|
|
408
|
+
*
|
|
409
|
+
* Add a name here if you ship another shared runtime symbol whose `instanceof`
|
|
410
|
+
* or constructor identity matters across module boundaries. Today the only
|
|
411
|
+
* such name is `@telorun/sdk` (carries the `Stream` class registered with
|
|
412
|
+
* `@marcbachmann/cel-js`).
|
|
446
413
|
*/
|
|
447
|
-
|
|
448
|
-
let dir = from;
|
|
449
|
-
while (true) {
|
|
450
|
-
if (await pathExists(path.join(dir, "package.json"))) return dir;
|
|
451
|
-
const parent = path.dirname(dir);
|
|
452
|
-
if (parent === dir) return null;
|
|
453
|
-
dir = parent;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
414
|
+
const REALM_COLLAPSE_NAMES: ReadonlyArray<string> = ["@telorun/sdk"];
|
|
456
415
|
|
|
457
416
|
/**
|
|
458
417
|
* Resolve a kernel-runtime dep name to the realpath of its package directory.
|
|
@@ -954,7 +913,7 @@ export const __testing__ = {
|
|
|
954
913
|
resolvePackageExportTarget,
|
|
955
914
|
resolveExportTargetValue,
|
|
956
915
|
tryResolveFile,
|
|
957
|
-
walkUpToPackageRoot,
|
|
958
916
|
EXPORTS_MAX_DEPTH,
|
|
959
917
|
DEFAULT_RESOLVER_CONDITIONS,
|
|
918
|
+
REALM_COLLAPSE_NAMES,
|
|
960
919
|
};
|
|
@@ -18,6 +18,7 @@ type ResourceDefinitionResource = RuntimeResource & {
|
|
|
18
18
|
schema: Record<string, any>;
|
|
19
19
|
capability?: string;
|
|
20
20
|
controllers?: Array<string>;
|
|
21
|
+
provide?: unknown;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -31,6 +32,11 @@ class ResourceDefinition implements ResourceInstance {
|
|
|
31
32
|
|
|
32
33
|
async init(ctx: ResourceContext) {
|
|
33
34
|
if (!this.resource.controllers?.length) {
|
|
35
|
+
if (this.resource.capability === "Telo.Provider" && this.resource.provide == null) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Telo.Definition '${this.resource.metadata.name}': 'capability: Telo.Provider' requires either 'controllers:' (TS-backed) or 'provide:' (template-backed).`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
34
40
|
const controllerInstance = createTemplateController(this.resource as any);
|
|
35
41
|
ctx.registerDefinition(this.resource);
|
|
36
42
|
await ctx.registerController(
|
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import type { ControllerInstance, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import { isCompiledValue } from "@telorun/sdk";
|
|
3
3
|
|
|
4
|
+
/** Reports the resources: entries available to dispatch against, by expanded
|
|
5
|
+
* name and kind. Used in error messages to guide the developer back to the
|
|
6
|
+
* template's `resources:` array when a dispatch target doesn't match. */
|
|
7
|
+
function describeAvailableTargets(
|
|
8
|
+
ctx: ResourceContext,
|
|
9
|
+
resources: any[] | undefined,
|
|
10
|
+
self: Record<string, unknown>,
|
|
11
|
+
): string {
|
|
12
|
+
if (!resources || resources.length === 0) return "<none>";
|
|
13
|
+
return resources
|
|
14
|
+
.map((r) => {
|
|
15
|
+
const expanded = ctx.moduleContext.expandWith(r?.metadata?.name ?? "", { self }) as string;
|
|
16
|
+
const kind = typeof r?.kind === "string" ? r.kind : "<unknown-kind>";
|
|
17
|
+
return `'${expanded || "<unnamed>"}' (${kind})`;
|
|
18
|
+
})
|
|
19
|
+
.join(", ");
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
export function createTemplateController(definition: {
|
|
5
23
|
schema: Record<string, any>;
|
|
6
24
|
resources?: any[];
|
|
7
25
|
invoke?: string | { kind?: string; name: string };
|
|
8
26
|
inputs?: Record<string, any>;
|
|
9
27
|
run?: string;
|
|
28
|
+
provide?: { kind: string; name: string };
|
|
29
|
+
result?: Record<string, any>;
|
|
10
30
|
}): ControllerInstance {
|
|
11
31
|
return {
|
|
12
32
|
schema: definition.schema ?? { type: "object", additionalProperties: true },
|
|
@@ -32,6 +52,9 @@ export function createTemplateController(definition: {
|
|
|
32
52
|
const runTarget = definition.run
|
|
33
53
|
? (ctx.moduleContext.expandWith(definition.run, { self }) as string)
|
|
34
54
|
: null;
|
|
55
|
+
const provideTarget = definition.provide?.name
|
|
56
|
+
? (ctx.moduleContext.expandWith(definition.provide.name, { self }) as string)
|
|
57
|
+
: null;
|
|
35
58
|
|
|
36
59
|
const persistentManifests: any[] = [];
|
|
37
60
|
let ephemeralTemplate: any = null;
|
|
@@ -40,7 +63,10 @@ export function createTemplateController(definition: {
|
|
|
40
63
|
const expandedName = ctx.moduleContext.expandWith(template.metadata?.name ?? "", {
|
|
41
64
|
self,
|
|
42
65
|
}) as string;
|
|
43
|
-
const isTarget =
|
|
66
|
+
const isTarget =
|
|
67
|
+
expandedName === invokeTarget ||
|
|
68
|
+
expandedName === runTarget ||
|
|
69
|
+
expandedName === provideTarget;
|
|
44
70
|
if (isTarget) {
|
|
45
71
|
ephemeralTemplate = template;
|
|
46
72
|
} else {
|
|
@@ -84,7 +110,8 @@ export function createTemplateController(definition: {
|
|
|
84
110
|
invoke: async (inputs: any) => {
|
|
85
111
|
if (!ephemeralTemplate) {
|
|
86
112
|
throw new Error(
|
|
87
|
-
`Template '${resource.metadata.name}':
|
|
113
|
+
`Template '${resource.metadata.name}': 'invoke:' targets '${invokeTarget}' ` +
|
|
114
|
+
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
88
115
|
);
|
|
89
116
|
}
|
|
90
117
|
const extraContext = { self, inputs };
|
|
@@ -95,7 +122,13 @@ export function createTemplateController(definition: {
|
|
|
95
122
|
return withEphemeral(expanded, async (name) => {
|
|
96
123
|
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
97
124
|
if (!entry?.instance?.invoke) {
|
|
98
|
-
|
|
125
|
+
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
126
|
+
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
127
|
+
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Template '${resource.metadata.name}': 'invoke:' target '${targetKind}/${invokeTarget}' ` +
|
|
130
|
+
`has capability '${actualCap}', not Telo.Invocable. Update 'invoke:' to a Telo.Invocable kind, or change the target's kind in 'resources:'.`,
|
|
131
|
+
);
|
|
99
132
|
}
|
|
100
133
|
// Top-level `inputs:` (sibling of `invoke:`) carries the values passed
|
|
101
134
|
// to the dispatch target's invoke(). When absent, fall back to the
|
|
@@ -108,7 +141,13 @@ export function createTemplateController(definition: {
|
|
|
108
141
|
extraContext,
|
|
109
142
|
)
|
|
110
143
|
: expanded.inputs ?? inputs;
|
|
111
|
-
|
|
144
|
+
const raw = await entry.instance.invoke(invokeInputs);
|
|
145
|
+
if (definition.result == null) return raw;
|
|
146
|
+
const resultContext = { self, result: raw };
|
|
147
|
+
return ctx.moduleContext.expandWith(
|
|
148
|
+
ctx.moduleContext.expandWith(definition.result, resultContext),
|
|
149
|
+
resultContext,
|
|
150
|
+
);
|
|
112
151
|
});
|
|
113
152
|
},
|
|
114
153
|
}),
|
|
@@ -117,24 +156,73 @@ export function createTemplateController(definition: {
|
|
|
117
156
|
run: async () => {
|
|
118
157
|
if (!ephemeralTemplate) {
|
|
119
158
|
throw new Error(
|
|
120
|
-
`Template '${resource.metadata.name}':
|
|
159
|
+
`Template '${resource.metadata.name}': 'run:' targets '${runTarget}' ` +
|
|
160
|
+
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
121
161
|
);
|
|
122
162
|
}
|
|
123
163
|
const extraContext = { self };
|
|
124
164
|
const expanded = ctx.moduleContext.expandWith(
|
|
125
165
|
ctx.moduleContext.expandWith(ephemeralTemplate, extraContext),
|
|
126
166
|
extraContext,
|
|
127
|
-
);
|
|
167
|
+
) as any;
|
|
128
168
|
return withEphemeral(expanded, async (name) => {
|
|
129
169
|
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
130
170
|
if (!entry?.instance?.run) {
|
|
131
|
-
|
|
171
|
+
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
172
|
+
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
173
|
+
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Template '${resource.metadata.name}': 'run:' target '${targetKind}/${runTarget}' ` +
|
|
176
|
+
`has capability '${actualCap}', not Telo.Runnable. Update 'run:' to a Telo.Runnable kind, or change the target's kind in 'resources:'.`,
|
|
177
|
+
);
|
|
132
178
|
}
|
|
133
179
|
return entry.instance.run();
|
|
134
180
|
});
|
|
135
181
|
},
|
|
136
182
|
}),
|
|
137
183
|
|
|
184
|
+
...(provideTarget && {
|
|
185
|
+
provide: async () => {
|
|
186
|
+
if (!ephemeralTemplate) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Template '${resource.metadata.name}': 'provide:' targets '${provideTarget}' ` +
|
|
189
|
+
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const extraContext = { self };
|
|
193
|
+
const expanded = ctx.moduleContext.expandWith(
|
|
194
|
+
ctx.moduleContext.expandWith(ephemeralTemplate, extraContext),
|
|
195
|
+
extraContext,
|
|
196
|
+
) as any;
|
|
197
|
+
return withEphemeral(expanded, async (name) => {
|
|
198
|
+
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
199
|
+
if (!entry?.instance?.invoke) {
|
|
200
|
+
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
201
|
+
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
202
|
+
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Template '${resource.metadata.name}': 'provide:' target '${targetKind}/${provideTarget}' ` +
|
|
205
|
+
`has capability '${actualCap}', not Telo.Invocable. Update 'provide:' to a Telo.Invocable kind, or change the target's kind in 'resources:'.`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const provideInputs: any =
|
|
209
|
+
definition.inputs != null
|
|
210
|
+
? ctx.moduleContext.expandWith(
|
|
211
|
+
ctx.moduleContext.expandWith(definition.inputs, extraContext),
|
|
212
|
+
extraContext,
|
|
213
|
+
)
|
|
214
|
+
: {};
|
|
215
|
+
const raw = await entry.instance.invoke(provideInputs);
|
|
216
|
+
if (definition.result == null) return raw;
|
|
217
|
+
const resultContext = { self, result: raw };
|
|
218
|
+
return ctx.moduleContext.expandWith(
|
|
219
|
+
ctx.moduleContext.expandWith(definition.result, resultContext),
|
|
220
|
+
resultContext,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
|
|
138
226
|
teardown: async () => {
|
|
139
227
|
await childContext.teardownResources();
|
|
140
228
|
},
|
package/src/kernel.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { EventStream } from "./event-stream.js";
|
|
|
30
30
|
import { EventBus } from "./events.js";
|
|
31
31
|
import { ModuleContext } from "./module-context.js";
|
|
32
32
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
33
|
+
import { resolveApplicationEnv } from "./application-env.js";
|
|
33
34
|
import { policyFingerprint } from "./runtime-registry.js";
|
|
34
35
|
import { SchemaValidator } from "./schema-validator.js";
|
|
35
36
|
|
|
@@ -304,15 +305,35 @@ export class Kernel implements IKernel {
|
|
|
304
305
|
const normalizedManifests = this.analyzer.normalize(allManifests, this.registry);
|
|
305
306
|
this.staticManifests = normalizedManifests;
|
|
306
307
|
|
|
308
|
+
let rootApplicationManifest: ResourceManifest | undefined;
|
|
307
309
|
for (const manifest of normalizedManifests) {
|
|
308
310
|
if (isModuleKind(manifest.kind)) {
|
|
309
|
-
// Root is always Telo.Application (Library root rejected above).
|
|
310
|
-
//
|
|
311
|
-
//
|
|
311
|
+
// Root is always Telo.Application (Library root rejected above).
|
|
312
|
+
// Application-level `variables` / `secrets` declarations carry an `env:`
|
|
313
|
+
// mapping per field; the kernel populates the root scope from
|
|
314
|
+
// `process.env` after the manifest loop so imports can read
|
|
315
|
+
// `${{ variables.X }}` during their own init.
|
|
312
316
|
this.rootContext.setTargets(manifest.targets ?? []);
|
|
317
|
+
if (manifest.kind === "Telo.Application") {
|
|
318
|
+
rootApplicationManifest = manifest;
|
|
319
|
+
}
|
|
313
320
|
}
|
|
314
321
|
this.rootContext.registerManifest(manifest);
|
|
315
322
|
}
|
|
323
|
+
|
|
324
|
+
if (rootApplicationManifest) {
|
|
325
|
+
const { variables, secrets } = resolveApplicationEnv(
|
|
326
|
+
rootApplicationManifest as Record<string, any>,
|
|
327
|
+
this.env,
|
|
328
|
+
this.sharedSchemaValidator,
|
|
329
|
+
);
|
|
330
|
+
if (Object.keys(variables).length > 0) {
|
|
331
|
+
this.rootContext.setVariables(variables);
|
|
332
|
+
}
|
|
333
|
+
if (Object.keys(secrets).length > 0) {
|
|
334
|
+
this.rootContext.setSecrets(secrets);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
316
337
|
}
|
|
317
338
|
|
|
318
339
|
/**
|