@telorun/kernel 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/dist/application-env.d.ts +24 -0
- package/dist/application-env.d.ts.map +1 -0
- package/dist/application-env.js +156 -0
- package/dist/application-env.js.map +1 -0
- package/dist/base-definition.d.ts +14 -0
- package/dist/base-definition.d.ts.map +1 -0
- package/dist/base-definition.js +17 -0
- package/dist/base-definition.js.map +1 -0
- package/dist/capabilities/capabilities/component.yaml +4 -0
- package/dist/capabilities/capabilities/executable.yaml +8 -0
- package/dist/capabilities/capabilities/handler.yaml +4 -0
- package/dist/capabilities/capabilities/listener.yaml +4 -0
- package/dist/capabilities/capabilities/provider.yaml +4 -0
- package/dist/capabilities/capabilities/template.yaml +4 -0
- package/dist/capabilities/capabilities/type.yaml +4 -0
- package/dist/capabilities/component.d.ts +3 -0
- package/dist/capabilities/component.d.ts.map +1 -0
- package/dist/capabilities/component.js +4 -0
- package/dist/capabilities/component.js.map +1 -0
- package/dist/capabilities/component.yaml +3 -0
- package/dist/capabilities/executable.d.ts +3 -0
- package/dist/capabilities/executable.d.ts.map +1 -0
- package/dist/capabilities/executable.js +5 -0
- package/dist/capabilities/executable.js.map +1 -0
- package/dist/capabilities/executable.yaml +7 -0
- package/dist/capabilities/handler.d.ts +3 -0
- package/dist/capabilities/handler.d.ts.map +1 -0
- package/dist/capabilities/handler.js +4 -0
- package/dist/capabilities/handler.js.map +1 -0
- package/dist/capabilities/handler.yaml +3 -0
- package/dist/capabilities/invokable.d.ts +3 -0
- package/dist/capabilities/invokable.d.ts.map +1 -0
- package/dist/capabilities/invokable.js +5 -0
- package/dist/capabilities/invokable.js.map +1 -0
- package/dist/capabilities/listener.d.ts +3 -0
- package/dist/capabilities/listener.d.ts.map +1 -0
- package/dist/capabilities/listener.js +5 -0
- package/dist/capabilities/listener.js.map +1 -0
- package/dist/capabilities/listener.yaml +3 -0
- package/dist/capabilities/mount.d.ts +3 -0
- package/dist/capabilities/mount.d.ts.map +1 -0
- package/dist/capabilities/mount.js +5 -0
- package/dist/capabilities/mount.js.map +1 -0
- package/dist/capabilities/provider.d.ts +3 -0
- package/dist/capabilities/provider.d.ts.map +1 -0
- package/dist/capabilities/provider.js +8 -0
- package/dist/capabilities/provider.js.map +1 -0
- package/dist/capabilities/provider.yaml +3 -0
- package/dist/capabilities/runnable.d.ts +3 -0
- package/dist/capabilities/runnable.d.ts.map +1 -0
- package/dist/capabilities/runnable.js +5 -0
- package/dist/capabilities/runnable.js.map +1 -0
- package/dist/capabilities/service.d.ts +3 -0
- package/dist/capabilities/service.d.ts.map +1 -0
- package/dist/capabilities/service.js +5 -0
- package/dist/capabilities/service.js.map +1 -0
- package/dist/capabilities/template.d.ts +3 -0
- package/dist/capabilities/template.d.ts.map +1 -0
- package/dist/capabilities/template.js +5 -0
- package/dist/capabilities/template.js.map +1 -0
- package/dist/capabilities/template.yaml +3 -0
- package/dist/capabilities/type.d.ts +3 -0
- package/dist/capabilities/type.d.ts.map +1 -0
- package/dist/capabilities/type.js +5 -0
- package/dist/capabilities/type.js.map +1 -0
- package/dist/capabilities/type.yaml +3 -0
- package/dist/controller-loaders/npm-loader.d.ts +32 -8
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +74 -101
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controllers/capability/capability-controller.d.ts +32 -0
- package/dist/controllers/capability/capability-controller.d.ts.map +1 -0
- package/dist/controllers/capability/capability-controller.js +26 -0
- package/dist/controllers/capability/capability-controller.js.map +1 -0
- package/dist/controllers/module/import-controller.d.ts +3 -2
- package/dist/controllers/module/import-controller.d.ts.map +1 -1
- package/dist/controllers/module/import-controller.js +23 -25
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/controllers/module/module.json +48 -0
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts +1 -0
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +3 -0
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts +5 -0
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.js +67 -6
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
- package/dist/internal-context.d.ts +25 -0
- package/dist/internal-context.d.ts.map +1 -0
- package/dist/internal-context.js +2 -0
- package/dist/internal-context.js.map +1 -0
- package/dist/kernel.d.ts +21 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +109 -5
- package/dist/kernel.js.map +1 -1
- package/dist/loader.d.ts +18 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +127 -0
- package/dist/loader.js.map +1 -0
- package/dist/manifest-adapters/http-adapter.d.ts +8 -0
- package/dist/manifest-adapters/http-adapter.d.ts.map +1 -0
- package/dist/manifest-adapters/http-adapter.js +31 -0
- package/dist/manifest-adapters/http-adapter.js.map +1 -0
- package/dist/manifest-adapters/local-file-adapter.d.ts +15 -0
- package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -0
- package/dist/manifest-adapters/local-file-adapter.js +95 -0
- package/dist/manifest-adapters/local-file-adapter.js.map +1 -0
- package/dist/manifest-adapters/manifest-adapter.d.ts +35 -0
- package/dist/manifest-adapters/manifest-adapter.d.ts.map +1 -0
- package/dist/manifest-adapters/manifest-adapter.js +2 -0
- package/dist/manifest-adapters/manifest-adapter.js.map +1 -0
- package/dist/manifest-adapters/registry-adapter.d.ts +9 -0
- package/dist/manifest-adapters/registry-adapter.d.ts.map +1 -0
- package/dist/manifest-adapters/registry-adapter.js +48 -0
- package/dist/manifest-adapters/registry-adapter.js.map +1 -0
- package/dist/manifest-schemas.d.ts +7 -23
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +18 -8
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/manifest-sources/analysis-stamp.d.ts +25 -0
- package/dist/manifest-sources/analysis-stamp.d.ts.map +1 -0
- package/dist/manifest-sources/analysis-stamp.js +151 -0
- package/dist/manifest-sources/analysis-stamp.js.map +1 -0
- package/dist/module-context-registry.d.ts +48 -0
- package/dist/module-context-registry.d.ts.map +1 -0
- package/dist/module-context-registry.js +91 -0
- package/dist/module-context-registry.js.map +1 -0
- package/dist/resource-context.d.ts +2 -0
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +28 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-valiator.d.ts +15 -0
- package/dist/schema-valiator.d.ts.map +1 -0
- package/dist/schema-valiator.js +127 -0
- package/dist/schema-valiator.js.map +1 -0
- package/dist/schema-validator.d.ts +28 -0
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +161 -1
- package/dist/schema-validator.js.map +1 -1
- package/dist/snapshot-serializer.d.ts +62 -0
- package/dist/snapshot-serializer.d.ts.map +1 -0
- package/dist/snapshot-serializer.js +164 -0
- package/dist/snapshot-serializer.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +9 -6
- package/src/application-env.ts +216 -0
- package/src/controller-loaders/npm-loader.ts +78 -103
- package/src/controllers/module/import-controller.ts +33 -36
- package/src/controllers/resource-definition/resource-definition-controller.ts +6 -0
- package/src/controllers/resource-definition/resource-template-controller.ts +95 -7
- package/src/internal-context.ts +25 -0
- package/src/kernel.ts +130 -5
- package/src/manifest-schemas.ts +31 -11
- package/src/manifest-sources/analysis-stamp.ts +169 -0
- package/src/resource-context.ts +34 -0
- package/src/schema-validator.ts +178 -2
- package/dist/generated/runtime-deps.json +0 -6
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
import { DiagnosticSeverity, StaticAnalyzer } from "@telorun/analyzer";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ResourceInstance } from "@telorun/sdk";
|
|
3
3
|
import { RuntimeError } from "@telorun/sdk";
|
|
4
|
+
import type { BuiltinControllerContext } from "../../internal-context.js";
|
|
4
5
|
import { ModuleContext } from "../../module-context.js";
|
|
5
6
|
import { isDefaultPolicy, normalizeRuntime } from "../../runtime-registry.js";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
>
|
|
11
|
-
|
|
12
|
-
// Only resolve relative/absolute-path sources against the importer's URL. Registry refs
|
|
13
|
-
// (std/foo@1.2.3) and absolute URLs (https://, file://) must pass through unchanged so the
|
|
14
|
-
// loader's source chain can dispatch them — otherwise `new URL("std/foo@1", "file:///srv/telo.yaml")`
|
|
15
|
-
// turns a registry ref into a bogus file path and LocalFileSource ENOENTs on it.
|
|
16
|
-
function resolveImportSource(source: string, baseSource: string): string {
|
|
17
|
-
if (source.startsWith(".") || source.startsWith("/")) {
|
|
18
|
-
return new URL(source, baseSource).toString();
|
|
19
|
-
}
|
|
20
|
-
return source;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function create(resource: any, ctx: ResourceContext): Promise<ResourceInstance> {
|
|
8
|
+
export async function create(
|
|
9
|
+
resource: any,
|
|
10
|
+
ctx: BuiltinControllerContext,
|
|
11
|
+
): Promise<ResourceInstance> {
|
|
24
12
|
const alias = resource.metadata.name as string;
|
|
25
13
|
|
|
26
14
|
const moduleSource: string = resource.module ?? resource.source;
|
|
@@ -36,27 +24,36 @@ export async function create(resource: any, ctx: ResourceContext): Promise<Resou
|
|
|
36
24
|
// Validate the imported module and all its transitive imports before loading for runtime.
|
|
37
25
|
// loadManifests() follows Telo.Import chains so definitions from sub-imports are present,
|
|
38
26
|
// preventing false UNDEFINED_KIND errors for kinds that come from the module's own imports.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
27
|
+
//
|
|
28
|
+
// Route URL resolution through the kernel/loader's own helper rather than
|
|
29
|
+
// a hand-rolled `new URL(...).toString()`. For LocalFileSource the
|
|
30
|
+
// outputs match; for any custom `ManifestSource` with a non-trivial
|
|
31
|
+
// `resolveRelative`, only this path produces the canonical URL the
|
|
32
|
+
// loader keyed its caches under — without which fast paths like
|
|
33
|
+
// `isImportValidatedAtLoad` silently miss.
|
|
34
|
+
const resolvedUrl = ctx.resolveImportUrl(base, moduleSource);
|
|
35
|
+
|
|
36
|
+
// Fast path: when the kernel's load-time `analyzeErrors` already covered
|
|
37
|
+
// this import's subtree (the common case — every Telo.Import declared in
|
|
38
|
+
// the entry graph is walked by `loadGraph` and validated by
|
|
39
|
+
// `kernel.load`), skip the redundant per-import StaticAnalyzer pass.
|
|
40
|
+
// Falls through to the full analysis for URLs that arrived
|
|
41
|
+
// programmatically after `load()` (e.g. dynamically constructed imports
|
|
42
|
+
// in tests). Two Telo.Imports with the same source but distinct
|
|
43
|
+
// metadata.name would each re-run analysis here — a memoisation hook
|
|
44
|
+
// can be reintroduced if that turns into a measurable cost.
|
|
45
|
+
if (!ctx.isImportValidatedAtLoad(resolvedUrl)) {
|
|
46
|
+
const analysisManifests = await ctx.loadManifests(resolvedUrl);
|
|
48
47
|
const diagnostics = new StaticAnalyzer().analyze(analysisManifests);
|
|
49
|
-
errors = diagnostics
|
|
48
|
+
const errors = diagnostics
|
|
50
49
|
.filter((d) => d.severity === DiagnosticSeverity.Error)
|
|
51
50
|
.map((d) => d.message);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
errors.join("\n"),
|
|
59
|
-
);
|
|
51
|
+
if (errors.length > 0) {
|
|
52
|
+
throw new RuntimeError(
|
|
53
|
+
"ERR_MANIFEST_VALIDATION_FAILED",
|
|
54
|
+
errors.join("\n"),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
// Load target module manifests for runtime. Inject variables/secrets as compile context so
|
|
@@ -18,6 +18,7 @@ type ResourceDefinitionResource = RuntimeResource & {
|
|
|
18
18
|
schema: Record<string, any>;
|
|
19
19
|
capability?: string;
|
|
20
20
|
controllers?: Array<string>;
|
|
21
|
+
provide?: unknown;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -31,6 +32,11 @@ class ResourceDefinition implements ResourceInstance {
|
|
|
31
32
|
|
|
32
33
|
async init(ctx: ResourceContext) {
|
|
33
34
|
if (!this.resource.controllers?.length) {
|
|
35
|
+
if (this.resource.capability === "Telo.Provider" && this.resource.provide == null) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Telo.Definition '${this.resource.metadata.name}': 'capability: Telo.Provider' requires either 'controllers:' (TS-backed) or 'provide:' (template-backed).`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
34
40
|
const controllerInstance = createTemplateController(this.resource as any);
|
|
35
41
|
ctx.registerDefinition(this.resource);
|
|
36
42
|
await ctx.registerController(
|
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import type { ControllerInstance, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import { isCompiledValue } from "@telorun/sdk";
|
|
3
3
|
|
|
4
|
+
/** Reports the resources: entries available to dispatch against, by expanded
|
|
5
|
+
* name and kind. Used in error messages to guide the developer back to the
|
|
6
|
+
* template's `resources:` array when a dispatch target doesn't match. */
|
|
7
|
+
function describeAvailableTargets(
|
|
8
|
+
ctx: ResourceContext,
|
|
9
|
+
resources: any[] | undefined,
|
|
10
|
+
self: Record<string, unknown>,
|
|
11
|
+
): string {
|
|
12
|
+
if (!resources || resources.length === 0) return "<none>";
|
|
13
|
+
return resources
|
|
14
|
+
.map((r) => {
|
|
15
|
+
const expanded = ctx.moduleContext.expandWith(r?.metadata?.name ?? "", { self }) as string;
|
|
16
|
+
const kind = typeof r?.kind === "string" ? r.kind : "<unknown-kind>";
|
|
17
|
+
return `'${expanded || "<unnamed>"}' (${kind})`;
|
|
18
|
+
})
|
|
19
|
+
.join(", ");
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
export function createTemplateController(definition: {
|
|
5
23
|
schema: Record<string, any>;
|
|
6
24
|
resources?: any[];
|
|
7
25
|
invoke?: string | { kind?: string; name: string };
|
|
8
26
|
inputs?: Record<string, any>;
|
|
9
27
|
run?: string;
|
|
28
|
+
provide?: { kind: string; name: string };
|
|
29
|
+
result?: Record<string, any>;
|
|
10
30
|
}): ControllerInstance {
|
|
11
31
|
return {
|
|
12
32
|
schema: definition.schema ?? { type: "object", additionalProperties: true },
|
|
@@ -32,6 +52,9 @@ export function createTemplateController(definition: {
|
|
|
32
52
|
const runTarget = definition.run
|
|
33
53
|
? (ctx.moduleContext.expandWith(definition.run, { self }) as string)
|
|
34
54
|
: null;
|
|
55
|
+
const provideTarget = definition.provide?.name
|
|
56
|
+
? (ctx.moduleContext.expandWith(definition.provide.name, { self }) as string)
|
|
57
|
+
: null;
|
|
35
58
|
|
|
36
59
|
const persistentManifests: any[] = [];
|
|
37
60
|
let ephemeralTemplate: any = null;
|
|
@@ -40,7 +63,10 @@ export function createTemplateController(definition: {
|
|
|
40
63
|
const expandedName = ctx.moduleContext.expandWith(template.metadata?.name ?? "", {
|
|
41
64
|
self,
|
|
42
65
|
}) as string;
|
|
43
|
-
const isTarget =
|
|
66
|
+
const isTarget =
|
|
67
|
+
expandedName === invokeTarget ||
|
|
68
|
+
expandedName === runTarget ||
|
|
69
|
+
expandedName === provideTarget;
|
|
44
70
|
if (isTarget) {
|
|
45
71
|
ephemeralTemplate = template;
|
|
46
72
|
} else {
|
|
@@ -84,7 +110,8 @@ export function createTemplateController(definition: {
|
|
|
84
110
|
invoke: async (inputs: any) => {
|
|
85
111
|
if (!ephemeralTemplate) {
|
|
86
112
|
throw new Error(
|
|
87
|
-
`Template '${resource.metadata.name}':
|
|
113
|
+
`Template '${resource.metadata.name}': 'invoke:' targets '${invokeTarget}' ` +
|
|
114
|
+
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
88
115
|
);
|
|
89
116
|
}
|
|
90
117
|
const extraContext = { self, inputs };
|
|
@@ -95,7 +122,13 @@ export function createTemplateController(definition: {
|
|
|
95
122
|
return withEphemeral(expanded, async (name) => {
|
|
96
123
|
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
97
124
|
if (!entry?.instance?.invoke) {
|
|
98
|
-
|
|
125
|
+
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
126
|
+
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
127
|
+
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Template '${resource.metadata.name}': 'invoke:' target '${targetKind}/${invokeTarget}' ` +
|
|
130
|
+
`has capability '${actualCap}', not Telo.Invocable. Update 'invoke:' to a Telo.Invocable kind, or change the target's kind in 'resources:'.`,
|
|
131
|
+
);
|
|
99
132
|
}
|
|
100
133
|
// Top-level `inputs:` (sibling of `invoke:`) carries the values passed
|
|
101
134
|
// to the dispatch target's invoke(). When absent, fall back to the
|
|
@@ -108,7 +141,13 @@ export function createTemplateController(definition: {
|
|
|
108
141
|
extraContext,
|
|
109
142
|
)
|
|
110
143
|
: expanded.inputs ?? inputs;
|
|
111
|
-
|
|
144
|
+
const raw = await entry.instance.invoke(invokeInputs);
|
|
145
|
+
if (definition.result == null) return raw;
|
|
146
|
+
const resultContext = { self, result: raw };
|
|
147
|
+
return ctx.moduleContext.expandWith(
|
|
148
|
+
ctx.moduleContext.expandWith(definition.result, resultContext),
|
|
149
|
+
resultContext,
|
|
150
|
+
);
|
|
112
151
|
});
|
|
113
152
|
},
|
|
114
153
|
}),
|
|
@@ -117,24 +156,73 @@ export function createTemplateController(definition: {
|
|
|
117
156
|
run: async () => {
|
|
118
157
|
if (!ephemeralTemplate) {
|
|
119
158
|
throw new Error(
|
|
120
|
-
`Template '${resource.metadata.name}':
|
|
159
|
+
`Template '${resource.metadata.name}': 'run:' targets '${runTarget}' ` +
|
|
160
|
+
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
121
161
|
);
|
|
122
162
|
}
|
|
123
163
|
const extraContext = { self };
|
|
124
164
|
const expanded = ctx.moduleContext.expandWith(
|
|
125
165
|
ctx.moduleContext.expandWith(ephemeralTemplate, extraContext),
|
|
126
166
|
extraContext,
|
|
127
|
-
);
|
|
167
|
+
) as any;
|
|
128
168
|
return withEphemeral(expanded, async (name) => {
|
|
129
169
|
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
130
170
|
if (!entry?.instance?.run) {
|
|
131
|
-
|
|
171
|
+
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
172
|
+
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
173
|
+
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Template '${resource.metadata.name}': 'run:' target '${targetKind}/${runTarget}' ` +
|
|
176
|
+
`has capability '${actualCap}', not Telo.Runnable. Update 'run:' to a Telo.Runnable kind, or change the target's kind in 'resources:'.`,
|
|
177
|
+
);
|
|
132
178
|
}
|
|
133
179
|
return entry.instance.run();
|
|
134
180
|
});
|
|
135
181
|
},
|
|
136
182
|
}),
|
|
137
183
|
|
|
184
|
+
...(provideTarget && {
|
|
185
|
+
provide: async () => {
|
|
186
|
+
if (!ephemeralTemplate) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Template '${resource.metadata.name}': 'provide:' targets '${provideTarget}' ` +
|
|
189
|
+
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const extraContext = { self };
|
|
193
|
+
const expanded = ctx.moduleContext.expandWith(
|
|
194
|
+
ctx.moduleContext.expandWith(ephemeralTemplate, extraContext),
|
|
195
|
+
extraContext,
|
|
196
|
+
) as any;
|
|
197
|
+
return withEphemeral(expanded, async (name) => {
|
|
198
|
+
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
199
|
+
if (!entry?.instance?.invoke) {
|
|
200
|
+
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
201
|
+
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
202
|
+
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Template '${resource.metadata.name}': 'provide:' target '${targetKind}/${provideTarget}' ` +
|
|
205
|
+
`has capability '${actualCap}', not Telo.Invocable. Update 'provide:' to a Telo.Invocable kind, or change the target's kind in 'resources:'.`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const provideInputs: any =
|
|
209
|
+
definition.inputs != null
|
|
210
|
+
? ctx.moduleContext.expandWith(
|
|
211
|
+
ctx.moduleContext.expandWith(definition.inputs, extraContext),
|
|
212
|
+
extraContext,
|
|
213
|
+
)
|
|
214
|
+
: {};
|
|
215
|
+
const raw = await entry.instance.invoke(provideInputs);
|
|
216
|
+
if (definition.result == null) return raw;
|
|
217
|
+
const resultContext = { self, result: raw };
|
|
218
|
+
return ctx.moduleContext.expandWith(
|
|
219
|
+
ctx.moduleContext.expandWith(definition.result, resultContext),
|
|
220
|
+
resultContext,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
|
|
138
226
|
teardown: async () => {
|
|
139
227
|
await childContext.teardownResources();
|
|
140
228
|
},
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ResourceContext } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context interface used by built-in kernel controllers (Telo.Application /
|
|
5
|
+
* Telo.Library / Telo.Import / Telo.Definition / Telo.Abstract) that need
|
|
6
|
+
* privileged access to load-time graph identity. These methods are
|
|
7
|
+
* intentionally *not* on the public `ResourceContext` exposed to module
|
|
8
|
+
* authors — they couple the caller to the kernel's load-time view of the
|
|
9
|
+
* world, and the import-controller is the only consumer today.
|
|
10
|
+
*
|
|
11
|
+
* `ResourceContextImpl` in this package implements both interfaces, so a
|
|
12
|
+
* controller authored against this type still works under the generic
|
|
13
|
+
* `controller.create(resource, ctx)` dispatch — the kernel just types it
|
|
14
|
+
* locally as `BuiltinControllerContext` instead of `ResourceContext`.
|
|
15
|
+
*/
|
|
16
|
+
export interface BuiltinControllerContext extends ResourceContext {
|
|
17
|
+
/** True when `url` resolved (via the loader's URL → canonical-source
|
|
18
|
+
* map) to a module that was part of the entry graph successfully
|
|
19
|
+
* analyzed during `Kernel.load()`. */
|
|
20
|
+
isImportValidatedAtLoad(url: string): boolean;
|
|
21
|
+
/** Resolve `importSource` against `fromSource` through the loader's
|
|
22
|
+
* source-chain `resolveRelative`. Identical to what `loadGraph` used
|
|
23
|
+
* internally — so the produced URL agrees with the loader's caches. */
|
|
24
|
+
resolveImportUrl(fromSource: string, importSource: string): string;
|
|
25
|
+
}
|
package/src/kernel.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
isModuleKind,
|
|
6
6
|
Loader,
|
|
7
7
|
StaticAnalyzer,
|
|
8
|
+
type LoadedGraph,
|
|
8
9
|
type ManifestSource,
|
|
9
10
|
} from "@telorun/analyzer";
|
|
10
11
|
import {
|
|
@@ -30,6 +31,13 @@ import { EventStream } from "./event-stream.js";
|
|
|
30
31
|
import { EventBus } from "./events.js";
|
|
31
32
|
import { ModuleContext } from "./module-context.js";
|
|
32
33
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
34
|
+
import {
|
|
35
|
+
computeAnalysisSignature,
|
|
36
|
+
readAnalysisStamp,
|
|
37
|
+
writeAnalysisStamp,
|
|
38
|
+
} from "./manifest-sources/analysis-stamp.js";
|
|
39
|
+
import { resolveEntryDir } from "./manifest-sources/local-manifest-cache-source.js";
|
|
40
|
+
import { resolveApplicationEnv } from "./application-env.js";
|
|
33
41
|
import { policyFingerprint } from "./runtime-registry.js";
|
|
34
42
|
import { SchemaValidator } from "./schema-validator.js";
|
|
35
43
|
|
|
@@ -117,6 +125,7 @@ export class Kernel implements IKernel {
|
|
|
117
125
|
private rootContext!: ModuleContext;
|
|
118
126
|
private staticManifests: ResourceManifest[] = [];
|
|
119
127
|
private _entryUrl?: string;
|
|
128
|
+
private _loadedGraph?: LoadedGraph;
|
|
120
129
|
// Lifecycle state — guards boot/runTargets/teardown/invoke transitions.
|
|
121
130
|
// teardown() is the only idempotent method; everything else throws on misuse.
|
|
122
131
|
private _bootCalled = false;
|
|
@@ -185,6 +194,37 @@ export class Kernel implements IKernel {
|
|
|
185
194
|
return this.registry;
|
|
186
195
|
}
|
|
187
196
|
|
|
197
|
+
/** The full LoadedGraph captured during `load()`. Used by the CLI to
|
|
198
|
+
* feed `writeManifestCache` so a successful `telo run` populates
|
|
199
|
+
* `<entry-dir>/.telo/manifests/` for subsequent runs — the same on-disk
|
|
200
|
+
* layout `telo install` writes. Undefined before `load()` has been
|
|
201
|
+
* called or if it threw before the graph was captured. */
|
|
202
|
+
getLoadedGraph(): LoadedGraph | undefined {
|
|
203
|
+
return this._loadedGraph;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** True when `url` resolves (via the loader's URL → canonical-source map)
|
|
207
|
+
* to a module that was already part of the entry graph successfully
|
|
208
|
+
* analyzed during `load()`. The import-controller uses it to skip its
|
|
209
|
+
* per-import `analyze()` pass when the kernel's load-time validation
|
|
210
|
+
* already covered the same subtree. */
|
|
211
|
+
isImportValidatedAtLoad(url: string): boolean {
|
|
212
|
+
if (!this._loadedGraph) return false;
|
|
213
|
+
const canonical = this.loader.canonicalize(url);
|
|
214
|
+
if (!canonical) return false;
|
|
215
|
+
return this._loadedGraph.modules.has(canonical);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Resolve a `Telo.Import.source` against the importing file's URL
|
|
219
|
+
* through the same source-chain `resolveRelative` the loader used at
|
|
220
|
+
* graph-walk time. The import-controller routes through here so its
|
|
221
|
+
* `resolveImportSource` no longer second-guesses the loader for
|
|
222
|
+
* custom `ManifestSource`s — `isImportValidatedAtLoad` etc. only hit
|
|
223
|
+
* when both sides agree on the canonical URL. */
|
|
224
|
+
resolveImportUrl(fromSource: string, importSource: string): string {
|
|
225
|
+
return this.loader.resolveImportUrl(fromSource, importSource);
|
|
226
|
+
}
|
|
227
|
+
|
|
188
228
|
/**
|
|
189
229
|
* Load built-in Runtime definitions (e.g., Telo.Application, Telo.Library).
|
|
190
230
|
* Also declares all known module namespaces upfront so that resources can be
|
|
@@ -225,6 +265,16 @@ export class Kernel implements IKernel {
|
|
|
225
265
|
async load(url: string): Promise<void> {
|
|
226
266
|
const sourceUrl = await this.loader.resolveEntryPoint(url);
|
|
227
267
|
this._entryUrl = sourceUrl;
|
|
268
|
+
// Point the shared schema validator at the entry-anchored cache so
|
|
269
|
+
// compiled AJV validators are persisted (standalone CJS) under
|
|
270
|
+
// `<entry-dir>/.telo/manifests/__validators/`. Memory- or HTTP-rooted
|
|
271
|
+
// entries skip the cache; their schema compiles stay in-process only.
|
|
272
|
+
const validatorCacheDir = resolveEntryDir(sourceUrl);
|
|
273
|
+
this.sharedSchemaValidator.setCacheDir(
|
|
274
|
+
validatorCacheDir
|
|
275
|
+
? `${validatorCacheDir}/.telo/manifests/__validators`
|
|
276
|
+
: undefined,
|
|
277
|
+
);
|
|
228
278
|
this.rootContext = new ModuleContext(
|
|
229
279
|
sourceUrl,
|
|
230
280
|
{},
|
|
@@ -253,6 +303,7 @@ export class Kernel implements IKernel {
|
|
|
253
303
|
if (analysisGraph.errors.length > 0) {
|
|
254
304
|
throw analysisGraph.errors[0].error;
|
|
255
305
|
}
|
|
306
|
+
this._loadedGraph = analysisGraph;
|
|
256
307
|
const staticManifests = flattenForAnalyzer(analysisGraph);
|
|
257
308
|
this.staticManifests = staticManifests;
|
|
258
309
|
|
|
@@ -275,7 +326,22 @@ export class Kernel implements IKernel {
|
|
|
275
326
|
}
|
|
276
327
|
}
|
|
277
328
|
|
|
278
|
-
|
|
329
|
+
// Hash-keyed analysis cache: when the entry's full LoadedGraph matches
|
|
330
|
+
// a previously-stamped successful run (same file bytes, same stamp
|
|
331
|
+
// protocol version), skip the per-resource validation walk inside
|
|
332
|
+
// `analyzeErrors`. Registration of identities / aliases / definitions
|
|
333
|
+
// and inline-resource normalisation still runs — only the diagnostic
|
|
334
|
+
// passes are elided. Memory- / HTTP-rooted entries have no
|
|
335
|
+
// local stamp store and always re-validate.
|
|
336
|
+
const entryDir = resolveEntryDir(sourceUrl);
|
|
337
|
+
const analysisSignature = computeAnalysisSignature(analysisGraph);
|
|
338
|
+
const stamp = entryDir ? await readAnalysisStamp(entryDir) : undefined;
|
|
339
|
+
const skipValidation = stamp?.signature === analysisSignature;
|
|
340
|
+
const errors = this.analyzer.analyzeErrors(
|
|
341
|
+
staticManifests,
|
|
342
|
+
{ skipValidation },
|
|
343
|
+
this.registry,
|
|
344
|
+
);
|
|
279
345
|
if (errors.length > 0) {
|
|
280
346
|
throw new RuntimeError(
|
|
281
347
|
"ERR_MANIFEST_VALIDATION_FAILED",
|
|
@@ -290,6 +356,19 @@ export class Kernel implements IKernel {
|
|
|
290
356
|
})),
|
|
291
357
|
);
|
|
292
358
|
}
|
|
359
|
+
if (entryDir && !skipValidation) {
|
|
360
|
+
// Best-effort: stamp the verdict so subsequent loads hit the fast
|
|
361
|
+
// path. A read-only filesystem (baked Docker image) reports the
|
|
362
|
+
// failure on stderr and keeps running — the lookup above will
|
|
363
|
+
// simply miss next time.
|
|
364
|
+
try {
|
|
365
|
+
await writeAnalysisStamp(entryDir, analysisSignature);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
this.stderr.write(
|
|
368
|
+
`[telo:kernel] analysis stamp write failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
293
372
|
|
|
294
373
|
// Load runtime configuration — root module gets access to host env.
|
|
295
374
|
// Imports are loaded separately via the import-controller; this load is
|
|
@@ -304,15 +383,54 @@ export class Kernel implements IKernel {
|
|
|
304
383
|
const normalizedManifests = this.analyzer.normalize(allManifests, this.registry);
|
|
305
384
|
this.staticManifests = normalizedManifests;
|
|
306
385
|
|
|
386
|
+
let rootApplicationManifest: ResourceManifest | undefined;
|
|
307
387
|
for (const manifest of normalizedManifests) {
|
|
308
388
|
if (isModuleKind(manifest.kind)) {
|
|
309
|
-
// Root is always Telo.Application (Library root rejected above).
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
389
|
+
// Root is always Telo.Application (Library root rejected above).
|
|
390
|
+
// Application-level `variables` / `secrets` declarations carry an `env:`
|
|
391
|
+
// mapping per field; the kernel populates the root scope from
|
|
392
|
+
// `process.env` after the manifest loop so imports can read
|
|
393
|
+
// `${{ variables.X }}` during their own init.
|
|
394
|
+
//
|
|
395
|
+
// Targets normalize down to bare names regardless of source surface.
|
|
396
|
+
// The analyzer's `resolveRefSentinels` pass already substituted any
|
|
397
|
+
// `!ref <name>` to `{kind, name}`; bare-string forms pass through.
|
|
398
|
+
// Anything else (e.g. an unresolved sentinel because the analyzer
|
|
399
|
+
// couldn't see it, or a malformed manifest) is a hard error —
|
|
400
|
+
// silently dropping the entry would leave the user staring at a
|
|
401
|
+
// "no targets ran" outcome with no signal where it went wrong.
|
|
402
|
+
const rawTargets = (manifest.targets ?? []) as unknown[];
|
|
403
|
+
const targetNames = rawTargets.map((t, index) => {
|
|
404
|
+
if (typeof t === "string") return t;
|
|
405
|
+
if (t && typeof t === "object" && typeof (t as { name?: unknown }).name === "string") {
|
|
406
|
+
return (t as { name: string }).name;
|
|
407
|
+
}
|
|
408
|
+
throw new RuntimeError(
|
|
409
|
+
"ERR_INVALID_VALUE",
|
|
410
|
+
`Telo.Application '${(manifest.metadata as { name?: string } | undefined)?.name ?? "(unnamed)"}' targets[${index}] could not be normalized to a resource name. Got: ${JSON.stringify(t)}`,
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
this.rootContext.setTargets(targetNames);
|
|
414
|
+
if (manifest.kind === "Telo.Application") {
|
|
415
|
+
rootApplicationManifest = manifest;
|
|
416
|
+
}
|
|
313
417
|
}
|
|
314
418
|
this.rootContext.registerManifest(manifest);
|
|
315
419
|
}
|
|
420
|
+
|
|
421
|
+
if (rootApplicationManifest) {
|
|
422
|
+
const { variables, secrets } = resolveApplicationEnv(
|
|
423
|
+
rootApplicationManifest as Record<string, any>,
|
|
424
|
+
this.env,
|
|
425
|
+
this.sharedSchemaValidator,
|
|
426
|
+
);
|
|
427
|
+
if (Object.keys(variables).length > 0) {
|
|
428
|
+
this.rootContext.setVariables(variables);
|
|
429
|
+
}
|
|
430
|
+
if (Object.keys(secrets).length > 0) {
|
|
431
|
+
this.rootContext.setSecrets(secrets);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
316
434
|
}
|
|
317
435
|
|
|
318
436
|
/**
|
|
@@ -419,6 +537,13 @@ export class Kernel implements IKernel {
|
|
|
419
537
|
if (this.rootContext) {
|
|
420
538
|
await this.rootContext.teardownResources();
|
|
421
539
|
}
|
|
540
|
+
// Drop the load-time graph so a teardown'd kernel doesn't pin every
|
|
541
|
+
// manifest file's text in memory (LoadedFile retains the parsed
|
|
542
|
+
// documents + the original YAML bytes). Reusing the kernel after
|
|
543
|
+
// teardown is a hard error elsewhere, so this is purely a memory
|
|
544
|
+
// hygiene step.
|
|
545
|
+
this._loadedGraph = undefined;
|
|
546
|
+
this.staticManifests = [];
|
|
422
547
|
await this.eventBus.emit("Kernel.Stopped", { exitCode: this._exitCode });
|
|
423
548
|
}
|
|
424
549
|
|
package/src/manifest-schemas.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { Type } from "@sinclair/typebox";
|
|
2
1
|
import AjvModule from "ajv";
|
|
3
2
|
import addFormats from "ajv-formats";
|
|
4
3
|
const Ajv = AjvModule.default ?? AjvModule;
|
|
5
4
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
// Re-export the shared ResourceRef fragment from the templating package
|
|
6
|
+
// so consumers that pull it from the kernel manifest-schemas surface keep
|
|
7
|
+
// working. The canonical home is `@telorun/templating` because the
|
|
8
|
+
// fragment describes the parsed shape of the `!ref` tag's TaggedSentinel.
|
|
9
|
+
export {
|
|
10
|
+
MANIFEST_SCHEMA_URI,
|
|
11
|
+
ManifestRootSchema,
|
|
12
|
+
ResourceRefSchema,
|
|
13
|
+
} from "@telorun/templating";
|
|
13
14
|
|
|
14
15
|
const metadataSchema = {
|
|
15
16
|
type: "object",
|
|
@@ -149,9 +150,28 @@ export const ResourceAbstractSchema = {
|
|
|
149
150
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
150
151
|
addFormats.default(ajv);
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// Lazy-compile validator: the AJV codegen cost (≈10–15 ms for these
|
|
154
|
+
// schemas) is only paid when a definition / abstract actually needs
|
|
155
|
+
// validating. A hello-world that loads no Telo.Definition or
|
|
156
|
+
// Telo.Abstract documents never triggers either compile; apps that
|
|
157
|
+
// do see them only pay once per process.
|
|
158
|
+
interface LazyValidator {
|
|
159
|
+
(data: unknown): boolean | Promise<unknown>;
|
|
160
|
+
errors?: any[] | null;
|
|
161
|
+
}
|
|
162
|
+
function lazyValidator(schema: object): LazyValidator {
|
|
163
|
+
let compiled: ReturnType<typeof ajv.compile> | undefined;
|
|
164
|
+
const fn: LazyValidator = (data: unknown) => {
|
|
165
|
+
if (!compiled) compiled = ajv.compile(schema);
|
|
166
|
+
const ok = compiled(data);
|
|
167
|
+
fn.errors = compiled.errors as any[] | null | undefined;
|
|
168
|
+
return ok;
|
|
169
|
+
};
|
|
170
|
+
return fn;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const validateResourceDefinition = lazyValidator(ResourceDefinitionSchema);
|
|
174
|
+
export const validateResourceAbstract = lazyValidator(ResourceAbstractSchema);
|
|
155
175
|
|
|
156
176
|
export function formatAjvErrors(errors: any[] | null | undefined): string {
|
|
157
177
|
if (!errors || errors.length === 0) return "Unknown schema error";
|