@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.
Files changed (26) hide show
  1. package/dist/application-env.d.ts +24 -0
  2. package/dist/application-env.d.ts.map +1 -0
  3. package/dist/application-env.js +156 -0
  4. package/dist/application-env.js.map +1 -0
  5. package/dist/controller-loaders/npm-loader.d.ts +1 -6
  6. package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
  7. package/dist/controller-loaders/npm-loader.js +21 -62
  8. package/dist/controller-loaders/npm-loader.js.map +1 -1
  9. package/dist/controllers/resource-definition/resource-definition-controller.d.ts +1 -0
  10. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  11. package/dist/controllers/resource-definition/resource-definition-controller.js +3 -0
  12. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  13. package/dist/controllers/resource-definition/resource-template-controller.d.ts +5 -0
  14. package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
  15. package/dist/controllers/resource-definition/resource-template-controller.js +67 -6
  16. package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
  17. package/dist/kernel.d.ts.map +1 -1
  18. package/dist/kernel.js +19 -3
  19. package/dist/kernel.js.map +1 -1
  20. package/package.json +6 -5
  21. package/src/application-env.ts +216 -0
  22. package/src/controller-loaders/npm-loader.ts +21 -62
  23. package/src/controllers/resource-definition/resource-definition-controller.ts +6 -0
  24. package/src/controllers/resource-definition/resource-template-controller.ts +95 -7
  25. package/src/kernel.ts +24 -3
  26. 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
- // `overrides` + `pnpm.overrides` pinning every name to `$<name>`. This is
206
- // the realm-collapse mechanism npm's `file:` symlinks the kernel's own
207
- // package locations, the resolver follows symlinks to the realpath, and
208
- // every controller transitively resolving these names lands on the
209
- // identical module instance the kernel itself is using.
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 overrides: Record<string, string> = {};
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 registry
218
- // resolution + overrides" for that name. Don't crash the loader.
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
- * Read the realm-collapse name list shipped with the kernel under
409
- * `dist/generated/runtime-deps.json`. The list is small and stable (today:
410
- * `@telorun/sdk`); see `scripts/generate-runtime-deps.mjs` for the rationale
411
- * about which packages belong here. Returns an empty array if the file is
412
- * unreadable the npm-loader then degrades to "no realm collapse," which is
413
- * still a working install path; only `Stream`-flavoured class-identity bugs
414
- * resurface.
415
- */
416
- async function loadRuntimeDeps(): Promise<string[]> {
417
- const here = fileURLToPath(import.meta.url);
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
- async function walkUpToPackageRoot(from: string): Promise<string | null> {
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 = expandedName === invokeTarget || expandedName === runTarget;
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}': no ephemeral resource for invoke target '${invokeTarget}'`,
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
- throw new Error(`Ephemeral resource '${name}' is not invocable`);
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
- return entry.instance.invoke(invokeInputs);
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}': no ephemeral resource for run target '${runTarget}'`,
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
- throw new Error(`Ephemeral resource '${name}' is not runnable`);
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). Applications
310
- // have no variables/secrets fields those are a Library-only contract, populated
311
- // by importers, not by the root manifest itself.
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
  /**
@@ -1,6 +0,0 @@
1
- {
2
- "generated": "scripts/generate-runtime-deps.mjs",
3
- "names": [
4
- "@telorun/sdk"
5
- ]
6
- }