@telorun/kernel 0.47.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/application-env.d.ts +26 -11
  2. package/dist/application-env.d.ts.map +1 -1
  3. package/dist/application-env.js +36 -12
  4. package/dist/application-env.js.map +1 -1
  5. package/dist/bundle/module-manifest.d.ts +6 -0
  6. package/dist/bundle/module-manifest.d.ts.map +1 -1
  7. package/dist/bundle/module-manifest.js +4 -0
  8. package/dist/bundle/module-manifest.js.map +1 -1
  9. package/dist/controller-loaders/npm-loader.d.ts +2 -16
  10. package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
  11. package/dist/controller-loaders/npm-loader.js +42 -1
  12. package/dist/controller-loaders/npm-loader.js.map +1 -1
  13. package/dist/controller-registry.d.ts +6 -6
  14. package/dist/controller-registry.d.ts.map +1 -1
  15. package/dist/controller-registry.js +14 -10
  16. package/dist/controller-registry.js.map +1 -1
  17. package/dist/controllers/module/import-controller.d.ts +0 -46
  18. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  19. package/dist/controllers/module/import-controller.js +0 -27
  20. package/dist/controllers/module/import-controller.js.map +1 -1
  21. package/dist/controllers/resource-definition/abstract-controller.d.ts +0 -4
  22. package/dist/controllers/resource-definition/abstract-controller.d.ts.map +1 -1
  23. package/dist/controllers/resource-definition/abstract-controller.js +0 -4
  24. package/dist/controllers/resource-definition/abstract-controller.js.map +1 -1
  25. package/dist/controllers/resource-definition/resource-definition-controller.d.ts +0 -4
  26. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  27. package/dist/controllers/resource-definition/resource-definition-controller.js +0 -4
  28. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  29. package/dist/controllers/resource-definition/resource-inherited-controller.d.ts.map +1 -1
  30. package/dist/controllers/resource-definition/resource-inherited-controller.js +0 -1
  31. package/dist/controllers/resource-definition/resource-inherited-controller.js.map +1 -1
  32. package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
  33. package/dist/controllers/resource-definition/resource-template-controller.js +0 -1
  34. package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
  35. package/dist/kernel.d.ts.map +1 -1
  36. package/dist/kernel.js +18 -11
  37. package/dist/kernel.js.map +1 -1
  38. package/dist/transports/oci/oci-client.d.ts +5 -0
  39. package/dist/transports/oci/oci-client.d.ts.map +1 -1
  40. package/dist/transports/oci/oci-client.js.map +1 -1
  41. package/dist/transports/oci/oci-transport.d.ts +18 -0
  42. package/dist/transports/oci/oci-transport.d.ts.map +1 -1
  43. package/dist/transports/oci/oci-transport.js +41 -8
  44. package/dist/transports/oci/oci-transport.js.map +1 -1
  45. package/dist/transports/registry-transport.d.ts +7 -0
  46. package/dist/transports/registry-transport.d.ts.map +1 -1
  47. package/dist/transports/registry-transport.js +29 -10
  48. package/dist/transports/registry-transport.js.map +1 -1
  49. package/dist/transports/transport.d.ts +19 -0
  50. package/dist/transports/transport.d.ts.map +1 -1
  51. package/package.json +3 -3
  52. package/src/application-env.ts +36 -11
  53. package/src/bundle/module-manifest.ts +10 -0
  54. package/src/controller-loaders/npm-loader.ts +47 -1
  55. package/src/controller-registry.ts +14 -10
  56. package/src/controllers/module/import-controller.ts +0 -27
  57. package/src/controllers/resource-definition/abstract-controller.ts +0 -4
  58. package/src/controllers/resource-definition/resource-definition-controller.ts +0 -4
  59. package/src/controllers/resource-definition/resource-inherited-controller.ts +0 -2
  60. package/src/controllers/resource-definition/resource-template-controller.ts +0 -2
  61. package/src/kernel.ts +20 -11
  62. package/src/transports/oci/oci-client.ts +5 -0
  63. package/src/transports/oci/oci-transport.ts +46 -10
  64. package/src/transports/registry-transport.ts +32 -10
  65. package/src/transports/transport.ts +20 -0
package/src/kernel.ts CHANGED
@@ -483,13 +483,17 @@ export class Kernel implements IKernel {
483
483
  );
484
484
  }
485
485
  // Bake the per-kind resource-config validators too. `_createInstance`
486
- // compiles `controller.schema` (which falls back to the definition's own
487
- // `schema`) for every resource at runtime work this analyze-only pass
488
- // otherwise skips because it stops before instantiation, leaving the
489
- // runtime to recompile and fail to persist them on a read-only image.
486
+ // compiles the definition's `schema` for every resource at runtime work
487
+ // this analyze-only pass otherwise skips because it stops before
488
+ // instantiation, leaving the runtime to recompile and fail to persist
489
+ // them on a read-only image.
490
490
  // Pre-compiling every `Telo.Definition` schema here writes them into the
491
- // same content-addressed `__validators/` cache the runtime reads.
492
- precompileDefinitionSchemas(staticManifests, this.sharedSchemaValidator);
491
+ // same content-addressed `__validators/` cache the runtime reads. The
492
+ // resolver lets it also bake each `extends` child's inheritance-resolved
493
+ // schema — the form the runtime actually validates against.
494
+ precompileDefinitionSchemas(staticManifests, this.sharedSchemaValidator, (def) =>
495
+ this.registry.resolverForDefinition(def),
496
+ );
493
497
  // Framework/builtin controller schemas (`Telo.Import`, `Telo.Definition`,
494
498
  // the module controller, …) aren't in the static manifests but are
495
499
  // validated per-resource at runtime just the same. They're registered by
@@ -1040,12 +1044,17 @@ export class Kernel implements IKernel {
1040
1044
  `Controller for ${kind} does not implement create method`,
1041
1045
  );
1042
1046
  }
1043
- if (!controller.schema?.type) {
1044
- throw new Error(`No schema defined for ${kind} controller`);
1047
+ // The DEFINITION is the sole resource-config contract — a controller module
1048
+ // cannot supply or override one. Keeping the schema on the manifest is what
1049
+ // lets `telo check` see it, the validator warm bake it, and the editor render
1050
+ // it; a code-side schema was invisible to all three and free to drift.
1051
+ const definition = this.controllers.getDefinition(resolvedKind);
1052
+ const configSchema = definition?.schema as Record<string, unknown> | undefined;
1053
+ if (!configSchema?.type) {
1054
+ throw new Error(`No schema defined for kind ${kind}`);
1045
1055
  }
1046
1056
 
1047
1057
  // Resolve eval paths from x-telo-eval annotations in the parent and own schema
1048
- const definition = this.controllers.getDefinition(resolvedKind);
1049
1058
  const parentDef = definition?.capability
1050
1059
  ? this.controllers.getDefinition(definition.capability)
1051
1060
  : undefined;
@@ -1063,8 +1072,8 @@ export class Kernel implements IKernel {
1063
1072
  // restoring the pre-CEL string view that the schema expects.
1064
1073
  try {
1065
1074
  this.sharedSchemaValidator
1066
- .compile(controller.schema)
1067
- .validate(stripCompiledValues(resource, controller.schema as Record<string, unknown>));
1075
+ .compile(configSchema)
1076
+ .validate(stripCompiledValues(resource, configSchema));
1068
1077
  } catch (error) {
1069
1078
  throw new RuntimeError(
1070
1079
  "ERR_RESOURCE_SCHEMA_VALIDATION_FAILED",
@@ -32,6 +32,11 @@ export interface OciManifest {
32
32
  artifactType?: string;
33
33
  config: OciDescriptor;
34
34
  layers: OciDescriptor[];
35
+ /** Descriptive key/value metadata on the manifest. Telo projects the module's
36
+ * declared provenance into the standard `org.opencontainers.image.*` keys —
37
+ * the only metadata channel GHCR exposes, since it does not serve the
38
+ * referrers API. */
39
+ annotations?: Record<string, string>;
35
40
  }
36
41
 
37
42
  function sha256Hex(bytes: Uint8Array): string {
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  DEFAULT_MANIFEST_FILENAME,
3
3
  IntegrityError,
4
+ sha256Base64Url,
4
5
  verifyIntegrity,
5
6
  type ManifestSource,
6
7
  } from "@telorun/analyzer";
7
8
 
8
9
  import { computeFilesIntegrity, injectFilesIntegrity } from "../../bundle/files-integrity.js";
9
- import { readOwnerManifest } from "../../bundle/module-manifest.js";
10
+ import { readOwnerManifest, type OwnerManifest } from "../../bundle/module-manifest.js";
10
11
  import { makeTarGz, readTarGz, toPayloadFiles } from "../../bundle/tar.js";
11
12
  import type {
12
13
  FetchedArtifact,
@@ -134,6 +135,39 @@ export class OciTransport implements Transport {
134
135
  return pullVerified(ref);
135
136
  }
136
137
 
138
+ /** Hashes the **UTF-8 encoding of the extracted `telo.yaml`**, which is what
139
+ * `pullVerified` checks an inline `#sha256-…` pin against on the read path.
140
+ *
141
+ * Cost note: a module is one tar blob, so there is no way to read `telo.yaml`
142
+ * without pulling the whole artifact — including any `files:` payload — and
143
+ * this path is deliberately uncached (a pin must hash what is published
144
+ * *now*, not a cached copy). Two consequences for callers: pinning N imports
145
+ * costs N full artifact pulls, and `pullVerified` also re-checks the
146
+ * dependency's `filesIntegrity`, so a corrupt *payload* upstream surfaces
147
+ * here as a pinning failure rather than a payload error. Both are acceptable
148
+ * for publish-time pinning, where correctness beats latency and refusing to
149
+ * pin against a corrupt dependency is the right outcome. */
150
+ async manifestHash(ref: string): Promise<string> {
151
+ const { manifest } = await pullVerified(ref);
152
+ return `sha256-${await sha256Base64Url(new TextEncoder().encode(manifest))}`;
153
+ }
154
+
155
+ /** Project a module's declared provenance onto the standard
156
+ * `org.opencontainers.image.*` annotation keys. Descriptive only — nothing
157
+ * addresses the artifact by these. Absent fields are omitted rather than
158
+ * written empty, so the manifest carries only what the module declared. */
159
+ private static annotationsFor(identity: OwnerManifest): Record<string, string> {
160
+ const mapped: Array<[string, string | undefined]> = [
161
+ ["org.opencontainers.image.title", identity.name],
162
+ ["org.opencontainers.image.version", identity.version],
163
+ ["org.opencontainers.image.description", identity.description],
164
+ ["org.opencontainers.image.source", identity.repository],
165
+ ["org.opencontainers.image.licenses", identity.license],
166
+ ["org.opencontainers.image.documentation", identity.documentation],
167
+ ];
168
+ return Object.fromEntries(mapped.filter((e): e is [string, string] => Boolean(e[1])));
169
+ }
170
+
137
171
  async publish(
138
172
  destination: string,
139
173
  bundle: PublishBundle,
@@ -144,19 +178,19 @@ export class OciTransport implements Transport {
144
178
  throw new Error("OCI publish requires metadata.version (used as the tag).");
145
179
  }
146
180
 
147
- // Destination is a full repo (`oci://host/repo`) or host-only
148
- // (`oci://host`), which defaults the repo to `<namespace>/<name>`.
181
+ // Destination must be a full repo (`oci://host/repo`). Identity is the ref,
182
+ // so the repo is never derived from `metadata.namespace`/`name` — a
183
+ // metadata-derived path is wrong whenever the repo differs from the name,
184
+ // and would silently push to a namespace the publisher may not own.
149
185
  const afterScheme = destination.replace(/^oci:\/\//, "").replace(/\/+$/, "");
150
186
  const slash = afterScheme.indexOf("/");
151
187
  const host = slash > 0 ? afterScheme.slice(0, slash) : afterScheme;
152
- let repo = slash > 0 ? afterScheme.slice(slash + 1) : "";
188
+ const repo = slash > 0 ? afterScheme.slice(slash + 1) : "";
153
189
  if (!repo) {
154
- if (!identity.namespace || !identity.name) {
155
- throw new Error(
156
- `OCI publish to host-only '${destination}' needs metadata.namespace and metadata.name to default the repo.`,
157
- );
158
- }
159
- repo = `${identity.namespace}/${identity.name}`;
190
+ throw new Error(
191
+ `OCI publish destination '${destination}' is host-only — it must name a full repository, ` +
192
+ `e.g. 'oci://${host || "ghcr.io"}/<org>/<name>'.`,
193
+ );
160
194
  }
161
195
  const tag = identity.version;
162
196
 
@@ -173,12 +207,14 @@ export class OciTransport implements Transport {
173
207
  const client = new OciClient(host, repo);
174
208
  const layerDigest = await client.pushBlob(tar);
175
209
  const config = await client.pushEmptyConfig();
210
+ const annotations = OciTransport.annotationsFor(identity);
176
211
  const manifest: OciManifest = {
177
212
  schemaVersion: 2,
178
213
  mediaType: OCI_MANIFEST_MEDIA_TYPE,
179
214
  artifactType: TELO_LAYER_MEDIA_TYPE,
180
215
  config,
181
216
  layers: [{ mediaType: TELO_LAYER_MEDIA_TYPE, digest: layerDigest, size: tar.length }],
217
+ ...(Object.keys(annotations).length > 0 ? { annotations } : {}),
182
218
  };
183
219
  await client.pushManifest(tag, manifest);
184
220
 
@@ -168,20 +168,26 @@ export class RegistryTransport implements Transport {
168
168
  return Array.isArray(body.versions) ? body.versions : [];
169
169
  }
170
170
 
171
- async digest(ref: string): Promise<string | null> {
172
- // Mirrors the sources' fetch-URL derivation: a direct URL points at (or
173
- // contains) the YAML file; a bare registry ref folds into the registry
174
- // layout. The digest is Telo's canonical hash over the `telo.yaml` bytes.
171
+ /** Mirrors the sources' fetch-URL derivation: a direct URL points at (or
172
+ * contains) the YAML file; a bare registry ref folds into the registry
173
+ * layout. `null` when this transport does not own the ref's shape. */
174
+ private manifestUrl(ref: string): string | null {
175
175
  const { base } = splitIntegrity(ref);
176
- let fetchUrl: string;
177
176
  if (base.startsWith("http://") || base.startsWith("https://")) {
178
- fetchUrl = base.includes(".yaml") ? base : `${base}/${DEFAULT_MANIFEST_FILENAME}`;
179
- } else if (isRegistryRef(ref)) {
177
+ return base.includes(".yaml") ? base : `${base}/${DEFAULT_MANIFEST_FILENAME}`;
178
+ }
179
+ if (isRegistryRef(ref)) {
180
180
  const { modulePath, version } = parseModuleRef(ref);
181
- fetchUrl = `${this.registryUrl.replace(/\/+$/, "")}/${modulePath}/${version}/${DEFAULT_MANIFEST_FILENAME}`;
182
- } else {
183
- return null;
181
+ return `${this.registryUrl.replace(/\/+$/, "")}/${modulePath}/${version}/${DEFAULT_MANIFEST_FILENAME}`;
184
182
  }
183
+ return null;
184
+ }
185
+
186
+ async digest(ref: string): Promise<string | null> {
187
+ // The digest is Telo's canonical hash over the `telo.yaml` bytes — the same
188
+ // value `manifestHash` returns, but absent-is-null rather than a throw.
189
+ const fetchUrl = this.manifestUrl(ref);
190
+ if (!fetchUrl) return null;
185
191
  await assertPublicEgress(fetchUrl);
186
192
  const res = await fetch(fetchUrl);
187
193
  if (res.status === 404) return null;
@@ -192,6 +198,22 @@ export class RegistryTransport implements Transport {
192
198
  return `sha256-${await sha256Base64Url(bytes)}`;
193
199
  }
194
200
 
201
+ /** Hashes the **raw response bytes**, which is exactly what `verifiedFetch`
202
+ * checks an inline `#sha256-…` pin against on the read path. */
203
+ async manifestHash(ref: string): Promise<string> {
204
+ const fetchUrl = this.manifestUrl(ref);
205
+ if (!fetchUrl) {
206
+ throw new Error(`cannot hash non-remote import '${ref}'`);
207
+ }
208
+ await assertPublicEgress(fetchUrl);
209
+ const res = await fetch(fetchUrl);
210
+ if (!res.ok) {
211
+ throw new Error(`fetch ${fetchUrl}: ${res.status} ${res.statusText}`);
212
+ }
213
+ const bytes = new Uint8Array(await res.arrayBuffer());
214
+ return `sha256-${await sha256Base64Url(bytes)}`;
215
+ }
216
+
195
217
  async fetchArtifact(ref: string): Promise<FetchedArtifact> {
196
218
  // `read` verifies the manifest bytes against the inline `#sha256-...` hash.
197
219
  const { text: manifest, source } = await this.source.read(ref);
@@ -99,6 +99,26 @@ export interface Transport {
99
99
  * digest per version and re-checks it on every track. */
100
100
  digest(ref: string): Promise<string | null>;
101
101
 
102
+ /** Telo's inline integrity hash (`sha256-<base64url>`) for the `telo.yaml`
103
+ * `ref` resolves to — the value written as a `#sha256-…` pin by `telo
104
+ * publish` and re-pinned by `telo upgrade`. Throws when the ref does not
105
+ * resolve; callers decide whether that is fatal (`--frozen`) or best-effort.
106
+ *
107
+ * This is on the interface, not computed by the caller, because *what gets
108
+ * hashed* is transport-specific and must match exactly what that transport's
109
+ * own `source.read()` verifies — otherwise a pin written at publish fails
110
+ * verification at import. HTTP/registry hash the raw response bytes;
111
+ * OCI hashes the UTF-8 encoding of the `telo.yaml` extracted from the tar
112
+ * layer. A caller cannot know which, so a caller-side scheme branch silently
113
+ * degrades the moment a transport is added — which is exactly how `oci://`
114
+ * refs came to be published unpinned.
115
+ *
116
+ * Distinct from `digest()`: that is an opaque transport-native content id for
117
+ * change detection, never written into a manifest or compared across
118
+ * transports. This is the portable, cross-transport hash Telo itself
119
+ * verifies. */
120
+ manifestHash(ref: string): Promise<string>;
121
+
102
122
  /** Push `bundle` to `destination` (a base ref / repo whose scheme this
103
123
  * transport owns), pinning the payload and writing the transport-native
104
124
  * artifact shape. Throws on failure. Used by `telo publish`. */