@telorun/kernel 0.4.1 → 0.5.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 (60) hide show
  1. package/dist/controller-loader.d.ts +19 -20
  2. package/dist/controller-loader.d.ts.map +1 -1
  3. package/dist/controller-loader.js +67 -247
  4. package/dist/controller-loader.js.map +1 -1
  5. package/dist/controller-loaders/napi-loader.d.ts +27 -0
  6. package/dist/controller-loaders/napi-loader.d.ts.map +1 -0
  7. package/dist/controller-loaders/napi-loader.js +158 -0
  8. package/dist/controller-loaders/napi-loader.js.map +1 -0
  9. package/dist/controller-loaders/npm-loader.d.ts +20 -0
  10. package/dist/controller-loaders/npm-loader.d.ts.map +1 -0
  11. package/dist/controller-loaders/npm-loader.js +256 -0
  12. package/dist/controller-loaders/npm-loader.js.map +1 -0
  13. package/dist/controller-registry.d.ts +30 -20
  14. package/dist/controller-registry.d.ts.map +1 -1
  15. package/dist/controller-registry.js +50 -99
  16. package/dist/controller-registry.js.map +1 -1
  17. package/dist/controllers/module/import-controller.d.ts +11 -0
  18. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  19. package/dist/controllers/module/import-controller.js +28 -1
  20. package/dist/controllers/module/import-controller.js.map +1 -1
  21. package/dist/controllers/resource-definition/abstract-controller.d.ts +35 -0
  22. package/dist/controllers/resource-definition/abstract-controller.d.ts.map +1 -0
  23. package/dist/controllers/resource-definition/abstract-controller.js +34 -0
  24. package/dist/controllers/resource-definition/abstract-controller.js.map +1 -0
  25. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  26. package/dist/controllers/resource-definition/resource-definition-controller.js +1 -1
  27. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  28. package/dist/kernel.d.ts +1 -1
  29. package/dist/kernel.d.ts.map +1 -1
  30. package/dist/kernel.js +35 -14
  31. package/dist/kernel.js.map +1 -1
  32. package/dist/manifest-schemas.d.ts +50 -0
  33. package/dist/manifest-schemas.d.ts.map +1 -1
  34. package/dist/manifest-schemas.js +31 -0
  35. package/dist/manifest-schemas.js.map +1 -1
  36. package/dist/module-context.d.ts +11 -1
  37. package/dist/module-context.d.ts.map +1 -1
  38. package/dist/module-context.js +6 -0
  39. package/dist/module-context.js.map +1 -1
  40. package/dist/resource-context.d.ts +2 -1
  41. package/dist/resource-context.d.ts.map +1 -1
  42. package/dist/resource-context.js +6 -1
  43. package/dist/resource-context.js.map +1 -1
  44. package/dist/runtime-registry.d.ts +50 -0
  45. package/dist/runtime-registry.d.ts.map +1 -0
  46. package/dist/runtime-registry.js +140 -0
  47. package/dist/runtime-registry.js.map +1 -0
  48. package/package.json +3 -3
  49. package/src/controller-loader.ts +77 -273
  50. package/src/controller-loaders/napi-loader.ts +191 -0
  51. package/src/controller-loaders/npm-loader.ts +285 -0
  52. package/src/controller-registry.ts +66 -129
  53. package/src/controllers/module/import-controller.ts +30 -1
  54. package/src/controllers/resource-definition/abstract-controller.ts +56 -0
  55. package/src/controllers/resource-definition/resource-definition-controller.ts +1 -0
  56. package/src/kernel.ts +43 -13
  57. package/src/manifest-schemas.ts +33 -0
  58. package/src/module-context.ts +22 -1
  59. package/src/resource-context.ts +8 -1
  60. package/src/runtime-registry.ts +170 -0
package/src/kernel.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AnalysisRegistry, DEFAULT_MANIFEST_FILENAME, isModuleKind, Loader, StaticAnalyzer } from "@telorun/analyzer";
2
2
  import {
3
3
  ControllerContext,
4
+ ControllerPolicy,
4
5
  Kernel as IKernel,
5
6
  type LoadOptions,
6
7
  ResourceContext,
@@ -23,8 +24,22 @@ import { EventStream } from "./event-stream.js";
23
24
  import { EventBus } from "./events.js";
24
25
  import { LocalFileAdapter } from "./manifest-adapters/local-file-adapter.js";
25
26
  import { ResourceContextImpl } from "./resource-context.js";
27
+ import { policyFingerprint } from "./runtime-registry.js";
26
28
  import { SchemaValidator } from "./schema-validator.js";
27
29
 
30
+ /** Walks up the EvaluationContext parent chain to the nearest enclosing
31
+ * ModuleContext and returns its controller policy (or undefined). Used to
32
+ * pick the right cache entry when a kind has been loaded under multiple
33
+ * runtime selections. */
34
+ function findEnclosingPolicy(ctx: IEvaluationContext): ControllerPolicy | undefined {
35
+ let cur: IEvaluationContext | undefined = ctx;
36
+ while (cur) {
37
+ if (cur instanceof ModuleContext) return cur.getControllerPolicy();
38
+ cur = cur.parent;
39
+ }
40
+ return undefined;
41
+ }
42
+
28
43
  export interface KernelOptions {
29
44
  stdin?: NodeJS.ReadableStream;
30
45
  stdout?: NodeJS.WritableStream;
@@ -83,8 +98,13 @@ export class Kernel implements IKernel {
83
98
  moduleName: string,
84
99
  kindName: string,
85
100
  controllerInstance: any,
101
+ fingerprint?: string,
86
102
  ): Promise<void> {
87
- this.controllers.registerController(`${moduleName}.${kindName}`, controllerInstance);
103
+ this.controllers.registerController(
104
+ `${moduleName}.${kindName}`,
105
+ controllerInstance,
106
+ fingerprint,
107
+ );
88
108
  await controllerInstance.register?.(this.createControllerContext(`${moduleName}.${kindName}`));
89
109
  }
90
110
 
@@ -148,6 +168,10 @@ export class Kernel implements IKernel {
148
168
  "Telo.Definition",
149
169
  await import("./controllers/resource-definition/resource-definition-controller.js"),
150
170
  );
171
+ this.controllers.registerController(
172
+ "Telo.Abstract",
173
+ await import("./controllers/resource-definition/abstract-controller.js"),
174
+ );
151
175
  const moduleController = await import("./controllers/module/module-controller.js");
152
176
  this.controllers.registerController("Telo.Application", moduleController);
153
177
  this.controllers.registerController("Telo.Library", moduleController);
@@ -260,10 +284,12 @@ export class Kernel implements IKernel {
260
284
  * Phase 2: Start - Initialize resources
261
285
  */
262
286
  async start(): Promise<void> {
263
- // Call controller register hooks first (before any initialization)
264
- for (const kind of this.controllers.getKinds()) {
287
+ // Call register hooks for controllers actually loaded at this point (built-ins).
288
+ // User-module kinds load their controllers during Phase 3 (Telo.Definition.init),
289
+ // and registerController() fires their register hook there.
290
+ for (const kind of this.controllers.getControllerKinds()) {
265
291
  const controller = this.controllers.getController(kind);
266
- if (controller?.register) {
292
+ if (controller.register) {
267
293
  await controller.register(this.createControllerContext(`controller:${kind}`));
268
294
  }
269
295
  }
@@ -475,12 +501,13 @@ export class Kernel implements IKernel {
475
501
  // resolveKind() throws with a clear message if the alias or kind is not found.
476
502
  const resolvedKind = this.rootContext.resolveKind(kind);
477
503
 
478
- const controller = this.controllers.getControllerOrUndefined(resolvedKind);
504
+ const fingerprint = policyFingerprint(findEnclosingPolicy(evalContext));
505
+ const controller = this.controllers.getControllerOrUndefined(resolvedKind, fingerprint);
479
506
  if (!controller) {
480
507
  const kindInfo =
481
508
  resolvedKind !== kind ? `'${kind}' (resolved to '${resolvedKind}')` : `'${kind}'`;
482
509
  throw new Error(
483
- `No controller registered for kind ${kindInfo}, known controllers are: ${this.controllers.getKinds().join(", ")}`,
510
+ `No controller registered for kind ${kindInfo} (runtime fingerprint "${fingerprint}"), known controllers are: ${this.controllers.getKinds().join(", ")}`,
484
511
  );
485
512
  }
486
513
 
@@ -539,14 +566,17 @@ export class Kernel implements IKernel {
539
566
 
540
567
  if (!runtime.length) return { instance, ctx };
541
568
 
542
- const wrapped: ResourceInstance = {
543
- ...instance,
544
- invoke: async (inputs: any) => {
545
- const expanded = evalContext.expandPaths(inputs as Record<string, unknown>, runtime);
546
- return instance.invoke!(expanded);
547
- },
569
+ // Override invoke in-place so all lifecycle methods (init/invoke/teardown/snapshot)
570
+ // share the same `this`. A wrapper object would split identity: state mutated by
571
+ // init() on the wrapper would be invisible to the original invoke(), which still
572
+ // runs with `this === instance`. Mutating in place also preserves the prototype
573
+ // chain — class-declared methods remain reachable.
574
+ const originalInvoke = instance.invoke!.bind(instance);
575
+ instance.invoke = async (inputs: any) => {
576
+ const expanded = evalContext.expandPaths(inputs as Record<string, unknown>, runtime);
577
+ return originalInvoke(expanded);
548
578
  };
549
- return { instance: wrapped, ctx };
579
+ return { instance, ctx };
550
580
  }
551
581
 
552
582
  /**
@@ -49,6 +49,16 @@ const throwsSchema = {
49
49
  },
50
50
  };
51
51
 
52
+ /** Alias-form pattern for `extends` values: "<Alias>.<AbstractName>".
53
+ * Resolved against the declaring file's `Telo.Import` aliases — identical to how
54
+ * kind prefixes work (e.g. `kind: Http.Api` resolves `Http` via the importer's
55
+ * alias registration). Identity form (`std/mod#Name`) is intentionally not
56
+ * accepted: aliases carry the module version via their `Telo.Import` source,
57
+ * which canonical module names can't.
58
+ * - Alias: PascalCase (first letter uppercase)
59
+ * - Name: PascalCase */
60
+ const EXTENDS_ALIAS_PATTERN = "^[A-Z][A-Za-z0-9_]*\\.[A-Z][A-Za-z0-9_]*$";
61
+
52
62
  const baseDefinition = {
53
63
  type: "object",
54
64
  required: ["kind", "metadata"],
@@ -56,6 +66,7 @@ const baseDefinition = {
56
66
  kind: { const: "Telo.Definition" },
57
67
  metadata: metadataSchema,
58
68
  capability: { type: "string" },
69
+ extends: { type: "string", pattern: EXTENDS_ALIAS_PATTERN },
59
70
  schema: { type: "object", additionalProperties: true },
60
71
  controllers: { type: "array", items: { type: "string" } },
61
72
  throws: throwsSchema,
@@ -114,11 +125,33 @@ export const ResourceDefinitionSchema = {
114
125
  ],
115
126
  };
116
127
 
128
+ /** Schema for `kind: Telo.Abstract`. Library-declared abstracts are type blueprints —
129
+ * they may carry an optional `capability` (lifecycle inherited by implementations)
130
+ * and an optional `schema` (shared base for implementations). `controllers` and `throws`
131
+ * are forbidden (no runtime implementation; throws lives on concrete definitions).
132
+ * Other fields are permitted for forward compatibility with typed-abstracts work
133
+ * (inputType, outputType, …) — Telo.Abstract is an extension point by design. */
134
+ export const ResourceAbstractSchema = {
135
+ type: "object",
136
+ required: ["kind", "metadata"],
137
+ properties: {
138
+ kind: { const: "Telo.Abstract" },
139
+ metadata: metadataSchema,
140
+ capability: { type: "string" },
141
+ schema: { type: "object", additionalProperties: true },
142
+ },
143
+ not: {
144
+ anyOf: [{ required: ["controllers"] }, { required: ["throws"] }],
145
+ },
146
+ additionalProperties: true,
147
+ };
148
+
117
149
  const ajv = new Ajv({ allErrors: true, strict: false });
118
150
  addFormats.default(ajv);
119
151
 
120
152
  export const validateRuntimeResource = ajv.compile(RuntimeResourceSchema);
121
153
  export const validateResourceDefinition = ajv.compile(ResourceDefinitionSchema);
154
+ export const validateResourceAbstract = ajv.compile(ResourceAbstractSchema);
122
155
 
123
156
  export function formatAjvErrors(errors: any[] | null | undefined): string {
124
157
  if (!errors || errors.length === 0) return "Unknown schema error";
@@ -1,4 +1,8 @@
1
- import type { Invocable, ModuleContext as IModuleContext } from "@telorun/sdk";
1
+ import type {
2
+ ControllerPolicy,
3
+ Invocable,
4
+ ModuleContext as IModuleContext,
5
+ } from "@telorun/sdk";
2
6
  import type { EmitEvent, InstanceFactory } from "@telorun/sdk";
3
7
  import { EvaluationContext } from "./evaluation-context.js";
4
8
 
@@ -55,6 +59,15 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
55
59
  /** Maps import alias → allowed kind names. Absent entry = unrestricted (e.g. Kernel). */
56
60
  private readonly importedKinds = new Map<string, Set<string>>();
57
61
 
62
+ /**
63
+ * Resolved controller-selection policy for this module's `Telo.Definition`s.
64
+ * Stamped by the parent `Telo.Import` controller from the import's `runtime:`
65
+ * field; read by `Telo.Definition.init` (via `ResourceContext.getControllerPolicy`)
66
+ * when invoking `ControllerLoader.load`. `undefined` means "no policy set" —
67
+ * loader treats it as `auto`.
68
+ */
69
+ private _controllerPolicy: ControllerPolicy | undefined;
70
+
58
71
  constructor(
59
72
  source: string,
60
73
  variables: Record<string, unknown> = {},
@@ -103,6 +116,14 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
103
116
  this._rebuildContext();
104
117
  }
105
118
 
119
+ setControllerPolicy(policy: ControllerPolicy | undefined): void {
120
+ this._controllerPolicy = policy;
121
+ }
122
+
123
+ getControllerPolicy(): ControllerPolicy | undefined {
124
+ return this._controllerPolicy;
125
+ }
126
+
106
127
  protected override onResourceSnapshotted(name: string, snap: Record<string, unknown>): void {
107
128
  this.setResource(name, snap);
108
129
  }
@@ -6,6 +6,7 @@ import {
6
6
  RuntimeError,
7
7
  RuntimeResource,
8
8
  isCompiledValue,
9
+ type ControllerPolicy,
9
10
  type EvaluationContext as IEvaluationContext,
10
11
  type LoadOptions,
11
12
  type ModuleContext,
@@ -17,6 +18,7 @@ import AjvModule from "ajv";
17
18
  import addFormats from "ajv-formats";
18
19
  import { Kernel } from "./kernel.js";
19
20
  import { formatAjvErrors } from "./manifest-schemas.js";
21
+ import { policyFingerprint } from "./runtime-registry.js";
20
22
  import { SchemaValidator } from "./schema-validator.js";
21
23
 
22
24
  const Ajv = AjvModule.default ?? AjvModule;
@@ -261,13 +263,18 @@ export class ResourceContextImpl implements ResourceContext {
261
263
  kindName: string,
262
264
  controllerInstance: any,
263
265
  ): Promise<void> {
264
- await this.kernel.registerController(moduleName, kindName, controllerInstance);
266
+ const fingerprint = policyFingerprint(this.moduleContext.getControllerPolicy());
267
+ await this.kernel.registerController(moduleName, kindName, controllerInstance, fingerprint);
265
268
  }
266
269
 
267
270
  registerDefinition(def: any) {
268
271
  this.kernel.registerResourceDefinition(def);
269
272
  }
270
273
 
274
+ getControllerPolicy(): ControllerPolicy | undefined {
275
+ return this.moduleContext.getControllerPolicy();
276
+ }
277
+
271
278
  on(event: string, handler: (payload?: any) => void | Promise<void>): void {
272
279
  this.kernel.on(event, handler);
273
280
  }
@@ -0,0 +1,170 @@
1
+ import type { ControllerPolicy } from "@telorun/sdk";
2
+ import { RuntimeError } from "@telorun/sdk";
3
+
4
+ export type { ControllerPolicy } from "@telorun/sdk";
5
+
6
+ /**
7
+ * The PURL-type prefix the kernel itself runs (i.e. "no FFI" controllers
8
+ * for this kernel). For the Node.js kernel, that's `pkg:npm`. A future
9
+ * Rust kernel reports `pkg:cargo` here.
10
+ */
11
+ export const KERNEL_NATIVE_PURL_TYPE = "pkg:npm";
12
+
13
+ /**
14
+ * Wildcard sentinel inside a resolved `ControllerPolicy.load`. Means
15
+ * "all remaining controllers in declaration order, minus PURL types
16
+ * already listed earlier in the same policy." May appear at most once.
17
+ */
18
+ export const POLICY_WILDCARD = "*";
19
+
20
+ /**
21
+ * Maps user-facing runtime labels to PURL-type prefixes. The user-facing
22
+ * label is the implementation directory name a contributor sees at
23
+ * `modules/<name>/<label>/`.
24
+ */
25
+ const LABEL_TO_PURL_TYPE: Readonly<Record<string, string>> = {
26
+ nodejs: "pkg:npm",
27
+ rust: "pkg:cargo",
28
+ };
29
+
30
+ /**
31
+ * Labels that only make sense as a single value (`runtime: auto`,
32
+ * `runtime: native`) — they describe a whole policy, not one slot in a
33
+ * list. `any` is also reserved but is allowed as the final list entry,
34
+ * so it is handled separately in `normalizeRuntime`.
35
+ */
36
+ const SINGLE_ONLY_LABELS = new Set(["auto", "native"]);
37
+
38
+ /**
39
+ * Default policy for missing `runtime:` field — equivalent to `runtime: auto`.
40
+ * Tries kernel-native first, then any other declared controller in declaration order.
41
+ */
42
+ export const DEFAULT_POLICY: ControllerPolicy = {
43
+ load: [KERNEL_NATIVE_PURL_TYPE, POLICY_WILDCARD],
44
+ };
45
+
46
+ /**
47
+ * Resolve a `runtime:` field value (string, array, or undefined) into a
48
+ * canonical `ControllerPolicy`. Throws `ERR_RUNTIME_INVALID` on:
49
+ * - empty array
50
+ * - unknown label
51
+ * - `any` anywhere but the final list entry
52
+ * - duplicate label
53
+ */
54
+ export function normalizeRuntime(value: string | ReadonlyArray<string> | undefined): ControllerPolicy {
55
+ if (value === undefined) {
56
+ return DEFAULT_POLICY;
57
+ }
58
+ if (typeof value === "string") {
59
+ return resolveSingle(value);
60
+ }
61
+ if (!Array.isArray(value)) {
62
+ throw new RuntimeError(
63
+ "ERR_RUNTIME_INVALID",
64
+ `runtime must be a string or array of strings, got ${typeof value}`,
65
+ );
66
+ }
67
+ if (value.length === 0) {
68
+ throw new RuntimeError(
69
+ "ERR_RUNTIME_INVALID",
70
+ "runtime: [] has no useful meaning. Omit the field for `auto`, or list at least one runtime label.",
71
+ );
72
+ }
73
+ const load: string[] = [];
74
+ for (let i = 0; i < value.length; i++) {
75
+ const entry = value[i];
76
+ if (typeof entry !== "string") {
77
+ throw new RuntimeError(
78
+ "ERR_RUNTIME_INVALID",
79
+ `runtime list entries must be strings, got ${typeof entry}`,
80
+ );
81
+ }
82
+ if (entry === "any") {
83
+ if (i !== value.length - 1) {
84
+ throw new RuntimeError(
85
+ "ERR_RUNTIME_INVALID",
86
+ "runtime: 'any' may only appear as the last entry in the list",
87
+ );
88
+ }
89
+ load.push(POLICY_WILDCARD);
90
+ continue;
91
+ }
92
+ const purlType = labelToPurlType(entry);
93
+ if (load.includes(purlType)) {
94
+ throw new RuntimeError(
95
+ "ERR_RUNTIME_INVALID",
96
+ `runtime: '${entry}' listed twice (resolves to ${purlType})`,
97
+ );
98
+ }
99
+ load.push(purlType);
100
+ }
101
+ return { load };
102
+ }
103
+
104
+ function resolveSingle(label: string): ControllerPolicy {
105
+ if (label === "auto") {
106
+ return DEFAULT_POLICY;
107
+ }
108
+ if (label === "native") {
109
+ return { load: [KERNEL_NATIVE_PURL_TYPE] };
110
+ }
111
+ if (label === "any") {
112
+ return { load: [POLICY_WILDCARD] };
113
+ }
114
+ return { load: [labelToPurlType(label)] };
115
+ }
116
+
117
+ function labelToPurlType(label: string): string {
118
+ if (SINGLE_ONLY_LABELS.has(label)) {
119
+ throw new RuntimeError(
120
+ "ERR_RUNTIME_INVALID",
121
+ `runtime label '${label}' describes a whole policy and is only valid as a single value, not inside a list`,
122
+ );
123
+ }
124
+ const purlType = LABEL_TO_PURL_TYPE[label];
125
+ if (!purlType) {
126
+ const known = Object.keys(LABEL_TO_PURL_TYPE).concat(["auto", "native", "any"]).sort().join(", ");
127
+ throw new RuntimeError(
128
+ "ERR_RUNTIME_INVALID",
129
+ `Unknown runtime label '${label}'. Known: ${known}`,
130
+ );
131
+ }
132
+ return purlType;
133
+ }
134
+
135
+ /**
136
+ * Stable short hash of a resolved policy, for use as a registry cache key
137
+ * suffix. Two imports with the same resolved policy share a cached
138
+ * controller; divergent policies get separate entries.
139
+ *
140
+ * Both `undefined` (no policy stamped) and any policy structurally equal to
141
+ * `DEFAULT_POLICY` (`runtime: auto`, missing `runtime:`, or any list that
142
+ * normalizes to the auto shape) collapse to the `"default"` fingerprint —
143
+ * the plan's contract is that "missing" is sugar for "auto", so they must
144
+ * share a cache entry.
145
+ */
146
+ export function policyFingerprint(policy: ControllerPolicy | undefined): string {
147
+ if (!policy || isDefaultPolicy(policy)) {
148
+ return "default";
149
+ }
150
+ return policy.load.join(",");
151
+ }
152
+
153
+ /**
154
+ * Structural equality check against `DEFAULT_POLICY`. Used at policy-stamp
155
+ * time (import-controller) to skip stamping when the resolved policy is the
156
+ * canonical default — `runtime: auto`, `runtime: [nodejs, any]`, etc. all
157
+ * normalize to the same shape and should be observationally identical to a
158
+ * plain omitted `runtime:` field, both at the fingerprint level (handled by
159
+ * `policyFingerprint`) and at the policy-presence level.
160
+ */
161
+ export function isDefaultPolicy(policy: ControllerPolicy): boolean {
162
+ if (policy === DEFAULT_POLICY) return true;
163
+ const a = policy.load;
164
+ const b = DEFAULT_POLICY.load;
165
+ if (a.length !== b.length) return false;
166
+ for (let i = 0; i < a.length; i++) {
167
+ if (a[i] !== b[i]) return false;
168
+ }
169
+ return true;
170
+ }