@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.
- package/README.md +100 -0
- package/dist/controller-registry.js +2 -2
- package/dist/controller-registry.js.map +1 -1
- package/dist/controllers/module/import-controller.d.ts.map +1 -1
- package/dist/controllers/module/import-controller.js +18 -9
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +76 -38
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +254 -89
- package/dist/evaluation-context.js.map +1 -1
- package/dist/execution-context.d.ts +1 -1
- package/dist/execution-context.d.ts.map +1 -1
- package/dist/execution-context.js +1 -1
- package/dist/execution-context.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +25 -2
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +100 -27
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-adapters/local-file-adapter.d.ts +1 -1
- package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -1
- package/dist/manifest-adapters/local-file-adapter.js +2 -1
- package/dist/manifest-adapters/local-file-adapter.js.map +1 -1
- package/dist/manifest-adapters/manifest-adapter.d.ts +1 -1
- package/dist/module-context.d.ts +28 -5
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +107 -4
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +14 -10
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +44 -8
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-valiator.d.ts +7 -1
- package/dist/schema-valiator.d.ts.map +1 -1
- package/dist/schema-valiator.js +75 -4
- package/dist/schema-valiator.js.map +1 -1
- package/dist/schema-validator.d.ts +15 -0
- package/dist/schema-validator.d.ts.map +1 -0
- package/dist/schema-validator.js +127 -0
- package/dist/schema-validator.js.map +1 -0
- package/package.json +21 -10
- package/src/controller-registry.ts +2 -2
- package/src/controllers/module/import-controller.ts +27 -11
- package/src/evaluation-context.ts +490 -0
- package/src/execution-context.ts +21 -0
- package/src/index.ts +4 -1
- package/src/kernel.ts +144 -38
- package/src/manifest-adapters/local-file-adapter.ts +2 -2
- package/src/manifest-adapters/manifest-adapter.ts +1 -1
- package/src/module-context.ts +211 -0
- package/src/resource-context.ts +67 -12
- package/src/schema-validator.ts +146 -0
- package/src/loader.ts +0 -134
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 {
|
|
22
|
+
import { LocalFileAdapter } from "./manifest-adapters/local-file-adapter.js";
|
|
19
23
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
20
|
-
import { SchemaValidator } from "./schema-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
536
|
-
|
|
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 = (
|
|
539
|
-
|
|
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 = (
|
|
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
|
|
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,
|
|
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 `
|
|
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
|
+
}
|
package/src/resource-context.ts
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
*
|
|
214
|
-
* call
|
|
268
|
+
* Register resources on the returned context with registerManifest(), then
|
|
269
|
+
* call initializeResources() to initialize them in isolation.
|
|
215
270
|
*/
|
|
216
|
-
spawnChildContext():
|
|
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>):
|
|
282
|
+
transientChild(context: Record<string, any>): IEvaluationContext {
|
|
228
283
|
return this.moduleContext.transientChild(context);
|
|
229
284
|
}
|
|
230
285
|
|