@telorun/kernel 0.2.6 → 0.2.8

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 (57) hide show
  1. package/README.md +100 -0
  2. package/dist/controller-registry.js +2 -2
  3. package/dist/controller-registry.js.map +1 -1
  4. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  5. package/dist/controllers/module/import-controller.js +18 -9
  6. package/dist/controllers/module/import-controller.js.map +1 -1
  7. package/dist/evaluation-context.d.ts +76 -38
  8. package/dist/evaluation-context.d.ts.map +1 -1
  9. package/dist/evaluation-context.js +254 -89
  10. package/dist/evaluation-context.js.map +1 -1
  11. package/dist/execution-context.d.ts +1 -1
  12. package/dist/execution-context.d.ts.map +1 -1
  13. package/dist/execution-context.js +1 -1
  14. package/dist/execution-context.js.map +1 -1
  15. package/dist/index.d.ts +4 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/kernel.d.ts +25 -2
  20. package/dist/kernel.d.ts.map +1 -1
  21. package/dist/kernel.js +100 -27
  22. package/dist/kernel.js.map +1 -1
  23. package/dist/manifest-adapters/local-file-adapter.d.ts +1 -1
  24. package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -1
  25. package/dist/manifest-adapters/local-file-adapter.js +2 -1
  26. package/dist/manifest-adapters/local-file-adapter.js.map +1 -1
  27. package/dist/manifest-adapters/manifest-adapter.d.ts +1 -1
  28. package/dist/module-context.d.ts +28 -5
  29. package/dist/module-context.d.ts.map +1 -1
  30. package/dist/module-context.js +107 -4
  31. package/dist/module-context.js.map +1 -1
  32. package/dist/resource-context.d.ts +14 -10
  33. package/dist/resource-context.d.ts.map +1 -1
  34. package/dist/resource-context.js +44 -8
  35. package/dist/resource-context.js.map +1 -1
  36. package/dist/schema-valiator.d.ts +7 -1
  37. package/dist/schema-valiator.d.ts.map +1 -1
  38. package/dist/schema-valiator.js +75 -4
  39. package/dist/schema-valiator.js.map +1 -1
  40. package/dist/schema-validator.d.ts +15 -0
  41. package/dist/schema-validator.d.ts.map +1 -0
  42. package/dist/schema-validator.js +127 -0
  43. package/dist/schema-validator.js.map +1 -0
  44. package/package.json +21 -10
  45. package/src/controller-registry.ts +2 -2
  46. package/src/controllers/module/import-controller.ts +27 -11
  47. package/src/evaluation-context.ts +490 -0
  48. package/src/execution-context.ts +21 -0
  49. package/src/index.ts +4 -1
  50. package/src/kernel.ts +144 -38
  51. package/src/manifest-adapters/local-file-adapter.ts +2 -2
  52. package/src/manifest-adapters/manifest-adapter.ts +1 -1
  53. package/src/module-context.ts +211 -0
  54. package/src/resource-context.ts +67 -12
  55. package/src/schema-validator.ts +146 -0
  56. package/src/loader.ts +0 -134
  57. package/src/schema-valiator.ts +0 -68
package/src/kernel.ts CHANGED
@@ -1,23 +1,35 @@
1
- import { AnalysisRegistry, StaticAnalyzer } from "@telorun/analyzer";
1
+ import { AnalysisRegistry, DEFAULT_MANIFEST_FILENAME, Loader, StaticAnalyzer } from "@telorun/analyzer";
2
2
  import {
3
- ControllerContext,
4
- Kernel as IKernel,
5
- ModuleContext,
6
- ResourceContext,
7
- ResourceDefinition,
8
- ResourceInstance,
9
- ResourceManifest,
10
- RuntimeError,
11
- RuntimeEvent,
12
- isCompiledValue,
3
+ ControllerContext,
4
+ Kernel as IKernel,
5
+ ResourceContext,
6
+ ResourceDefinition,
7
+ ResourceInstance,
8
+ ResourceManifest,
9
+ RuntimeError,
10
+ RuntimeEvent,
11
+ isCompiledValue,
12
+ type EvaluationContext as IEvaluationContext,
13
+ type ModuleContext as IModuleContext,
14
+ type ParsedArgs,
13
15
  } from "@telorun/sdk";
16
+ import { ModuleContext } from "./module-context.js";
14
17
  import * as path from "path";
18
+ import { parseArgs } from "util";
15
19
  import { ControllerRegistry } from "./controller-registry.js";
16
20
  import { EventStream } from "./event-stream.js";
17
21
  import { EventBus } from "./events.js";
18
- import { Loader } from "./loader.js";
22
+ import { LocalFileAdapter } from "./manifest-adapters/local-file-adapter.js";
19
23
  import { ResourceContextImpl } from "./resource-context.js";
20
- import { SchemaValidator } from "./schema-valiator.js";
24
+ import { SchemaValidator } from "./schema-validator.js";
25
+
26
+ export interface KernelOptions {
27
+ stdin?: NodeJS.ReadableStream;
28
+ stdout?: NodeJS.WritableStream;
29
+ stderr?: NodeJS.WritableStream;
30
+ env?: Record<string, string | undefined>;
31
+ argv?: string[];
32
+ }
21
33
 
22
34
  /**
23
35
  * Kernel: Central orchestrator managing lifecycle and message bus
@@ -27,26 +39,30 @@ export class Kernel implements IKernel {
27
39
  private readonly loader = new Loader();
28
40
  private readonly analyzer = new StaticAnalyzer();
29
41
  private readonly registry = new AnalysisRegistry();
30
- // private manifests: ManifestRegistry = new ManifestRegistry();
31
42
  private controllers: ControllerRegistry = new ControllerRegistry();
32
43
  private eventBus: EventBus = new EventBus();
33
44
  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
45
 
41
46
  private holdCount = 0;
42
47
  private idleResolvers: Array<() => void> = [];
43
48
  private _exitCode = 0;
44
- // private bootContextRegistry = new BootContextRegistry();
45
49
  private readonly sharedSchemaValidator = new SchemaValidator();
46
50
  private rootContext!: ModuleContext;
47
51
  private staticManifests: ResourceManifest[] = [];
48
52
 
49
- constructor() {
53
+ readonly stdin: NodeJS.ReadableStream;
54
+ readonly stdout: NodeJS.WritableStream;
55
+ readonly stderr: NodeJS.WritableStream;
56
+ readonly env: Record<string, string | undefined>;
57
+ readonly argv: string[];
58
+
59
+ constructor(options: KernelOptions = {}) {
60
+ this.stdin = options.stdin ?? process.stdin;
61
+ this.stdout = options.stdout ?? process.stdout;
62
+ this.stderr = options.stderr ?? process.stderr;
63
+ this.env = options.env ?? process.env;
64
+ this.argv = options.argv ?? [];
65
+ this.loader.register(new LocalFileAdapter());
50
66
  this.setupEventStreaming();
51
67
  }
52
68
 
@@ -125,11 +141,8 @@ export class Kernel implements IKernel {
125
141
  * Load from runtime configuration file
126
142
  */
127
143
  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
- );
144
+ const resolvedUrl = new URL(runtimeYamlPath, `file://${process.cwd()}/`).href;
145
+ const sourceUrl = await this.loader.resolveEntryPoint(resolvedUrl);
133
146
  this.rootContext = new ModuleContext(
134
147
  sourceUrl,
135
148
  {},
@@ -138,7 +151,7 @@ export class Kernel implements IKernel {
138
151
  [],
139
152
  this._createInstance.bind(this),
140
153
  (event, payload) => this.eventBus.emit(event, payload),
141
- process.env,
154
+ this.env,
142
155
  );
143
156
  // Initialize built-in Runtime definitions first
144
157
  await this.loadBuiltinDefinitions();
@@ -205,7 +218,7 @@ export class Kernel implements IKernel {
205
218
  * @deprecated Use loadFromConfig instead
206
219
  */
207
220
  async loadDirectory(dirPath: string): Promise<void> {
208
- const configYamlPath = path.join(dirPath, "module.yaml");
221
+ const configYamlPath = path.join(dirPath, DEFAULT_MANIFEST_FILENAME);
209
222
 
210
223
  await this.loadFromConfig(configYamlPath);
211
224
  }
@@ -350,18 +363,68 @@ export class Kernel implements IKernel {
350
363
  };
351
364
  }
352
365
 
366
+ /**
367
+ * Walk up the parent chain from a given evaluation context to find the nearest
368
+ * ModuleContext ancestor. Falls back to rootContext if none found.
369
+ */
370
+ private findModuleContext(ctx: IEvaluationContext): IModuleContext {
371
+ let current: IEvaluationContext | undefined = ctx;
372
+ while (current) {
373
+ if (current instanceof ModuleContext) return current;
374
+ current = current.parent;
375
+ }
376
+ return this.rootContext;
377
+ }
378
+
353
379
  private createResourceContext(
354
- moduleContext: ModuleContext,
380
+ moduleContext: IModuleContext,
355
381
  resource: ResourceManifest,
382
+ args?: ParsedArgs,
356
383
  ): ResourceContext {
357
384
  return new ResourceContextImpl(
358
385
  this,
359
386
  moduleContext,
360
387
  resource.metadata,
361
388
  this.sharedSchemaValidator,
389
+ this.stdin,
390
+ this.stdout,
391
+ this.stderr,
392
+ args,
362
393
  );
363
394
  }
364
395
 
396
+ /**
397
+ * Parse kernel.argv using a controller's args spec (if present).
398
+ * If the controller exports no args spec, does a generic parse.
399
+ */
400
+ private parseArgsForController(controller: any): ParsedArgs {
401
+ if (this.argv.length === 0) return { _: [] };
402
+
403
+ const argSpec = controller.args;
404
+ if (argSpec) {
405
+ const options: Record<string, { type: "string" | "boolean"; short?: string }> = {};
406
+ for (const [name, def] of Object.entries(argSpec) as [string, any][]) {
407
+ options[name] = { type: def.type ?? "string" };
408
+ if (def.alias) options[name].short = def.alias;
409
+ }
410
+ const { values, positionals } = parseArgs({
411
+ args: this.argv,
412
+ options,
413
+ allowPositionals: true,
414
+ strict: false,
415
+ });
416
+ return { ...values, _: positionals } as ParsedArgs;
417
+ }
418
+
419
+ // Generic parse: no spec, best-effort
420
+ const { values, positionals } = parseArgs({
421
+ args: this.argv,
422
+ allowPositionals: true,
423
+ strict: false,
424
+ });
425
+ return { ...values, _: positionals } as ParsedArgs;
426
+ }
427
+
365
428
  /**
366
429
  * Create phase only: resolves the controller, validates the schema, and calls
367
430
  * controller.create(). Returns { instance, ctx } so initializeResources can
@@ -369,7 +432,7 @@ export class Kernel implements IKernel {
369
432
  * is not yet registered (retry signal).
370
433
  */
371
434
  private async _createInstance(
372
- evalContext: ModuleContext,
435
+ evalContext: IEvaluationContext,
373
436
  resource: ResourceManifest,
374
437
  ): Promise<{ instance: ResourceInstance; ctx: ResourceContext } | null> {
375
438
  const kind = resource.kind;
@@ -434,7 +497,9 @@ export class Kernel implements IKernel {
434
497
  ) as ResourceManifest)
435
498
  : resource;
436
499
 
437
- const ctx = this.createResourceContext(evalContext, processedResource);
500
+ const parsedArgs = this.parseArgsForController(controller);
501
+ const moduleCtx = this.findModuleContext(evalContext);
502
+ const ctx = this.createResourceContext(moduleCtx, processedResource, parsedArgs);
438
503
  const instance = await controller.create(processedResource, ctx);
439
504
  if (!instance) return null;
440
505
 
@@ -529,20 +594,61 @@ function placeholderForSchema(schema: Record<string, unknown>): unknown {
529
594
  }
530
595
  }
531
596
 
597
+ /** Resolve a `$ref` (only `#/$defs/...` form) against the root schema. */
598
+ function resolveSchemaRef(
599
+ schema: Record<string, unknown>,
600
+ root: Record<string, unknown>,
601
+ ): Record<string, unknown> {
602
+ if (schema.$ref && typeof schema.$ref === "string" && (schema.$ref as string).startsWith("#/$defs/")) {
603
+ const defName = (schema.$ref as string).slice("#/$defs/".length);
604
+ const defs = root.$defs as Record<string, Record<string, unknown>> | undefined;
605
+ const resolved = defs?.[defName];
606
+ if (resolved) return resolved;
607
+ }
608
+ return schema;
609
+ }
610
+
611
+ /** Collect property schemas from top-level `properties` and all `oneOf`/`anyOf` sub-schemas. */
612
+ function collectSchemaProperties(
613
+ schema: Record<string, unknown>,
614
+ ): Record<string, Record<string, unknown>> {
615
+ const props: Record<string, Record<string, unknown>> = {
616
+ ...((schema.properties ?? {}) as Record<string, Record<string, unknown>>),
617
+ };
618
+ for (const sub of (schema.oneOf ?? schema.anyOf ?? []) as Record<string, unknown>[]) {
619
+ if (sub && typeof sub === "object" && sub.properties) {
620
+ for (const [k, v] of Object.entries(sub.properties as Record<string, Record<string, unknown>>)) {
621
+ if (!(k in props)) props[k] = v;
622
+ }
623
+ }
624
+ }
625
+ return props;
626
+ }
627
+
532
628
  /** Replaces CompiledValue wrappers with schema-appropriate placeholders for schema validation.
533
629
  * Template strings were compiled from YAML at load time; this restores a shape
534
630
  * that AJV can validate without evaluating expressions. */
535
- function stripCompiledValues(v: unknown, schema: Record<string, unknown> = {}): unknown {
536
- if (isCompiledValue(v)) return placeholderForSchema(schema);
631
+ function stripCompiledValues(
632
+ v: unknown,
633
+ schema: Record<string, unknown> = {},
634
+ rootSchema?: Record<string, unknown>,
635
+ ): unknown {
636
+ const root = rootSchema ?? schema;
637
+ const resolved = resolveSchemaRef(schema, root);
638
+
639
+ if (isCompiledValue(v)) return placeholderForSchema(resolved);
537
640
  if (Array.isArray(v)) {
538
- const itemSchema = (schema.items ?? {}) as Record<string, unknown>;
539
- return v.map((item) => stripCompiledValues(item, itemSchema));
641
+ const itemSchema = resolveSchemaRef(
642
+ (resolved.items ?? {}) as Record<string, unknown>,
643
+ root,
644
+ );
645
+ return v.map((item) => stripCompiledValues(item, itemSchema, root));
540
646
  }
541
647
  if (v !== null && typeof v === "object") {
542
- const props = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
648
+ const props = collectSchemaProperties(resolved);
543
649
  const out: Record<string, unknown> = {};
544
650
  for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
545
- out[k] = stripCompiledValues(val, props[k] ?? {});
651
+ out[k] = stripCompiledValues(val, props[k] ?? {}, root);
546
652
  }
547
653
  return out;
548
654
  }
@@ -1,4 +1,4 @@
1
- import type { ManifestAdapter } from "@telorun/analyzer";
1
+ import { DEFAULT_MANIFEST_FILENAME, type ManifestAdapter } from "@telorun/analyzer";
2
2
  import * as fs from "fs/promises";
3
3
  import * as path from "path";
4
4
 
@@ -19,7 +19,7 @@ export class LocalFileAdapter implements ManifestAdapter {
19
19
  : pathOrUrl;
20
20
  const resolvedPath = path.resolve(normalizedPath);
21
21
  const stat = await fs.stat(resolvedPath);
22
- const filePath = stat.isDirectory() ? path.join(resolvedPath, "module.yaml") : resolvedPath;
22
+ const filePath = stat.isDirectory() ? path.join(resolvedPath, DEFAULT_MANIFEST_FILENAME) : resolvedPath;
23
23
  const text = await fs.readFile(filePath, "utf-8");
24
24
  return { text, source: `file://${filePath}` };
25
25
  }
@@ -17,7 +17,7 @@ export interface ManifestAdapter {
17
17
  /**
18
18
  * Read a single manifest entry point.
19
19
  * - File path or URL → read that file/URL.
20
- * - Directory path → find and read `module.yaml` within it.
20
+ * - Directory path → find and read `telo.yaml` within it.
21
21
  */
22
22
  read(pathOrUrl: string): Promise<ManifestSourceData>;
23
23
  /**
@@ -0,0 +1,211 @@
1
+ import type { Invocable, ModuleContext as IModuleContext } from "@telorun/sdk";
2
+ import type { EmitEvent, InstanceFactory } from "@telorun/sdk";
3
+ import { EvaluationContext } from "./evaluation-context.js";
4
+
5
+ /** Wraps process.env so that missing keys return null instead of throwing in CEL.
6
+ * cel-js uses Object.hasOwn(obj, key) before accessing obj[key], so we must
7
+ * intercept getOwnPropertyDescriptor to report every string key as "own". */
8
+ function lenientEnv(env: Record<string, string | undefined>): Record<string, string | null> {
9
+ return new Proxy(env as Record<string, string | null>, {
10
+ get(target, key) {
11
+ if (typeof key !== "string") return (target as any)[key];
12
+ return key in target ? (target[key] ?? null) : null;
13
+ },
14
+ has() {
15
+ return true;
16
+ },
17
+ getOwnPropertyDescriptor(target, key) {
18
+ if (typeof key !== "string") return Object.getOwnPropertyDescriptor(target, key);
19
+ const value = key in target ? (target[key] ?? null) : null;
20
+ return { configurable: true, enumerable: true, writable: true, value };
21
+ },
22
+ });
23
+ }
24
+
25
+ function collectSecretValues(secrets: Record<string, unknown>): Set<string> {
26
+ const values = new Set<string>();
27
+ for (const value of Object.values(secrets)) {
28
+ if (typeof value === "string" && value.length > 0) {
29
+ values.add(value);
30
+ }
31
+ }
32
+ return values;
33
+ }
34
+
35
+ /**
36
+ * Persistent, module-scoped context. Three reserved CEL namespaces:
37
+ * variables, secrets, resources.
38
+ *
39
+ * Unlike the base EvaluationContext, ModuleContext is stateful and mutable:
40
+ * variables/secrets/resources accumulate during multi-pass initialization and
41
+ * the context record is rebuilt on each mutation. Import aliases are tracked
42
+ * here for alias-prefixed kind resolution (e.g. MyImport.Http.Route).
43
+ *
44
+ * Imported modules are surfaced under resources.<alias> alongside local
45
+ * resources — no separate imports namespace needed.
46
+ */
47
+ export class ModuleContext extends EvaluationContext implements IModuleContext {
48
+ private _variables: Record<string, unknown>;
49
+ private _secrets: Record<string, unknown>;
50
+ private _resources: Record<string, unknown>;
51
+
52
+ /** Maps import alias → real module name for kind resolution. */
53
+ readonly importAliases = new Map<string, string>();
54
+
55
+ /** Maps import alias → allowed kind names. Absent entry = unrestricted (e.g. Kernel). */
56
+ private readonly importedKinds = new Map<string, Set<string>>();
57
+
58
+ constructor(
59
+ source: string,
60
+ variables: Record<string, unknown> = {},
61
+ secrets: Record<string, unknown> = {},
62
+ resources: Record<string, unknown> = {},
63
+ private targets: string[] = [],
64
+ createInstance: InstanceFactory = async () => null,
65
+ emit: EmitEvent,
66
+ private readonly _hostEnv?: Record<string, string | undefined>,
67
+ ) {
68
+ super(source, {}, createInstance, new Set(), emit);
69
+ this._variables = variables;
70
+ this._secrets = secrets;
71
+ this._resources = resources;
72
+ this._rebuildContext();
73
+ }
74
+
75
+ get variables(): Record<string, unknown> {
76
+ return this._variables;
77
+ }
78
+
79
+ get secrets(): Record<string, unknown> {
80
+ return this._secrets;
81
+ }
82
+
83
+ get resources(): Record<string, unknown> {
84
+ return this._resources;
85
+ }
86
+
87
+ setVariables(vars: Record<string, unknown>): void {
88
+ this._variables = vars;
89
+ this._rebuildContext();
90
+ }
91
+
92
+ setTargets(vars: string[]): void {
93
+ this.targets = vars;
94
+ }
95
+
96
+ setSecrets(secrets: Record<string, unknown>): void {
97
+ this._secrets = secrets;
98
+ this._rebuildContext();
99
+ }
100
+
101
+ setResource(name: string, props: Record<string, unknown>): void {
102
+ this._resources = { ...this._resources, [name]: props };
103
+ this._rebuildContext();
104
+ }
105
+
106
+ protected override onResourceSnapshotted(name: string, snap: Record<string, unknown>): void {
107
+ this.setResource(name, snap);
108
+ }
109
+
110
+ /**
111
+ * Register an imported module under the given alias, with the list of kind names
112
+ * it exports. An empty kinds array means no restriction (used for built-ins like Kernel).
113
+ */
114
+ registerImport(alias: string, targetModule: string, kinds: string[]): void {
115
+ this.importAliases.set(alias, targetModule);
116
+ if (kinds.length > 0) {
117
+ this.importedKinds.set(alias, new Set(kinds));
118
+ }
119
+ }
120
+
121
+ getInstance(name: string): unknown {
122
+ const entry = this.resourceInstances.get(name);
123
+ if (!entry) {
124
+ throw new Error(
125
+ `Resource '${name}' not found in module context. Available resources: ${[...this.resourceInstances.keys()].join(", ")}`,
126
+ );
127
+ }
128
+ return entry?.instance;
129
+ }
130
+
131
+ getInvocable<TInput = Record<string, any>, TOutput = any>(
132
+ name: string,
133
+ ): Invocable<TInput, TOutput> {
134
+ const instance = this.getInstance(name);
135
+
136
+ if (typeof (instance as any)?.invoke !== "function") {
137
+ throw new Error(`Resource '${name}' does not have an invoke() method.`);
138
+ }
139
+ return instance as Invocable<TInput, TOutput>;
140
+ }
141
+
142
+ /**
143
+ * Resolve a fully-qualified kind like "Http.Server" to its real kind "http-server.Server".
144
+ * Splits on the first dot, looks up the prefix in importAliases, validates against
145
+ * importedKinds (if set), and reconstructs the resolved kind.
146
+ * Throws with a clear message if the alias is unknown or the kind is not exported.
147
+ */
148
+ resolveKind(kind: string): string {
149
+ const dot = kind.indexOf(".");
150
+ if (dot === -1) {
151
+ throw new Error(`Kind '${kind}' must be fully qualified (e.g. 'Module.KindName')`);
152
+ }
153
+ const prefix = kind.slice(0, dot);
154
+ const suffix = kind.slice(dot + 1);
155
+ const realModule = this.importAliases.get(prefix);
156
+ if (!realModule) {
157
+ const known = [...this.importAliases.keys()].join(", ") || "(none)";
158
+ throw new Error(
159
+ `Kind '${kind}': no module imported with alias '${prefix}'. Known aliases: ${known}`,
160
+ );
161
+ }
162
+ const allowed = this.importedKinds.get(prefix);
163
+ if (allowed !== undefined && !allowed.has(suffix)) {
164
+ throw new Error(
165
+ `Kind '${suffix}' is not exported by module '${realModule}' (imported as '${prefix}'). ` +
166
+ `Exported kinds: ${[...allowed].join(", ")}`,
167
+ );
168
+ }
169
+ return `${realModule}.${suffix}`;
170
+ }
171
+
172
+ private _rebuildContext(): void {
173
+ this._context = {
174
+ variables: this._variables,
175
+ secrets: this._secrets,
176
+ resources: this._resources,
177
+ ...(this._hostEnv ? { env: lenientEnv(this._hostEnv) } : {}),
178
+ };
179
+ this._secretValues = collectSecretValues(this._secrets);
180
+ }
181
+
182
+ override async invoke<TInputs>(kind: string, name: string, inputs: TInputs): Promise<any> {
183
+ const result = await super.invoke(kind, name, inputs);
184
+ const entry = this.resourceInstances.get(name);
185
+ if (entry && typeof (entry.instance as any).snapshot === "function") {
186
+ const snap = await Promise.resolve((entry.instance as any).snapshot());
187
+ this.setResource(name, snap as Record<string, unknown>);
188
+ }
189
+ return result;
190
+ }
191
+
192
+ async run(name: string) {
193
+ const resource = this.resourceInstances.get(name);
194
+ if (!resource) {
195
+ throw new Error(
196
+ `Target resource ${name} not found in module context. Available resources: ${[...this.resourceInstances.keys()].join(", ")}`,
197
+ );
198
+ }
199
+ if (typeof resource.instance.run === "function") {
200
+ await resource.instance.run();
201
+ } else {
202
+ throw new Error(`Target resource ${name} does not have a run() method.`);
203
+ }
204
+ }
205
+
206
+ async runTargets() {
207
+ for (const target of this.targets) {
208
+ await this.run(target);
209
+ }
210
+ }
211
+ }
@@ -1,31 +1,44 @@
1
1
  import {
2
- EvaluationContext,
3
- ModuleContext,
4
2
  NoopValidator,
5
3
  ResourceContext,
6
4
  RuntimeError,
7
5
  RuntimeResource,
8
6
  isCompiledValue,
7
+ type EvaluationContext as IEvaluationContext,
8
+ type ModuleContext,
9
+ type ParsedArgs,
10
+ type TypeRule,
9
11
  } from "@telorun/sdk";
12
+ import { EvaluationContext } from "./evaluation-context.js";
10
13
  import AjvModule from "ajv";
11
14
  import addFormats from "ajv-formats";
12
15
  import { Kernel } from "./kernel.js";
13
16
  import { formatAjvErrors } from "./manifest-schemas.js";
14
- import { SchemaValidator } from "./schema-valiator.js";
17
+ import { SchemaValidator } from "./schema-validator.js";
15
18
 
16
19
  const Ajv = AjvModule.default ?? AjvModule;
17
20
 
18
21
  export class ResourceContextImpl implements ResourceContext {
22
+ readonly stdin: NodeJS.ReadableStream;
23
+ readonly stdout: NodeJS.WritableStream;
24
+ readonly stderr: NodeJS.WritableStream;
25
+ readonly args: ParsedArgs;
26
+
19
27
  constructor(
20
28
  readonly kernel: Kernel,
21
29
  readonly moduleContext: ModuleContext,
22
30
  private readonly metadata: Record<string, any>,
23
31
  private readonly validator: SchemaValidator = new SchemaValidator(),
24
- ) {}
25
-
26
- stdin: NodeJS.ReadableStream = process.stdin;
27
- stdout: NodeJS.WritableStream = process.stdout;
28
- stderr: NodeJS.WritableStream = process.stderr;
32
+ stdin?: NodeJS.ReadableStream,
33
+ stdout?: NodeJS.WritableStream,
34
+ stderr?: NodeJS.WritableStream,
35
+ args?: ParsedArgs,
36
+ ) {
37
+ this.stdin = stdin ?? process.stdin;
38
+ this.stdout = stdout ?? process.stdout;
39
+ this.stderr = stderr ?? process.stderr;
40
+ this.args = args ?? { _: [] };
41
+ }
29
42
 
30
43
  createSchemaValidator(schema: any) {
31
44
  if (!schema) {
@@ -42,6 +55,48 @@ export class ResourceContextImpl implements ResourceContext {
42
55
  return this.validator.getSchema(name);
43
56
  }
44
57
 
58
+ registerTypeRules(name: string, rules: TypeRule[]): void {
59
+ this.validator.addTypeRules(name, rules);
60
+ }
61
+
62
+ lookupTypeRules(name: string): TypeRule[] | undefined {
63
+ return this.validator.getTypeRules(name);
64
+ }
65
+
66
+ createTypeValidator(typeRef: string | Record<string, any> | undefined) {
67
+ if (!typeRef) return new NoopValidator();
68
+
69
+ // String reference: look up registered type schema by name
70
+ if (typeof typeRef === "string") {
71
+ const schema = this.validator.getSchema(typeRef);
72
+ if (!schema) {
73
+ throw new RuntimeError(
74
+ "ERR_TYPE_NOT_FOUND",
75
+ `Type "${typeRef}" not found in schema registry`,
76
+ );
77
+ }
78
+ const base = this.validator.compile(schema);
79
+ const rules = this.validator.getTypeRules(typeRef);
80
+ if (rules && rules.length > 0) {
81
+ return this.validator.composeWithRules(base, typeRef, rules);
82
+ }
83
+ return base;
84
+ }
85
+
86
+ // Inline schema object: if it has a `schema` property, it's a type resource shape
87
+ if (typeRef.schema && typeof typeRef.schema === "object") {
88
+ const base = this.validator.compile(typeRef.schema);
89
+ const rules = Array.isArray(typeRef.rules) ? typeRef.rules : [];
90
+ if (rules.length > 0) {
91
+ return this.validator.composeWithRules(base, "inline", rules);
92
+ }
93
+ return base;
94
+ }
95
+
96
+ // Raw JSON Schema object (direct schema, not wrapped in type resource)
97
+ return this.validator.compile(typeRef);
98
+ }
99
+
45
100
  validateSchema(value: any, schema: any) {
46
101
  const ajv = new Ajv({
47
102
  removeAdditional: true,
@@ -210,10 +265,10 @@ export class ResourceContextImpl implements ResourceContext {
210
265
 
211
266
  /**
212
267
  * Create a child EvaluationContext attached to the current module context.
213
- * Queue resources on the returned context with pendingResources.push(), then
214
- * call initializeChildContext() to initialize them in isolation.
268
+ * Register resources on the returned context with registerManifest(), then
269
+ * call initializeResources() to initialize them in isolation.
215
270
  */
216
- spawnChildContext(): EvaluationContext {
271
+ spawnChildContext(): IEvaluationContext {
217
272
  const child = new EvaluationContext(
218
273
  this.moduleContext.source,
219
274
  this.moduleContext.context,
@@ -224,7 +279,7 @@ export class ResourceContextImpl implements ResourceContext {
224
279
  return this.moduleContext.spawnChild(child);
225
280
  }
226
281
 
227
- transientChild(context: Record<string, any>): EvaluationContext {
282
+ transientChild(context: Record<string, any>): IEvaluationContext {
228
283
  return this.moduleContext.transientChild(context);
229
284
  }
230
285