@telorun/kernel 0.66.0 → 0.68.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/dist/application-env.d.ts +25 -0
- package/dist/application-env.d.ts.map +1 -1
- package/dist/application-env.js +79 -3
- package/dist/application-env.js.map +1 -1
- package/dist/controller-loader.d.ts +7 -5
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +7 -5
- package/dist/controller-loader.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +29 -8
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +6 -0
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +20 -8
- package/dist/evaluation-context.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +32 -3
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-schemas.d.ts +1 -1
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +6 -0
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +3 -4
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +21 -20
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +71 -51
- package/dist/resource-context.js.map +1 -1
- package/dist/resource-handle.d.ts +11 -0
- package/dist/resource-handle.d.ts.map +1 -0
- package/dist/resource-handle.js +32 -0
- package/dist/resource-handle.js.map +1 -0
- package/dist/runtime-seam.d.ts.map +1 -1
- package/dist/runtime-seam.js +8 -2
- package/dist/runtime-seam.js.map +1 -1
- package/dist/schema-compiled-values.d.ts.map +1 -1
- package/dist/schema-compiled-values.js +39 -4
- package/dist/schema-compiled-values.js.map +1 -1
- package/dist/schema-validator.d.ts +21 -1
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +88 -6
- package/dist/schema-validator.js.map +1 -1
- package/dist/type-field-schema.d.ts +21 -0
- package/dist/type-field-schema.d.ts.map +1 -0
- package/dist/type-field-schema.js +54 -0
- package/dist/type-field-schema.js.map +1 -0
- package/dist/zone-context.d.ts +94 -0
- package/dist/zone-context.d.ts.map +1 -0
- package/dist/zone-context.js +272 -0
- package/dist/zone-context.js.map +1 -0
- package/package.json +4 -4
- package/src/application-env.ts +93 -4
- package/src/controller-loader.ts +7 -5
- package/src/controllers/resource-definition/resource-definition-controller.ts +31 -13
- package/src/evaluation-context.ts +21 -7
- package/src/kernel.ts +38 -1
- package/src/manifest-schemas.ts +6 -0
- package/src/module-context.ts +3 -4
- package/src/resource-context.ts +111 -50
- package/src/resource-handle.ts +35 -0
- package/src/runtime-seam.ts +9 -1
- package/src/schema-compiled-values.ts +36 -4
- package/src/schema-validator.ts +87 -5
- package/src/type-field-schema.ts +77 -0
- package/src/zone-context.ts +337 -0
package/src/kernel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AnalysisRegistry,
|
|
3
3
|
buildEvalPaths,
|
|
4
|
+
collectZoneModuleDocuments,
|
|
4
5
|
flattenForAnalyzer,
|
|
5
6
|
flattenLoadedModule,
|
|
6
7
|
isModuleKind,
|
|
@@ -45,6 +46,7 @@ import { formatSpanCounter } from "./logging/span-id.js";
|
|
|
45
46
|
import { ambientInvokeContext } from "./evaluation-context.js";
|
|
46
47
|
import { ModuleContext } from "./module-context.js";
|
|
47
48
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
49
|
+
import { mintResourceHandle } from "./resource-handle.js";
|
|
48
50
|
import { nodeCelHandlers } from "./cel-handlers.js";
|
|
49
51
|
import { parseRef, seedInvokeSource } from "./invoke-dispatch.js";
|
|
50
52
|
import { stripCompiledValues } from "./schema-compiled-values.js";
|
|
@@ -70,6 +72,7 @@ import {
|
|
|
70
72
|
collectDeclaredEnvKeys,
|
|
71
73
|
precompileApplicationEnvSchemas,
|
|
72
74
|
precompileDefinitionSchemas,
|
|
75
|
+
precompileTypeSchemas,
|
|
73
76
|
resolveApplicationEnv,
|
|
74
77
|
} from "./application-env.js";
|
|
75
78
|
import { policyFingerprint } from "./runtime-registry.js";
|
|
@@ -520,7 +523,10 @@ export class Kernel implements IKernel {
|
|
|
520
523
|
const skipValidation = stamp?.signature === analysisSignature;
|
|
521
524
|
const errors = this.analyzer.analyzeErrors(
|
|
522
525
|
staticManifests,
|
|
523
|
-
|
|
526
|
+
// Imported libraries' full documents, for the zone stage's per-library
|
|
527
|
+
// export derivation: `flattenForAnalyzer` forwards only each library's
|
|
528
|
+
// export surface, never its internal dispatch chain.
|
|
529
|
+
{ skipValidation, moduleDocuments: collectZoneModuleDocuments(analysisGraph) },
|
|
524
530
|
this.registry,
|
|
525
531
|
);
|
|
526
532
|
if (errors.length > 0) {
|
|
@@ -566,6 +572,26 @@ export class Kernel implements IKernel {
|
|
|
566
572
|
// same content-addressed `__validators/` cache the runtime reads. The
|
|
567
573
|
// resolver lets it also bake each `extends` child's inheritance-resolved
|
|
568
574
|
// schema — the form the runtime actually validates against.
|
|
575
|
+
//
|
|
576
|
+
// Named types first: a contract that is a `$ref` to one resolves through
|
|
577
|
+
// the schema registry, which at runtime is populated by the type
|
|
578
|
+
// resources' own init. Without them a `$ref` contract bakes nothing.
|
|
579
|
+
//
|
|
580
|
+
// Fed from the GRAPH, not from `staticManifests`: flatten forwards every
|
|
581
|
+
// module's definitions but only the ENTRY's resource instances, and a
|
|
582
|
+
// named type is a resource instance — so a library that declares its
|
|
583
|
+
// shapes once and `$ref`s them (`oauth-client`, `vector-store`) has no
|
|
584
|
+
// type doc in the flattened view at all.
|
|
585
|
+
const graphDocs = [...analysisGraph.modules.values()].flatMap((mod) =>
|
|
586
|
+
flattenLoadedModule(mod),
|
|
587
|
+
);
|
|
588
|
+
// Canonicalize `telo://Self/<type>` to the id the type registers under.
|
|
589
|
+
// `analyze()` did this to its own view; these projections are separate
|
|
590
|
+
// objects, and an un-canonicalized `$ref` resolves to nothing here while
|
|
591
|
+
// the runtime resolves it fine — a guaranteed miss on exactly the
|
|
592
|
+
// contracts that reference a named shape.
|
|
593
|
+
this.registry.resolveSchemaTypeRefs([...graphDocs, ...staticManifests]);
|
|
594
|
+
await precompileTypeSchemas(graphDocs, this.sharedSchemaValidator);
|
|
569
595
|
precompileDefinitionSchemas(staticManifests, this.sharedSchemaValidator, (def) =>
|
|
570
596
|
this.registry.resolverForDefinition(def),
|
|
571
597
|
);
|
|
@@ -1286,6 +1312,17 @@ export class Kernel implements IKernel {
|
|
|
1286
1312
|
const instance = await controller.create(processedResource, ctx);
|
|
1287
1313
|
if (!instance) return null;
|
|
1288
1314
|
|
|
1315
|
+
// Mint the instance's identity here, at the single instance-production site,
|
|
1316
|
+
// so an instance is never observable without a handle — the same argument
|
|
1317
|
+
// that put contract binding here. First mint wins: a `base:` child IS the
|
|
1318
|
+
// parent instance returned verbatim and must not be re-identified.
|
|
1319
|
+
const handle = mintResourceHandle(
|
|
1320
|
+
instance,
|
|
1321
|
+
resolvedKind,
|
|
1322
|
+
(processedResource.metadata?.name as string | undefined) ?? "<unnamed>",
|
|
1323
|
+
);
|
|
1324
|
+
(ctx as ResourceContextImpl).bindResourceIdentity(handle, resolvedKind, processedResource);
|
|
1325
|
+
|
|
1289
1326
|
// Bind the resolved invocation contract to the instance, here at the kernel's
|
|
1290
1327
|
// single instance-production site — so every consumer holds an already
|
|
1291
1328
|
// enforcing instance, including the majority that read a Phase-5-injected ref
|
package/src/manifest-schemas.ts
CHANGED
|
@@ -96,6 +96,12 @@ const KNOWN_CAPABILITIES = [
|
|
|
96
96
|
// path, and dispatch emits trace events, so routing logs through it would
|
|
97
97
|
// generate telemetry from inside the telemetry path. See kernel/specs/logging.md §10.
|
|
98
98
|
"Telo.Sink",
|
|
99
|
+
// `Telo.Executable` is deliberately declarable NOWHERE: it is the slot-
|
|
100
|
+
// constraint parent of Invocable and Runnable ("control can be transferred to
|
|
101
|
+
// this"), naming no lifecycle role. Listing it here keeps the open third-party
|
|
102
|
+
// fallback branch below from accepting it — and since no branch above admits
|
|
103
|
+
// it either, `capability: Telo.Executable` fails validation outright.
|
|
104
|
+
"Telo.Executable",
|
|
99
105
|
] as const;
|
|
100
106
|
|
|
101
107
|
/** Rule 8: `throws:` is only meaningful on Telo.Invocable or Telo.Runnable.
|
package/src/module-context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { executeInvokeStep, getRefIdentity, RuntimeError } from "@telorun/sdk";
|
|
1
|
+
import { deriveContext, executeInvokeStep, getRefIdentity, RuntimeError } from "@telorun/sdk";
|
|
2
2
|
import type { ScopeConfig } from "./logging/scope-config.js";
|
|
3
3
|
import type {
|
|
4
4
|
BootTarget,
|
|
@@ -585,12 +585,11 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
585
585
|
);
|
|
586
586
|
const targetCtx: InvokeContext | undefined =
|
|
587
587
|
tracing && ctx
|
|
588
|
-
? {
|
|
589
|
-
cancellation: ctx.cancellation,
|
|
588
|
+
? deriveContext(ctx, {
|
|
590
589
|
invocationId: appSpanId,
|
|
591
590
|
parentInvocationId: undefined,
|
|
592
591
|
traceId: appTraceId,
|
|
593
|
-
}
|
|
592
|
+
})
|
|
594
593
|
: ctx;
|
|
595
594
|
|
|
596
595
|
const steps: Record<string, unknown> = {};
|
package/src/resource-context.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
+
InvokeError,
|
|
2
3
|
NoopValidator,
|
|
3
4
|
ResourceContext,
|
|
4
5
|
ResourceInstance,
|
|
5
6
|
ResourceManifest,
|
|
6
7
|
RuntimeError,
|
|
7
8
|
RuntimeResource,
|
|
9
|
+
UNCANCELLABLE_CONTEXT,
|
|
8
10
|
createCancellationSource,
|
|
11
|
+
deriveContext,
|
|
12
|
+
getRefIdentity,
|
|
9
13
|
resolveRefInstance,
|
|
10
14
|
type CancellationSource,
|
|
11
15
|
type ControllerPolicy,
|
|
12
16
|
type EvaluationContext as IEvaluationContext,
|
|
17
|
+
type InvokeByNameOptions,
|
|
13
18
|
type InvokeContext,
|
|
14
19
|
type LoadOptions,
|
|
15
20
|
type ModuleContext,
|
|
@@ -17,10 +22,14 @@ import {
|
|
|
17
22
|
type OpenSpan,
|
|
18
23
|
type OpenSpanOptions,
|
|
19
24
|
type ParsedArgs,
|
|
25
|
+
type ResourceDefinition,
|
|
26
|
+
type ResourceHandle,
|
|
20
27
|
type RuntimeSeam,
|
|
21
28
|
type TypeRule,
|
|
29
|
+
type ZoneEntry,
|
|
22
30
|
} from "@telorun/sdk";
|
|
23
31
|
import { isRefSentinel } from "@telorun/templating";
|
|
32
|
+
import { ZoneContext } from "./zone-context.js";
|
|
24
33
|
import * as path from "path";
|
|
25
34
|
import { pathToFileURL } from "url";
|
|
26
35
|
import type { ModuleArtifact } from "./bundle/module-artifact.js";
|
|
@@ -34,6 +43,7 @@ interface KernelModuleContext {
|
|
|
34
43
|
getLoggingConfig?(): ScopeConfig | undefined;
|
|
35
44
|
}
|
|
36
45
|
import { stripCompiledValues } from "./schema-compiled-values.js";
|
|
46
|
+
import { resolveTypeFieldSchema } from "./type-field-schema.js";
|
|
37
47
|
import AjvModule from "ajv";
|
|
38
48
|
import addFormats from "ajv-formats";
|
|
39
49
|
import { Kernel } from "./kernel.js";
|
|
@@ -170,7 +180,12 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
170
180
|
if (!schema) {
|
|
171
181
|
return new NoopValidator();
|
|
172
182
|
}
|
|
173
|
-
|
|
183
|
+
// Never persisted: the schema is author data in a RESOURCE field, which the
|
|
184
|
+
// build-time warm does not walk, so a disk entry could only ever miss and
|
|
185
|
+
// be rewritten on every boot. Compiling through the kernel's validator is
|
|
186
|
+
// still what keeps one engine in the process — its formats, its `x-telo-*`
|
|
187
|
+
// keywords, its non-strict mode.
|
|
188
|
+
return this.validator.compile(schema, { persist: false });
|
|
174
189
|
}
|
|
175
190
|
|
|
176
191
|
registerSchema(name: string, schema: object): void {
|
|
@@ -207,54 +222,13 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
207
222
|
* anyway, for the decisions a validator cannot answer: which properties carry
|
|
208
223
|
* `x-telo-stream` and must be exempt from the walk, and which paths a
|
|
209
224
|
* `default:` can be written to. Returns undefined when the reference resolves
|
|
210
|
-
* to nothing.
|
|
211
|
-
resolveTypeSchema(typeRef: unknown): Record<string, any> | undefined {
|
|
212
|
-
return this.followTypeAlias(this.readTypeSchema(typeRef), new Set());
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
private readTypeSchema(typeRef: unknown): Record<string, any> | undefined {
|
|
216
|
-
if (!typeRef) return undefined;
|
|
217
|
-
if (typeof typeRef === "string") return this.validator.getSchema(typeRef) as any;
|
|
218
|
-
if (typeof typeRef !== "object") return undefined;
|
|
219
|
-
const ref = typeRef as Record<string, any>;
|
|
220
|
-
if (ref.schema && typeof ref.schema === "object") return ref.schema;
|
|
221
|
-
if (typeof ref.name === "string") return this.validator.getSchema(ref.name) as any;
|
|
222
|
-
if (ref.type || ref.properties || ref.$ref) return ref;
|
|
223
|
-
return undefined;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Follow a schema that is nothing but a `$ref` to a registered type, so the
|
|
228
|
-
* schema-level questions (which properties are streams, which paths carry a
|
|
229
|
-
* default) are asked of the real shape rather than of an alias.
|
|
230
|
-
*
|
|
231
|
-
* Only the whole-document alias form is followed, and only to READ it — the
|
|
232
|
-
* schema handed to AJV keeps its `$ref`s intact, because AJV resolves them
|
|
233
|
-
* itself against the registered ids and each type stays its own document with
|
|
234
|
-
* its own `$defs`. Inlining instead would move a `$ref: "#/$defs/X"` out of the
|
|
235
|
-
* document that defines `$defs.X`.
|
|
225
|
+
* to nothing.
|
|
236
226
|
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
schema: Record<string, any> | undefined,
|
|
243
|
-
seen: Set<string>,
|
|
244
|
-
): Record<string, any> | undefined {
|
|
245
|
-
let current = schema;
|
|
246
|
-
while (
|
|
247
|
-
current &&
|
|
248
|
-
typeof current.$ref === "string" &&
|
|
249
|
-
Object.keys(current).length === 1 &&
|
|
250
|
-
!seen.has(current.$ref)
|
|
251
|
-
) {
|
|
252
|
-
seen.add(current.$ref);
|
|
253
|
-
const target = this.validator.getSchema(current.$ref) as Record<string, any> | undefined;
|
|
254
|
-
if (!target) return current;
|
|
255
|
-
current = target;
|
|
256
|
-
}
|
|
257
|
-
return current;
|
|
227
|
+
* Shared with the build-time validator warm through
|
|
228
|
+
* {@link resolveTypeFieldSchema} — the warm must land on the same schema
|
|
229
|
+
* object the runtime compiles, or its baked entry is one nothing asks for. */
|
|
230
|
+
resolveTypeSchema(typeRef: unknown): Record<string, any> | undefined {
|
|
231
|
+
return resolveTypeFieldSchema(typeRef, (name) => this.validator.getSchema(name));
|
|
258
232
|
}
|
|
259
233
|
|
|
260
234
|
/** Compile `schema` but compose the CEL `rules:` registered under `name`.
|
|
@@ -350,6 +324,88 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
350
324
|
return createCancellationSource();
|
|
351
325
|
}
|
|
352
326
|
|
|
327
|
+
// ── Execution zones (kernel/specs/execution-zones.md) ─────────────────────
|
|
328
|
+
//
|
|
329
|
+
// Identity is held here (it is the resource's, not the zone subsystem's);
|
|
330
|
+
// everything else delegates to `ZoneContext`, which owns the annotation
|
|
331
|
+
// resolution, correlation walk and stack matching — and memoizes them, since
|
|
332
|
+
// this sits on the dispatch path.
|
|
333
|
+
|
|
334
|
+
#self: ResourceHandle | undefined;
|
|
335
|
+
#zones: ZoneContext | undefined;
|
|
336
|
+
|
|
337
|
+
/** Kernel-internal: stamped at `create()`, the moment the instance exists. */
|
|
338
|
+
bindResourceIdentity(
|
|
339
|
+
handle: ResourceHandle,
|
|
340
|
+
resolvedKind: string,
|
|
341
|
+
manifest: Record<string, unknown>,
|
|
342
|
+
): void {
|
|
343
|
+
this.#self = handle;
|
|
344
|
+
this.#zones = new ZoneContext({
|
|
345
|
+
resourceName: (this.metadata?.name as string) ?? "<unnamed>",
|
|
346
|
+
resolvedKind,
|
|
347
|
+
self: handle,
|
|
348
|
+
// The SAME object Phase-5 injection later mutates, so a correlation
|
|
349
|
+
// pointer read at invoke time sees live instances in ref slots.
|
|
350
|
+
manifest,
|
|
351
|
+
resolveDefinition: (kind) => this.kernel.getAnalysisRegistry().resolveDefinition(kind),
|
|
352
|
+
resolveDefinitionIn: (kind, module) =>
|
|
353
|
+
this.kernel.getAnalysisRegistry().resolveDefinitionIn(kind, module),
|
|
354
|
+
resolveLocalInstance: (name) => this.resolveLocalInstance(name),
|
|
355
|
+
resolveLocalManifest: (name) =>
|
|
356
|
+
this.contextForName(name).resourceInstances.get(name)?.resource as
|
|
357
|
+
| Record<string, unknown>
|
|
358
|
+
| undefined,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
get self(): ResourceHandle {
|
|
363
|
+
if (!this.#self) {
|
|
364
|
+
throw new RuntimeError(
|
|
365
|
+
"ERR_RESOURCE_IDENTITY_UNBOUND",
|
|
366
|
+
`[${this.metadata.name}] ctx.self is unavailable inside create() — the handle is minted when create() returns`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return this.#self;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The zone subsystem, available once `create()` has returned. */
|
|
373
|
+
private zoneContext(): ZoneContext {
|
|
374
|
+
if (!this.#zones) {
|
|
375
|
+
throw new RuntimeError(
|
|
376
|
+
"ERR_RESOURCE_IDENTITY_UNBOUND",
|
|
377
|
+
`[${this.metadata.name}] zones are unavailable inside create() — the handle is minted when create() returns`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return this.#zones;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
withZone<T>(
|
|
384
|
+
slot: string,
|
|
385
|
+
fn: (ctx: InvokeContext, entry: ZoneEntry) => Promise<T>,
|
|
386
|
+
base?: InvokeContext,
|
|
387
|
+
): Promise<T> {
|
|
388
|
+
return this.zoneContext().withZone(slot, fn, base);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
requireZone(field: string, ctx?: InvokeContext): ZoneEntry {
|
|
392
|
+
return this.zoneContext().requireZone(field, ctx);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
findZone(field: string, ctx?: InvokeContext): ZoneEntry | undefined {
|
|
396
|
+
return this.zoneContext().findZone(field, ctx);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
zonesFor(instance: ResourceInstance, ctx?: InvokeContext): readonly ZoneEntry[] {
|
|
400
|
+
return this.zoneContext().zonesFor(instance, ctx);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** The root context for runtime-driven inbound work — inherits nothing from
|
|
404
|
+
* whatever ambient happens to be live at the registration site. */
|
|
405
|
+
rootContext(opts?: { cancellation?: CancellationSource }): InvokeContext {
|
|
406
|
+
return opts?.cancellation?.context ?? UNCANCELLABLE_CONTEXT;
|
|
407
|
+
}
|
|
408
|
+
|
|
353
409
|
/** In-flight fire-and-forget tasks this resource spawned. Owned here, not by
|
|
354
410
|
* the kernel: the resource drains them in its own teardown (see
|
|
355
411
|
* `drainDetached`), so background work is bounded by the resource's lifetime. */
|
|
@@ -401,8 +457,13 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
401
457
|
return this.owningContext.openSpan(base, opts);
|
|
402
458
|
}
|
|
403
459
|
|
|
404
|
-
invoke<TInputs>(
|
|
405
|
-
|
|
460
|
+
invoke<TInputs>(
|
|
461
|
+
kind: string,
|
|
462
|
+
name: string,
|
|
463
|
+
inputs: TInputs,
|
|
464
|
+
options?: InvokeByNameOptions,
|
|
465
|
+
): Promise<any> {
|
|
466
|
+
return this.contextForName(name).invoke(kind, name, inputs, options?.ctx);
|
|
406
467
|
}
|
|
407
468
|
|
|
408
469
|
invokeResolved<TInputs>(
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ResourceHandle, ResourceInstanceId } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Instance → handle, minted at `create()` — the kernel's single
|
|
5
|
+
* instance-production site, where the invocation contract already binds — so an
|
|
6
|
+
* instance is never observable without one. The reverse direction deliberately
|
|
7
|
+
* does not exist: nothing turns a handle back into someone else's live
|
|
8
|
+
* instance, which is what keeps the ambient zone stack from leaking instances
|
|
9
|
+
* across module boundaries.
|
|
10
|
+
*/
|
|
11
|
+
const handles = new WeakMap<object, ResourceHandle>();
|
|
12
|
+
|
|
13
|
+
let counter = 0;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mint (or return) the handle for a live instance. Idempotent, first mint wins
|
|
17
|
+
* — a `base:` child IS its parent's instance returned verbatim, so the nested
|
|
18
|
+
* parent create stamps first and the child create must not re-identify it; one
|
|
19
|
+
* live instance, one id, exactly like `stampRefIdentity`.
|
|
20
|
+
*/
|
|
21
|
+
export function mintResourceHandle(instance: object, kind: string, name: string): ResourceHandle {
|
|
22
|
+
const existing = handles.get(instance);
|
|
23
|
+
if (existing) return existing;
|
|
24
|
+
const handle: ResourceHandle = Object.freeze({
|
|
25
|
+
id: `ri-${++counter}` as ResourceInstanceId,
|
|
26
|
+
ref: Object.freeze({ kind, name }),
|
|
27
|
+
});
|
|
28
|
+
handles.set(instance, handle);
|
|
29
|
+
return handle;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The handle minted for a live instance, if any. */
|
|
33
|
+
export function handleOfInstance(instance: object): ResourceHandle | undefined {
|
|
34
|
+
return handles.get(instance);
|
|
35
|
+
}
|
package/src/runtime-seam.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Loader,
|
|
3
3
|
StaticAnalyzer,
|
|
4
|
+
collectZoneModuleDocuments,
|
|
4
5
|
flattenForAnalyzer,
|
|
5
6
|
type AnalysisDiagnostic,
|
|
6
7
|
type ManifestSource,
|
|
8
|
+
type ZoneModuleDocuments,
|
|
7
9
|
} from "@telorun/analyzer";
|
|
8
10
|
import {
|
|
9
11
|
Stream,
|
|
@@ -211,6 +213,7 @@ export class KernelRuntimeSeam implements RuntimeSeam {
|
|
|
211
213
|
// `analyze()`'s — carried out of the try so the checks below can see them.
|
|
212
214
|
let parseDiagnostics: AnalysisDiagnostic[] = [];
|
|
213
215
|
let versionDiagnostics: AnalysisDiagnostic[] = [];
|
|
216
|
+
let moduleDocuments: ZoneModuleDocuments[] = [];
|
|
214
217
|
try {
|
|
215
218
|
const graph = await loader.loadGraph(source, {
|
|
216
219
|
desugarImports: options?.desugarImports ?? true,
|
|
@@ -219,6 +222,9 @@ export class KernelRuntimeSeam implements RuntimeSeam {
|
|
|
219
222
|
parseDiagnostics = graph.parseDiagnostics;
|
|
220
223
|
versionDiagnostics = graph.versionDiagnostics;
|
|
221
224
|
manifests = flattenForAnalyzer(graph);
|
|
225
|
+
// The zone stage derives each imported library's export contracts from
|
|
226
|
+
// its own full documents, which the flattened list drops.
|
|
227
|
+
moduleDocuments = collectZoneModuleDocuments(graph);
|
|
222
228
|
} catch (err) {
|
|
223
229
|
// A graph that would not load is an answer, not a failure of the call —
|
|
224
230
|
// "this manifest does not load, and here is the reason" is precisely what
|
|
@@ -243,7 +249,9 @@ export class KernelRuntimeSeam implements RuntimeSeam {
|
|
|
243
249
|
|
|
244
250
|
// `analyze()` never sees version skew, so without merging these a major
|
|
245
251
|
// mismatch — which `load()` refuses to boot on — would check clean.
|
|
246
|
-
const diagnostics = new StaticAnalyzer({ celHandlers: nodeCelHandlers }).analyze(manifests
|
|
252
|
+
const diagnostics = new StaticAnalyzer({ celHandlers: nodeCelHandlers }).analyze(manifests, {
|
|
253
|
+
moduleDocuments,
|
|
254
|
+
});
|
|
247
255
|
return {
|
|
248
256
|
diagnostics: [...versionDiagnostics, ...diagnostics].map(toCheckDiagnostic),
|
|
249
257
|
};
|
|
@@ -61,6 +61,35 @@ function collectSchemaProperties(
|
|
|
61
61
|
return props;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** True when a ref slot is holding CONFIG rather than a reference.
|
|
65
|
+
*
|
|
66
|
+
* A slot annotated `x-telo-ref` is normally handed back whole, but the
|
|
67
|
+
* annotation can sit on a node that is a reference AND a config carrier at
|
|
68
|
+
* once: `targets:` puts it on the array ITEM so a bare `!ref Foo` is accepted,
|
|
69
|
+
* while the same item may be a step object (`{ref, when}` /
|
|
70
|
+
* `{invoke, inputs, when}`) whose `when` is a CEL guard that must be stripped
|
|
71
|
+
* like any other. Told apart by what the value IS, three ways:
|
|
72
|
+
*
|
|
73
|
+
* - a reference carries a `kind` — `resolveRefSentinels` rewrites a `!ref` to
|
|
74
|
+
* `{kind, name, alias?}`, and the only other object a ref slot admits is an
|
|
75
|
+
* inline definition (`{kind, …config}`);
|
|
76
|
+
* - a live instance is either not a plain object, or exposes a method (a
|
|
77
|
+
* controller's `create()` may return an object literal — `Assert.Schema`
|
|
78
|
+
* does). Copying one is what the walk exists to avoid, and its graph is
|
|
79
|
+
* routinely cyclic;
|
|
80
|
+
* - what is left came from YAML, where a function cannot appear. */
|
|
81
|
+
function isConfigAtRefSlot(value: unknown): boolean {
|
|
82
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
83
|
+
const proto = Object.getPrototypeOf(value);
|
|
84
|
+
if (proto !== Object.prototype && proto !== null) return false;
|
|
85
|
+
const obj = value as Record<string, unknown>;
|
|
86
|
+
if ("kind" in obj) return false;
|
|
87
|
+
for (const member of Object.values(obj)) {
|
|
88
|
+
if (typeof member === "function") return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
/** Replaces CompiledValue wrappers with schema-appropriate placeholders for schema validation.
|
|
65
94
|
* Template strings were compiled from YAML at load time; this restores a shape
|
|
66
95
|
* that AJV can validate without evaluating expressions. When no schema is
|
|
@@ -88,10 +117,13 @@ export function stripCompiledValues(
|
|
|
88
117
|
const resolved = resolveSchemaRef(nodeSchema, root);
|
|
89
118
|
|
|
90
119
|
if (isCompiledValue(value)) return placeholderForSchema(resolved);
|
|
91
|
-
// A slot the schema declares as a reference is never config
|
|
92
|
-
// `{kind, name}` ref or the live instance Phase 5 replaced it
|
|
93
|
-
// schema declares no shape to validate against either way.
|
|
94
|
-
|
|
120
|
+
// A slot the schema declares as a reference is never config when it HOLDS a
|
|
121
|
+
// reference: a `{kind, name}` ref or the live instance Phase 5 replaced it
|
|
122
|
+
// with, and the schema declares no shape to validate against either way. A
|
|
123
|
+
// ref slot carrying config beside the ref keeps walking — bailing there left
|
|
124
|
+
// a boot target's `when: !cel` a CompiledValue for AJV to reject as
|
|
125
|
+
// "must be string", which is the whole gated-target form.
|
|
126
|
+
if (resolved["x-telo-ref"] !== undefined && !isConfigAtRefSlot(value)) return value;
|
|
95
127
|
|
|
96
128
|
if (Array.isArray(value)) {
|
|
97
129
|
const itemSchema = resolveSchemaRef((resolved.items ?? {}) as Record<string, unknown>, root);
|
package/src/schema-validator.ts
CHANGED
|
@@ -146,6 +146,64 @@ function collapseSentinelsToSource(value: unknown): unknown {
|
|
|
146
146
|
return value;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/** Schema keywords whose VALUE is a map keyed by author-chosen names rather
|
|
150
|
+
* than by keyword. A name may legitimately be `x-telo-…`, so the strip below
|
|
151
|
+
* must not treat a key in one of these maps as an annotation. */
|
|
152
|
+
const NAME_KEYED_SCHEMA_KEYWORDS = new Set([
|
|
153
|
+
"properties",
|
|
154
|
+
"patternProperties",
|
|
155
|
+
"dependentSchemas",
|
|
156
|
+
"dependentRequired",
|
|
157
|
+
"$defs",
|
|
158
|
+
"definitions",
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
/** Schema keywords whose value is DATA, not a subschema. The strip must not
|
|
162
|
+
* descend into them at all: an `x-telo-…` key inside a `const` / `default` /
|
|
163
|
+
* `enum` member is part of the value being matched or filled, so removing it
|
|
164
|
+
* would change what the validator accepts and what it writes — and would make
|
|
165
|
+
* two schemas that differ only there hash alike. */
|
|
166
|
+
const DATA_VALUE_KEYWORDS = new Set(["const", "default", "enum", "examples"]);
|
|
167
|
+
|
|
168
|
+
/** Deep-clone `schema` without its `x-telo-*` annotations — applied, like
|
|
169
|
+
* {@link collapseSentinelsToSource}, before both AJV compilation and cache
|
|
170
|
+
* hashing.
|
|
171
|
+
*
|
|
172
|
+
* Every `x-telo-*` keyword is analyzer/editor metadata: AJV runs `strict:
|
|
173
|
+
* false` and registers the known ones as no-op keywords, so none of them emits
|
|
174
|
+
* a single line of validation code. Leaving them in the hashed form makes the
|
|
175
|
+
* cache key sensitive to differences that cannot change what the validator
|
|
176
|
+
* does — and one such difference is real and systematic. The analyzer rewrites
|
|
177
|
+
* `x-telo-ref.kind` to its canonical `<module>.<Kind>` in the declaring scope
|
|
178
|
+
* (`resolveSchemaRefKinds`), and `telo install`'s warm pass bakes THAT view;
|
|
179
|
+
* the kernel's controller registry never runs the rewrite, so at runtime the
|
|
180
|
+
* same kind's schema still reads `Self.Connection`. Two keys, one validator:
|
|
181
|
+
* every kind whose schema declares an alias-qualified ref missed the baked
|
|
182
|
+
* cache on every boot and tried to rewrite it — the EACCES noise on a
|
|
183
|
+
* read-only image.
|
|
184
|
+
*
|
|
185
|
+
* Stripping is what makes the key describe the compiled validator and nothing
|
|
186
|
+
* else, so the two views converge without either side having to agree on an
|
|
187
|
+
* annotation's spelling. `normalizeRefSlots` runs FIRST and is unaffected: it
|
|
188
|
+
* reads `x-telo-ref` to drop a legacy scalar `type` at a ref slot, which does
|
|
189
|
+
* change validation, and it has already done so by the time this runs. */
|
|
190
|
+
function stripTeloAnnotations(value: unknown, nameKeyed = false): unknown {
|
|
191
|
+
// An array's items are schema nodes (`allOf`, tuple `items`), never names.
|
|
192
|
+
if (Array.isArray(value)) return value.map((item) => stripTeloAnnotations(item));
|
|
193
|
+
if (!value || typeof value !== "object") return value;
|
|
194
|
+
const out: Record<string, unknown> = {};
|
|
195
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
196
|
+
if (!nameKeyed && k.startsWith("x-telo-")) continue;
|
|
197
|
+
// A data-bearing keyword's value is carried over verbatim; a name-keyed
|
|
198
|
+
// map's VALUES are schema nodes again, so only its keys are exempt.
|
|
199
|
+
out[k] =
|
|
200
|
+
!nameKeyed && DATA_VALUE_KEYWORDS.has(k)
|
|
201
|
+
? v
|
|
202
|
+
: stripTeloAnnotations(v, !nameKeyed && NAME_KEYED_SCHEMA_KEYWORDS.has(k));
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
149
207
|
export class SchemaValidator {
|
|
150
208
|
private ajv: InstanceType<typeof Ajv>;
|
|
151
209
|
private typeRules = new Map<string, TypeRule[]>();
|
|
@@ -162,6 +220,13 @@ export class SchemaValidator {
|
|
|
162
220
|
* process — `compiledValidators` is keyed by object identity and would
|
|
163
221
|
* miss those cases. */
|
|
164
222
|
private hashCache = new Map<string, DataValidator>();
|
|
223
|
+
/** Hashes whose compile went through the disk layer. A `persist: false`
|
|
224
|
+
* compile populates `hashCache` too — a repeat within the process should
|
|
225
|
+
* still collapse — but must not be mistaken for a baked entry: returning it
|
|
226
|
+
* to a persisting caller would suppress that caller's write permanently, so
|
|
227
|
+
* content shared with a warmable schema would never reach the cache. Such a
|
|
228
|
+
* caller falls through and compiles again, this time with the disk layer. */
|
|
229
|
+
private persistedHashes = new Set<string>();
|
|
165
230
|
/** Where cache-failure diagnostics go. Injected rather than reached for
|
|
166
231
|
* globally: this class is constructed outside the kernel's stdio scope, and
|
|
167
232
|
* §13.1 forbids the kernel writing to `process.stderr` directly. Defaults to
|
|
@@ -239,7 +304,18 @@ export class SchemaValidator {
|
|
|
239
304
|
this.cacheWritable = opts?.write ?? true;
|
|
240
305
|
}
|
|
241
306
|
|
|
242
|
-
|
|
307
|
+
/** Compile `schema` to a validator, reusing the in-memory and on-disk caches.
|
|
308
|
+
*
|
|
309
|
+
* `persist: false` keeps the compile in memory only — no disk read, no disk
|
|
310
|
+
* write. It is for a schema the build-time warm cannot see: an author-written
|
|
311
|
+
* JSON Schema sitting in a RESOURCE field (`ctx.createSchemaValidator`),
|
|
312
|
+
* rather than a kind's config schema or an invocation contract. Those are
|
|
313
|
+
* baked by `precompileDefinitionSchemas`; a resource-field schema never was,
|
|
314
|
+
* so persisting it only ever produced a miss-then-write on every boot — the
|
|
315
|
+
* EACCES noise on a read-only image. Declining to own what it cannot warm is
|
|
316
|
+
* the cache being honest, not a capability given up: the in-memory layers
|
|
317
|
+
* still collapse a repeat compile within the process. */
|
|
318
|
+
compile(schema: any, options?: { persist?: boolean }): DataValidator {
|
|
243
319
|
if (schema && typeof schema === "object") {
|
|
244
320
|
const cached = this.compiledValidators.get(schema as object);
|
|
245
321
|
if (cached) return cached;
|
|
@@ -284,7 +360,7 @@ export class SchemaValidator {
|
|
|
284
360
|
// precompiled (runtime) views of one schema land on the same cache key. The
|
|
285
361
|
// hashed and the compiled schema are this same canonical form. See
|
|
286
362
|
// `collapseSentinelsToSource`.
|
|
287
|
-
const sanitized = collapseSentinelsToSource(injected);
|
|
363
|
+
const sanitized = collapseSentinelsToSource(stripTeloAnnotations(injected));
|
|
288
364
|
|
|
289
365
|
const hash = createHash("sha256")
|
|
290
366
|
.update(
|
|
@@ -295,15 +371,17 @@ export class SchemaValidator {
|
|
|
295
371
|
)
|
|
296
372
|
.digest("hex")
|
|
297
373
|
.slice(0, 32);
|
|
374
|
+
const persist = options?.persist ?? true;
|
|
298
375
|
const cachedByHash = this.hashCache.get(hash);
|
|
299
|
-
if (cachedByHash) {
|
|
376
|
+
if (cachedByHash && (!persist || this.persistedHashes.has(hash))) {
|
|
300
377
|
if (schema && typeof schema === "object") {
|
|
301
378
|
this.compiledValidators.set(schema as object, cachedByHash);
|
|
302
379
|
}
|
|
303
380
|
return cachedByHash;
|
|
304
381
|
}
|
|
305
382
|
|
|
306
|
-
const validate = this.compileAjvOrLoadCached(sanitized, hash);
|
|
383
|
+
const validate = this.compileAjvOrLoadCached(sanitized, hash, persist);
|
|
384
|
+
if (persist) this.persistedHashes.add(hash);
|
|
307
385
|
|
|
308
386
|
const validator = {
|
|
309
387
|
validate: (data: any) => {
|
|
@@ -342,8 +420,12 @@ export class SchemaValidator {
|
|
|
342
420
|
private compileAjvOrLoadCached(
|
|
343
421
|
schema: any,
|
|
344
422
|
hash: string,
|
|
423
|
+
persist: boolean,
|
|
345
424
|
): ValidateFunction {
|
|
346
|
-
|
|
425
|
+
// `persist: false` drops the whole disk layer — the read too, not just the
|
|
426
|
+
// write. Nothing bakes these entries, so a lookup is an ENOENT probe whose
|
|
427
|
+
// only possible hit is one this process wrote on an earlier run.
|
|
428
|
+
const cacheDir = persist ? this.cacheDir : undefined;
|
|
347
429
|
if (cacheDir) {
|
|
348
430
|
const cachePath = path.join(cacheDir, `${hash}.cjs`);
|
|
349
431
|
try {
|