@hypit/hypit 0.1.12 → 0.1.14

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 (74) hide show
  1. package/README.md +36 -2
  2. package/bin/hypit.mjs +0 -2
  3. package/dist/public/model-kit.d.ts +16 -0
  4. package/package.json +3 -1
  5. package/packages/cli/README.md +3 -0
  6. package/packages/cli/package.json +1 -0
  7. package/packages/cli/src/commands/environment.ts +12 -7
  8. package/packages/cli/src/commands/results.ts +19 -3
  9. package/packages/cli/src/main.ts +2 -1
  10. package/packages/cli/src/oauth.ts +50 -7
  11. package/packages/cli/src/output.ts +2 -2
  12. package/packages/cli/src/source-discovery.ts +2 -2
  13. package/packages/credential-store-file/README.md +60 -0
  14. package/packages/credential-store-file/package.json +21 -0
  15. package/packages/credential-store-file/src/activation.ts +29 -0
  16. package/packages/credential-store-file/src/index.ts +1 -0
  17. package/packages/credential-store-file/src/store.ts +89 -0
  18. package/packages/fonts-open/src/surface.ts +8 -2
  19. package/packages/hyperframes/README.md +16 -2
  20. package/packages/hyperframes/src/browser-program.ts +6 -1
  21. package/packages/hyperframes/src/document.ts +31 -21
  22. package/packages/hyperframes/src/project.ts +11 -20
  23. package/packages/media-execution/README.md +7 -0
  24. package/packages/media-execution/src/execute.ts +5 -5
  25. package/packages/media-execution/src/index.ts +1 -1
  26. package/packages/media-execution/src/process-env.ts +20 -0
  27. package/packages/media-execution/src/surface.ts +83 -76
  28. package/packages/model-kit/README.md +28 -0
  29. package/packages/model-kit/src/index.ts +58 -19
  30. package/packages/package-loader-node/README.md +10 -0
  31. package/packages/package-loader-node/src/index.ts +1 -0
  32. package/packages/package-loader-node/src/loader.ts +25 -5
  33. package/packages/package-loader-node/src/location.ts +11 -2
  34. package/packages/project-context-node/README.md +2 -0
  35. package/packages/project-context-node/src/project-context.ts +5 -3
  36. package/packages/provider-hyperframes-local/README.md +95 -13
  37. package/packages/provider-hyperframes-local/package.json +13 -3
  38. package/packages/provider-hyperframes-local/src/activation.ts +20 -6
  39. package/packages/provider-hyperframes-local/src/browser-install.ts +5 -0
  40. package/packages/provider-hyperframes-local/src/browser.ts +118 -0
  41. package/packages/provider-hyperframes-local/src/capture-bootstrap.ts +4 -6
  42. package/packages/provider-hyperframes-local/src/capture-exit.ts +24 -0
  43. package/packages/provider-hyperframes-local/src/capture-process.ts +54 -59
  44. package/packages/provider-hyperframes-local/src/capture-worker.ts +28 -7
  45. package/packages/provider-hyperframes-local/src/capture.ts +12 -5
  46. package/packages/provider-hyperframes-local/src/opaque-capture.ts +46 -18
  47. package/packages/provider-hyperframes-local/src/options.ts +2 -2
  48. package/packages/provider-hyperframes-local/src/process-tree.ts +82 -0
  49. package/packages/provider-hyperframes-local/src/process.ts +22 -0
  50. package/packages/provider-hyperframes-local/src/program.ts +25 -33
  51. package/packages/provider-hyperframes-local/src/provider.ts +4 -7
  52. package/packages/provider-hyperframes-local/src/render.ts +9 -4
  53. package/packages/provider-hypihub/README.md +3 -0
  54. package/packages/runtime-host-node/README.md +5 -0
  55. package/packages/runtime-host-node/src/index.ts +7 -2
  56. package/packages/runtime-host-node/src/packages.ts +13 -7
  57. package/packages/runtime-local/README.md +10 -0
  58. package/packages/runtime-local/src/config.ts +1 -1
  59. package/packages/runtime-local/src/credentials.ts +10 -5
  60. package/packages/runtime-local/src/runtime.ts +1 -1
  61. package/packages/seedance/README.md +42 -0
  62. package/packages/seedance/src/index.ts +5 -22
  63. package/packages/seedance/src/surface.ts +2 -3
  64. package/packages/seedance/src/validation.ts +16 -0
  65. package/packages/studio/src/preview/runtime-shim.ts +17 -3
  66. package/packages/studio/src/server.ts +2 -2
  67. package/packages/video-cli/README.md +5 -1
  68. package/packages/video-cli/package.json +1 -0
  69. package/packages/video-cli/src/distribution.ts +3 -10
  70. package/packages/video-cli/src/version.ts +1 -1
  71. package/packages/workspace-fs-node/src/workspace.ts +2 -2
  72. package/packages/yt-dlp/README.md +3 -2
  73. package/packages/yt-dlp/package.json +4 -0
  74. package/packages/yt-dlp/src/download.ts +10 -14
@@ -13,13 +13,13 @@ import type { HostFacet } from "@hypit/host";
13
13
  import {
14
14
  bindGenerationMedia,
15
15
  bindGenerationText,
16
- finalizeGenerationRequestDraft,
17
16
  generationModuleRef,
18
17
  generationProducers,
19
18
  generationTypes,
20
19
  mediaBindingSchemaFromPort,
21
20
  requestDraftSchemaFromPorts,
22
21
  requestSchemaFromPorts,
22
+ sealGenerationPortRequest,
23
23
  verifyGenerationMediaBinding,
24
24
  verifyRequestDraftAgainstPorts,
25
25
  verifyRequestAgainstPorts,
@@ -28,6 +28,7 @@ import type {
28
28
  GenerationMediaBinding,
29
29
  GenerationMediaPort,
30
30
  GenerationPortTable,
31
+ GenerationRequest,
31
32
  GenerationRequestDraft,
32
33
  } from "@hypit/generation";
33
34
  import { textDependency, textTypes } from "@hypit/text";
@@ -76,6 +77,10 @@ export type ExactModelEndpointSpec = {
76
77
  readonly requestTypeName: string;
77
78
  readonly producerName: string;
78
79
  readonly ports: GenerationPortTable;
80
+ /** Additional complete-request rules, after port and supplied-input validation. */
81
+ readonly validateRequest?: (request: GenerationRequest) => void;
82
+ /** Pure, synchronous checks of supplied values, in drafts and complete requests; throw to reject. */
83
+ readonly validateInputs?: (inputs: GenerationRequestDraft) => void;
79
84
  };
80
85
 
81
86
  export type ExactModelEndpoint = {
@@ -89,6 +94,9 @@ export type ExactModelEndpoint = {
89
94
  readonly mediaBindings: Readonly<Record<string, ExactModelMediaBindingEndpoint>>;
90
95
  readonly textBindings: Readonly<Record<string, ExactModelTextBindingEndpoint>>;
91
96
  readonly ports: GenerationPortTable;
97
+ readonly validateRequest: (request: unknown) => void;
98
+ readonly validateDraft: (draft: unknown) => void;
99
+ readonly sealRequest: (ports: GenerationRequest["ports"]) => GenerationRequest;
92
100
  readonly fragment: ReturnType<typeof sealGraphFragment>;
93
101
  };
94
102
 
@@ -267,7 +275,7 @@ export function plannedExactModelRequest(
267
275
 
268
276
  const knownNeed = state.needs.find((need) => need.id === binding.id);
269
277
  if (knownNeed !== undefined) {
270
- verifyRequestAgainstPorts(endpoint.ports, knownNeed.constraints);
278
+ endpoint.validateRequest(knownNeed.constraints);
271
279
  return {
272
280
  model: endpoint.ports.model,
273
281
  ports: structuredClone((knownNeed.constraints as unknown as GenerationRequestDraft).ports),
@@ -281,6 +289,12 @@ export function plannedExactModelRequest(
281
289
  Object.values(step.outputs).map((record) => [record, step] as const)));
282
290
  const requestRecord = generationStep.inputs.request;
283
291
  if (requestRecord === undefined) return undefined;
292
+ const knownRequest = records.get(requestRecord);
293
+ if (knownRequest !== undefined) {
294
+ const request = inlineValue<GenerationRequest>(knownRequest.value, `${endpoint.ports.model} request`);
295
+ endpoint.validateRequest(request);
296
+ return { model: endpoint.ports.model, ports: structuredClone(request.ports), pendingMedia: [], complete: false };
297
+ }
284
298
  const finalize = producedBy.get(requestRecord);
285
299
  if (finalize === undefined || !sameReference(finalize.producer, endpoint.finalizeProducer)) return undefined;
286
300
  const finalDraft = finalize.inputs.draft;
@@ -295,7 +309,7 @@ export function plannedExactModelRequest(
295
309
  const record = records.get(recordId);
296
310
  if (record !== undefined) {
297
311
  const draft = inlineValue<GenerationRequestDraft>(record.value, `${endpoint.ports.model} request draft`);
298
- verifyRequestDraftAgainstPorts(endpoint.ports, draft);
312
+ endpoint.validateDraft(draft);
299
313
  return draft;
300
314
  }
301
315
  const step = producedBy.get(recordId);
@@ -354,6 +368,8 @@ export function plannedExactModelRequest(
354
368
  };
355
369
 
356
370
  const draft = rebuildDraft(finalDraft);
371
+ endpoint.validateDraft(draft);
372
+ if (pendingMedia.length === 0) endpoint.validateRequest(draft);
357
373
  return {
358
374
  model: endpoint.ports.model,
359
375
  ports: structuredClone(draft.ports),
@@ -502,6 +518,15 @@ export function defineExactModelModule<const Key extends string>(
502
518
  };
503
519
 
504
520
  const endpoints = Object.fromEntries(endpointData.map((item): [Key, ExactModelEndpoint] => {
521
+ const validateRequest = (request: unknown): void => {
522
+ verifyRequestAgainstPorts(item.spec.ports, request);
523
+ item.spec.validateInputs?.(request);
524
+ item.spec.validateRequest?.(request);
525
+ };
526
+ const validateDraft = (draft: unknown): void => {
527
+ verifyRequestDraftAgainstPorts(item.spec.ports, draft);
528
+ item.spec.validateInputs?.(draft);
529
+ };
505
530
  const fragment = sealGraphFragment({
506
531
  inputs: [{ name: "request", type: item.requestType }],
507
532
  operations: [{
@@ -527,6 +552,14 @@ export function defineExactModelModule<const Key extends string>(
527
552
  mediaBindings: item.mediaBindings,
528
553
  textBindings: item.textBindings,
529
554
  ports: item.spec.ports,
555
+ validateRequest,
556
+ validateDraft,
557
+ sealRequest(ports) {
558
+ const request = sealGenerationPortRequest(item.spec.ports, ports);
559
+ item.spec.validateInputs?.(request);
560
+ item.spec.validateRequest?.(request);
561
+ return request;
562
+ },
530
563
  fragment,
531
564
  } satisfies ExactModelEndpoint];
532
565
  }));
@@ -540,12 +573,12 @@ export function defineExactModelModule<const Key extends string>(
540
573
  validators: endpointData.flatMap((item) => [{
541
574
  type: item.requestType,
542
575
  handler({ value }) {
543
- verifyRequestAgainstPorts(item.spec.ports, inlineRequest(value, item.spec.key));
576
+ endpoints[item.spec.key]!.validateRequest(inlineRequest(value, item.spec.key));
544
577
  },
545
578
  }, {
546
579
  type: item.draftType,
547
580
  handler({ value }) {
548
- verifyRequestDraftAgainstPorts(item.spec.ports, inlineRequest(value, `${item.spec.key} draft`));
581
+ endpoints[item.spec.key]!.validateDraft(inlineRequest(value, `${item.spec.key} draft`));
549
582
  },
550
583
  }]),
551
584
  producers: endpointData.flatMap((item) => [
@@ -555,7 +588,7 @@ export function defineExactModelModule<const Key extends string>(
555
588
  const requestRecord = inputs.request;
556
589
  assert(requestRecord !== undefined, `${item.spec.key} request input is missing`);
557
590
  const request = inlineRequest(requestRecord.value, item.spec.key);
558
- verifyRequestAgainstPorts(item.spec.ports, request);
591
+ endpoints[item.spec.key]!.validateRequest(request);
559
592
  return { outputs: {}, needs: { generation: request } };
560
593
  },
561
594
  },
@@ -570,11 +603,13 @@ export function defineExactModelModule<const Key extends string>(
570
603
  verifyGenerationMediaBinding(port, value);
571
604
  const artifact = inputs.artifact!.value;
572
605
  assert(artifact.kind === "blob", `${binding.port} artifact must be a Blob`);
606
+ const bound = bindGenerationMedia(item.spec.ports, draft, binding.port, value, artifact);
607
+ endpoints[item.spec.key]!.validateDraft(bound);
573
608
  return {
574
609
  outputs: {
575
610
  draft: {
576
611
  kind: "inline" as const,
577
- value: canonicalize(bindGenerationMedia(item.spec.ports, draft, binding.port, value, artifact)),
612
+ value: canonicalize(bound),
578
613
  },
579
614
  },
580
615
  needs: {},
@@ -586,11 +621,13 @@ export function defineExactModelModule<const Key extends string>(
586
621
  handler: ({ inputs }: ProducerHandlerContext) => {
587
622
  const draft = inlineValue<GenerationRequestDraft>(inputs.draft!.value, `${item.spec.key} draft`);
588
623
  const text = inlineValue<Text>(inputs.text!.value, `${binding.port} Text`);
624
+ const bound = bindGenerationText(item.spec.ports, draft, binding.port, text);
625
+ endpoints[item.spec.key]!.validateDraft(bound);
589
626
  return {
590
627
  outputs: {
591
628
  draft: {
592
629
  kind: "inline" as const,
593
- value: canonicalize(bindGenerationText(item.spec.ports, draft, binding.port, text)),
630
+ value: canonicalize(bound),
594
631
  },
595
632
  },
596
633
  needs: {},
@@ -599,18 +636,20 @@ export function defineExactModelModule<const Key extends string>(
599
636
  })),
600
637
  {
601
638
  producer: item.finalizeProducer,
602
- handler: ({ inputs }) => ({
603
- outputs: {
604
- request: {
605
- kind: "inline" as const,
606
- value: canonicalize(finalizeGenerationRequestDraft(
607
- item.spec.ports,
608
- inlineValue<GenerationRequestDraft>(inputs.draft!.value, `${item.spec.key} draft`),
609
- )),
639
+ handler: ({ inputs }) => {
640
+ const endpoint = endpoints[item.spec.key]!;
641
+ const draft = inlineValue<GenerationRequestDraft>(inputs.draft!.value, `${item.spec.key} draft`);
642
+ endpoint.validateDraft(draft);
643
+ return {
644
+ outputs: {
645
+ request: {
646
+ kind: "inline" as const,
647
+ value: canonicalize(endpoint.sealRequest(draft.ports)),
648
+ },
610
649
  },
611
- },
612
- needs: {},
613
- }),
650
+ needs: {},
651
+ };
652
+ },
614
653
  },
615
654
  ]),
616
655
  plannedNeeds: Object.values(endpoints).map((endpoint) => exactModelPlannedNeedFacet(endpoint)),
@@ -51,3 +51,13 @@ This is trusted execution, not a sandbox. Builtins, native libraries and process
51
51
  (including `process.cwd()` and `process.env`) remain shared. Existing module bindings do not change
52
52
  under an active Build; a file or dependency first
53
53
  read later uses ordinary filesystem semantics. Scoped loading does not make a project tree immutable.
54
+
55
+ ## Dependency installation options
56
+
57
+ A Distribution package may declare `hypit.dependencyInstallEnv` in its package.json, keyed by its
58
+ ordinary direct external dependency names. Each value is an environment-variable map passed to that
59
+ dependency's explicit npm installation. For example, an owner can disable an SDK's automatic asset
60
+ download and prepare that asset through its ManagedProgram instead. Options do not propagate to
61
+ unrelated installations or the caller's process. Conflicting values from selected packages fail;
62
+ no package wins by discovery order. They are command inputs, not a second dependency inventory,
63
+ readiness record, or new version system. Project packages remain owned by their package manager.
@@ -15,6 +15,7 @@ export {
15
15
  } from "./distribution-resolution.js";
16
16
  export {
17
17
  locateNodePackage,
18
+ NodePackageNotFoundError,
18
19
  externalPackageInstallRoot,
19
20
  resolveNodePackageExecutable,
20
21
  resolveNodePackageResource,
@@ -16,7 +16,7 @@ import { externalPackageRoots, locateNodePackage } from "./location.js";
16
16
  type PackageJson = {
17
17
  readonly name: string;
18
18
  readonly version?: string;
19
- readonly hypit?: { readonly activation?: string };
19
+ readonly hypit?: { readonly activation?: string; readonly dependencyInstallEnv?: Readonly<Record<string, Readonly<Record<string, string>>>> };
20
20
  readonly dependencies: Readonly<Record<string, string>>;
21
21
  };
22
22
 
@@ -43,7 +43,19 @@ function parsePackageJson(value: unknown, subject: string): PackageJson {
43
43
  return {
44
44
  name: text(item.name, `${subject}.name`),
45
45
  ...(item.version === undefined ? {} : { version: text(item.version, `${subject}.version`) }),
46
- ...(hypit?.activation === undefined ? {} : { hypit: { activation: text(hypit.activation, `${subject}.hypit.activation`) } }),
46
+ ...(hypit === undefined ? {} : { hypit: {
47
+ ...(hypit.activation === undefined ? {} : { activation: text(hypit.activation, `${subject}.hypit.activation`) }),
48
+ ...(hypit.dependencyInstallEnv === undefined ? {} : { dependencyInstallEnv: Object.fromEntries(
49
+ Object.entries(object(hypit.dependencyInstallEnv, `${subject}.hypit.dependencyInstallEnv`)).map(([name, raw]) => {
50
+ assert(Object.hasOwn(dependencies, name) && !name.startsWith("@hypit/"), `${subject}: installation environment must name a direct external dependency: ${name}`);
51
+ const env = object(raw, `${subject}.hypit.dependencyInstallEnv.${name}`);
52
+ for (const [key, value] of Object.entries(env)) {
53
+ assert(/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key) && typeof value === "string", `${subject}: invalid installation environment for ${name}`);
54
+ }
55
+ return [name, env as Record<string, string>];
56
+ }),
57
+ ) }),
58
+ } }),
47
59
  dependencies: Object.fromEntries(Object.entries(dependencies).map(([name, version]) => [
48
60
  name,
49
61
  text(version, `${subject}.dependencies.${name}`),
@@ -183,11 +195,11 @@ export class NodePackageSelectionMissingError extends Error {
183
195
  export async function distributionExternalPackageRequirements(
184
196
  specifiers: readonly string[],
185
197
  distributionRoot: string,
186
- ): Promise<readonly { readonly name: string; readonly version: string; readonly specifier: string }[]> {
198
+ ): Promise<readonly { readonly name: string; readonly version: string; readonly specifier: string; readonly env?: Readonly<Record<string, string>> }[]> {
187
199
  const root = resolve(distributionRoot);
188
200
  const queue = [...new Set(specifiers.filter((name) => name.startsWith("@hypit/")))].sort();
189
201
  const visited = new Set<string>();
190
- const external = new Map<string, { readonly name: string; readonly version: string; readonly specifier: string }>();
202
+ const external = new Map<string, { readonly name: string; readonly version: string; readonly specifier: string; readonly env?: Readonly<Record<string, string>> }>();
191
203
  while (queue.length > 0) {
192
204
  const name = queue.shift()!;
193
205
  if (visited.has(name)) continue;
@@ -199,7 +211,15 @@ export async function distributionExternalPackageRequirements(
199
211
  continue;
200
212
  }
201
213
  const specifier = `${dependency}@${version}`;
202
- external.set(specifier, { name: dependency, version, specifier });
214
+ const requested = physical.json.hypit?.dependencyInstallEnv?.[dependency];
215
+ const previous = external.get(specifier);
216
+ const env = { ...previous?.env };
217
+ for (const [key, value] of Object.entries(requested ?? {})) {
218
+ assert(env[key] === undefined || env[key] === value, `Conflicting installation environment ${key} for ${specifier}`);
219
+ env[key] = value;
220
+ }
221
+ external.set(specifier, { name: dependency, version, specifier,
222
+ ...(Object.keys(env).length === 0 ? {} : { env }) });
203
223
  }
204
224
  }
205
225
  return [...external.values()].sort((left, right) => left.specifier.localeCompare(right.specifier));
@@ -2,6 +2,14 @@ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "n
2
2
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
+ /** Only absence is repairable by installing a package; malformed manifests and I/O errors are not. */
6
+ export class NodePackageNotFoundError extends Error {
7
+ constructor(name: string) {
8
+ super(`cannot locate installed package ${name}`);
9
+ this.name = "NodePackageNotFoundError";
10
+ }
11
+ }
12
+
5
13
  export type LocatedNodePackage = {
6
14
  readonly root: string;
7
15
  readonly manifest: {
@@ -88,7 +96,8 @@ export function declaredExternalPackageRoot(root: string, from: string | URL, na
88
96
  // An upstream asset a package ships as optional is still selected by one exact version here.
89
97
  // `hypit packages install` places it under the machine npm root named by this selection, so
90
98
  // reading only the required map makes the documented repair unusable for every optional asset.
91
- const version = value.dependencies?.[name] ?? value.optionalDependencies?.[name];
99
+ // npm lets optionalDependencies override dependencies with the same name.
100
+ const version = value.optionalDependencies?.[name] ?? value.dependencies?.[name];
92
101
  return version === undefined ? undefined : externalPackageInstallRoot(root, name, version);
93
102
  }
94
103
  const parent = dirname(cursor);
@@ -223,7 +232,7 @@ export function locateNodePackage(nameValue: string, options: LocateNodePackageO
223
232
  if (found !== undefined) return found;
224
233
  }
225
234
  }
226
- throw new Error(`cannot locate installed package ${name}`);
235
+ throw new NodePackageNotFoundError(name);
227
236
  }
228
237
 
229
238
  function packageFile(
@@ -6,6 +6,8 @@ It does not load Providers, install programs, inspect credentials or select serv
6
6
 
7
7
  1. `resolveProjectRoot({ workspaceRoot?, cwd? })` uses the explicit Workspace, otherwise the nearest
8
8
  `package.json` above cwd, otherwise cwd itself. Source and Run filenames do not choose the project.
9
+ After selecting the existing directory, it returns its real filesystem path so project and Source
10
+ paths use the same representation. Symlinks do not redirect the preceding parent-project search.
9
11
  2. `findRuntimeProfile(projectRoot)` reads only that project's `.hypit/runtime` file and resolves its
10
12
  path relative to the project. An entrypoint's explicit `--runtime` overrides this read for that invocation.
11
13
  3. The selected Runtime implementation interprets the Profile, including `dataRoot`, Credentials,
@@ -1,4 +1,4 @@
1
- import { stat } from "node:fs/promises";
1
+ import { realpath, stat } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
 
4
4
  async function nearestProjectPackageRoot(start: string): Promise<string | undefined> {
@@ -28,8 +28,10 @@ export async function resolveProjectRoot(options: {
28
28
  readonly cwd?: string;
29
29
  } = {}): Promise<string> {
30
30
  const start = resolve(options.workspaceRoot ?? options.cwd ?? process.cwd());
31
- if (options.workspaceRoot !== undefined) return start;
32
- return await nearestProjectPackageRoot(start) ?? start;
31
+ const selected = options.workspaceRoot !== undefined ? start : await nearestProjectPackageRoot(start) ?? start;
32
+ // Select through the caller's directory first; following a link before discovery
33
+ // could choose a different parent project. Source readers also return real paths.
34
+ return await realpath(selected);
33
35
  }
34
36
 
35
37
  /** Project package discovery cannot escape an already resolved project. */
@@ -2,7 +2,7 @@
2
2
 
3
3
  Trusted local Provider for the `@hypit/render-hyperframes#render-visual` capability. It stages the
4
4
  Resource dependencies declared by a `HyperframesDocument`, renders a silent MP4 with the
5
- HyperFrames engine, probes the bytes, and returns a verified `RenderedVisual`. Before staging a typed
5
+ HyperFrames engine, probes the bytes, and returns a verified `RenderedVisual`. Before capturing a typed
6
6
  Surface it decodes the exact bytes and checks declared dimensions, still/frame timing, SDR/sRGB and
7
7
  opaque/straight-alpha facts. These checks validate the typed rendering input; they do not create
8
8
  content identity or hidden output metadata.
@@ -64,12 +64,67 @@ GPU, or `auto` to let the engine decide. Capture uses screenshots and independen
64
64
  the CLI's automatic worker and drawElement policies do not override the count. The opaque screenshot
65
65
  adapter uses the engine's public session, video injector and page seek protocol plus Chrome CDP.
66
66
  It waits for seek completion, dynamic images/fonts and the page compositor before capturing PNGs.
67
+ Image readiness includes CSS class and pseudo-element images, CSS masks and SVG images. A failed
68
+ image decode reports its URL instead of producing a successful frame with missing media. Failed
69
+ declared fonts also fail capture instead of silently leaving fallback glyphs in the output.
67
70
  The pinned engine couples its PNG session setup to transparent export, so this adapter initializes
68
71
  an opaque session and chooses PNG separately at capture. It does not patch engine methods or files.
69
72
 
70
- The Runtime Adapter also declares one managed browser program. `programs up` invokes the pinned
71
- HyperFrames CLI's `browser ensure`; its probe resolves and starts that browser and checks
72
- the FFmpeg/FFprobe toolchain before a Build.
73
+ The Runtime Adapter declares one managed browser program. Prepare it explicitly:
74
+
75
+ ```sh
76
+ hypit programs up --runtime ./hypit.runtime.json --endpoint hyperframes.local
77
+ hypit doctor --runtime ./hypit.runtime.json
78
+ ```
79
+
80
+ Use the instance name from the Profile. `runtime up` also prepares it and starts the Runtime Worker.
81
+ The Provider's `package.json` declares its recommended Chrome Headless Shell version in
82
+ `hypit.renderBrowser.version`, alongside the engine dependency used to test that release. The
83
+ installer consumes that declaration; it does not have a browser version constant or fetch a
84
+ "latest" channel. `config.browserVersion` explicitly selects another exact four-part version.
85
+ Changing the recommendation belongs to a Provider release and requires real rendering tests; a
86
+ Puppeteer recommendation alone is not a HyperFrames compatibility guarantee.
87
+
88
+ The default cache is `~/.cache/hyperframes/chrome`; `config.browserCacheDirectory` selects another
89
+ location. Only the selected version is used, regardless of other cached or system browsers.
90
+ Projects sharing this cache use the existing ManagedProgram preparation lock/logs under
91
+ `.hypit-render-program`. Readiness comes from the executable and its reported version, not a receipt.
92
+ Explicit preparation reuses a healthy installation or repairs only the selected managed version.
93
+ A failed download reports failure without selecting another version or browser.
94
+
95
+ `config.browserDownloadBaseUrl` selects a Chrome for Testing archive mirror for explicit preparation.
96
+ It is an absolute HTTP(S) base URL, without credentials, a query or a fragment. The browser library
97
+ appends the selected version, platform and archive name; the mirror must serve that same layout.
98
+ For example, a base of `https://mirror.example/chrome-for-testing` serves archives beneath
99
+ `<base>/<version>/<platform>/chrome-headless-shell-<platform>.zip`. Choose a source maintained by
100
+ the user or organization; this Provider does not keep a mirror list or choose one by region.
101
+ Omitting the setting uses the browser library's official Chrome for Testing source. A configured
102
+ source replaces it: failed transfers or invalid archives fail preparation without trying the official
103
+ source or another mirror. Normal HTTP redirects supplied by the selected server are handled by the
104
+ download library. Preparation displays the complete archive URL, version and destination before
105
+ download. An npm registry setting does not redirect this binary download.
106
+
107
+ The download source only determines where missing installation bytes come from. It does not change
108
+ the executable selection, and changing it does not invalidate a healthy cached version. No source
109
+ receipt is stored. To install into an empty location, explicitly choose `browserCacheDirectory`.
110
+
111
+ `config.chromePath` selects a user-managed Chrome/Chromium executable. It cannot be combined with
112
+ `browserVersion` or `browserDownloadBaseUrl`; invalid combinations fail instead of assigning precedence. Relative paths resolve
113
+ from the Runtime Profile root. This mode never downloads or repairs a browser. Its version remains
114
+ under the user's control, including system-browser auto-updates. `HYPERFRAMES_BROWSER_PATH` and
115
+ `PRODUCER_HEADLESS_SHELL_PATH` do not select browsers in this Provider; configure `chromePath`.
116
+ On platforms without a supported managed download, explicitly select an installed browser.
117
+
118
+ `doctor` displays the selected path and its source, and only inspects it. Build preflight, rendering
119
+ and previews never install a browser. `programs up` / `runtime up` display the selected managed
120
+ version, installation location and download URL before running preparation. The probe runs `--version` and checks
121
+ FFmpeg/FFprobe; it does not promise GPU or page compatibility. Capture receives that same selected
122
+ path as the engine's `chromePath`, including its GPU probe. Active Workers keep their loaded package
123
+ recommendation; restart them explicitly after changing Profile or package dependencies.
124
+
125
+ The Provider's `hypit.dependencyInstallEnv` disables Puppeteer's browser download while preparing its
126
+ engine/producer npm dependencies. The repository `.puppeteerrc.cjs` does the same for checkout installs.
127
+ No browser postinstall allowlist is required. The former `hyperframesCliPath` option is removed.
73
128
 
74
129
  The same executor is exported for callers with an already compiled document:
75
130
 
@@ -96,11 +151,31 @@ execution earlier. ResourceStore I/O and Surface probes receive the cancellation
96
151
  ResourceStore must implement the port's cancellation behavior, including streaming reads and writes.
97
152
 
98
153
  Browser launch, source extraction, capture and encoding run in one disposable child process per
99
- render. At cancellation it receives a stop request and has up to five seconds to clean up. The owner
100
- then terminates any remaining process tree, including Chrome's separate process groups, and awaits
101
- the child exit before removing temporary files and returning failure. This also covers engine calls
102
- that do not accept a signal. The deadline initiates shutdown; the call may spend additional time
103
- closing resources. Completed Outputs in the Build remain available for a new Run and Build.
154
+ render. After successful capture closes its resources, the child sends its completion message,
155
+ flushes that message and disconnects IPC so it can exit normally. The owner awaits exit and drains
156
+ diagnostics before returning. Normal completion does not enumerate or forcibly terminate processes.
157
+ On failure, cleanup may be incomplete: the child reports the error and keeps IPC open while the owner
158
+ discovers and terminates the remaining process tree, before it can become orphaned.
159
+ The capture child also installs synchronous exit cleanup before loading the engine. Early
160
+ `process.exit()`, uncaught exceptions, and catchable `SIGINT`/`SIGTERM` exits stop descendants
161
+ while their owner still exists, even if browser initialization never returned a session.
162
+ Successful resource closure removes this exit handler. No historical browser PID list is retained.
163
+
164
+ Uncatchable termination (`SIGKILL`, native crashes, or OS termination on Windows) cannot run that
165
+ cleanup. The owner reports the termination and cannot confirm descendant cleanup; it never searches
166
+ by an already-exited root PID. If an orphan keeps the output pipes open, the owner closes its pipe
167
+ ends after five seconds so the failed invocation can settle. Guaranteed cleanup after an OS hard
168
+ kill requires containment supplied by that deployment (for example a process job or container);
169
+ this local Node implementation does not provide that guarantee.
170
+
171
+ At cancellation the child receives a stop request and has up to five seconds to clean up. A child
172
+ that remains after cancellation or its completion message is forcibly terminated along with its discovered
173
+ process tree, including Chrome's separate process groups. Cleanup problems are reported through the
174
+ existing diagnostic callback; they do not discard a render already reported as completed. If process
175
+ enumeration fails, the owner still terminates the direct child but cannot confirm descendant cleanup.
176
+ This also covers engine calls that do not accept a signal. The deadline initiates shutdown; the call
177
+ may spend additional time closing resources. Completed Outputs in the Build remain available for a
178
+ new Run and Build.
104
179
 
105
180
  Deployments may additionally set `initializationTimeoutMs` or `frameTimeoutMs` when they have a
106
181
  measured stage deadline. Initialization here means initializing an already created browser session;
@@ -109,7 +184,10 @@ more of the shared render budget on a slow initialization or frame.
109
184
  An explicit stage-timeout error names the worker and stage/frame, aborts sibling workers and awaits
110
185
  cleanup. A completed worker closes its Chrome immediately.
111
186
 
112
- One call stages the HTML and every declared asset once. Typed Surface validation includes a complete
187
+ One call stages the HTML and every declared asset once. Typed Surface inspection reads the completed
188
+ staged file directly, without retaining its chunks, assembling another whole-file buffer, or writing
189
+ a second temporary copy. The caller keeps that file until inspection and capture have settled.
190
+ Typed Surface validation includes a complete
113
191
  decoded-frame count, even for a short render interval. The renderer then finds source-frame windows
114
192
  needed by that interval, merges overlapping windows, and extracts them one source/window at a time.
115
193
  Decoded PNGs are shared by all workers in this call. Each worker initializes its own page, then takes
@@ -125,9 +203,13 @@ do not share staged files or decoded PNGs. Exact compiler sampling markers retai
125
203
  fractional playback rates. There is no SVML rewrite, intermediate cut MP4 or repeat normalization.
126
204
  Already compiled video documents need the current compiler's frame markers.
127
205
 
128
- Source extraction uses the pinned engine's FFmpeg/FFprobe resolver. `ffmpegPath` selects the final
129
- H.264 encoder; `ffprobePath` verifies output and typed surfaces. `nodePath` and `hyperframesCliPath`
130
- serve managed browser installation. The requested frame range travels in the Model's Need.
206
+ `ffmpegPath` selects both source decoding and final H.264 encoding; `ffprobePath` selects source,
207
+ output and typed Surface inspection. Bare commands resolve through PATH. The capture child passes
208
+ these selected executables to the engine's public binary overrides; inherited
209
+ `HYPERFRAMES_FFMPEG_PATH` / `HYPERFRAMES_FFPROBE_PATH` cannot select a different decoding toolchain.
210
+ `nodePath` selects the managed
211
+ browser installer's Node executable. Capture uses the current Node process's executable.
212
+ The requested frame range travels in the Model's Need; browser paths stay in the Provider.
131
213
 
132
214
  ```ts
133
215
  import { createLocalHyperframesProvider } from "@hypit/provider-hyperframes-local";
@@ -8,7 +8,18 @@
8
8
  ".": "./src/index.ts"
9
9
  },
10
10
  "hypit": {
11
- "activation": "./src/activation.ts"
11
+ "activation": "./src/activation.ts",
12
+ "renderBrowser": {
13
+ "version": "152.0.7928.2"
14
+ },
15
+ "dependencyInstallEnv": {
16
+ "@hyperframes/engine": {
17
+ "PUPPETEER_SKIP_DOWNLOAD": "true"
18
+ },
19
+ "@hyperframes/producer": {
20
+ "PUPPETEER_SKIP_DOWNLOAD": "true"
21
+ }
22
+ }
12
23
  },
13
24
  "dependencies": {
14
25
  "@hyperframes/engine": "0.7.101",
@@ -17,13 +28,12 @@
17
28
  "@hypit/hyperframes": "workspace:*",
18
29
  "@hypit/media": "workspace:*",
19
30
  "@hypit/media-execution": "workspace:*",
20
- "@hypit/package-loader-node": "workspace:*",
21
31
  "@hypit/protocol": "workspace:*",
22
32
  "@hypit/render-hyperframes": "workspace:*",
23
33
  "@hypit/runtime": "workspace:*",
24
34
  "@hypit/runtime-host-node": "workspace:*",
25
35
  "@hypit/runtime-kit": "workspace:*",
26
- "hyperframes": "0.7.101",
36
+ "@puppeteer/browsers": "3.2.2",
27
37
  "tsx": "4.21.0"
28
38
  },
29
39
  "devDependencies": {
@@ -1,3 +1,4 @@
1
+ import { resolve } from "node:path";
1
2
  import {
2
3
  createRuntimeEndpointAdapterFacet,
3
4
  runtimeConfigExact,
@@ -11,7 +12,7 @@ import {
11
12
  } from "@hypit/runtime-host-node";
12
13
 
13
14
  import { createLocalHyperframesProvider } from "./provider.js";
14
- import { defaultHyperframesCliPath } from "./provider.js";
15
+ import { configuredBrowserPath, browserExecutablePath, selectedBrowserVersion } from "./browser.js";
15
16
  import type { HyperframesBrowserGpu, HyperframesQuality, HyperframesWorkers } from "./provider.js";
16
17
  import { localHyperframesBrowserProgram } from "./program.js";
17
18
 
@@ -21,11 +22,10 @@ const localHyperframesRuntimeAdapter = createRuntimeEndpointAdapterFacet({
21
22
  if (context.pool === undefined) throw new Error("local HyperFrames Provider Pool is required");
22
23
  const config = runtimeConfigObject(context.config, "local HyperFrames");
23
24
  runtimeConfigExact(config, [
24
- "nodePath", "hyperframesCliPath", "ffprobePath", "ffmpegPath", "workers", "maxWorkers", "quality", "browserGpu",
25
+ "nodePath", "chromePath", "browserVersion", "browserCacheDirectory", "browserDownloadBaseUrl", "ffprobePath", "ffmpegPath", "workers", "maxWorkers", "quality", "browserGpu",
25
26
  "defaultConcurrency", "browserCapacity", "initializationTimeoutMs", "frameTimeoutMs", "processTimeoutMs", "maxProcessOutputBytes", "maxRenderedBytes",
26
27
  ], "local HyperFrames");
27
28
  runtimeConfigString(config.nodePath, "HyperFrames nodePath");
28
- runtimeConfigString(config.hyperframesCliPath, "HyperFrames hyperframesCliPath");
29
29
  runtimeConfigString(config.ffprobePath, "HyperFrames ffprobePath");
30
30
  const workers = config.workers;
31
31
  if (workers !== undefined && workers !== "auto") {
@@ -40,10 +40,21 @@ const localHyperframesRuntimeAdapter = createRuntimeEndpointAdapterFacet({
40
40
  throw new Error("HyperFrames browserGpu is invalid");
41
41
  }
42
42
  const configuredNode = runtimeConfigString(config.nodePath, "HyperFrames nodePath");
43
- const configuredCli = runtimeConfigString(config.hyperframesCliPath, "HyperFrames hyperframesCliPath");
43
+ const configuredChrome = runtimeConfigString(config.chromePath, "HyperFrames chromePath");
44
+ const browserVersion = runtimeConfigString(config.browserVersion, "HyperFrames browserVersion");
45
+ const browserDownloadBaseUrl = runtimeConfigString(config.browserDownloadBaseUrl, "HyperFrames browserDownloadBaseUrl");
46
+ const configuredCache = runtimeConfigString(config.browserCacheDirectory, "HyperFrames browserCacheDirectory");
47
+ const chromePath = configuredBrowserPath({ ...(configuredChrome === undefined ? {} : {
48
+ chromePath: resolve(context.dataRoot, configuredChrome),
49
+ }) });
50
+ const browser = {
51
+ ...(browserVersion === undefined ? {} : { browserVersion }),
52
+ ...(browserDownloadBaseUrl === undefined ? {} : { browserDownloadBaseUrl }),
53
+ ...(chromePath === undefined ? {} : { chromePath }),
54
+ ...(configuredCache === undefined ? {} : { browserCacheDirectory: resolve(context.dataRoot, configuredCache) }),
55
+ };
44
56
  const configuredFfprobe = runtimeConfigString(config.ffprobePath, "HyperFrames ffprobePath");
45
57
  const nodePath = resolveRuntimeExecutable(context.dataRoot, configuredNode ?? process.execPath);
46
- const hyperframesCliPath = () => resolveRuntimeExecutable(context.dataRoot, configuredCli ?? defaultHyperframesCliPath());
47
58
  const configuredFfmpeg = runtimeConfigString(config.ffmpegPath, "HyperFrames ffmpegPath");
48
59
  const ffmpegPath = resolveRuntimeExecutable(context.dataRoot, configuredFfmpeg ?? "ffmpeg");
49
60
  const ffprobePath = resolveRuntimeExecutable(context.dataRoot, configuredFfprobe ?? "ffprobe");
@@ -60,6 +71,7 @@ const localHyperframesRuntimeAdapter = createRuntimeEndpointAdapterFacet({
60
71
  instance: context.instance,
61
72
  pool: context.pool,
62
73
  nodePath,
74
+ ...browser,
63
75
  ffprobePath,
64
76
  ffmpegPath,
65
77
  ...(workers === undefined ? {} : { workers: workers as HyperframesWorkers }),
@@ -77,11 +89,13 @@ const localHyperframesRuntimeAdapter = createRuntimeEndpointAdapterFacet({
77
89
  program: localHyperframesBrowserProgram({
78
90
  id: context.instance,
79
91
  nodePath,
80
- hyperframesCliPath,
92
+ ...browser,
81
93
  ffprobePath,
82
94
  ffmpegPath,
83
95
  }),
84
96
  diagnose: async () => [
97
+ { severity: "info", code: "HYPERFRAMES_BROWSER_SELECTION", subject: context.instance,
98
+ message: `${chromePath === undefined ? `Managed Chrome Headless Shell ${selectedBrowserVersion(browser)} (${browserVersion === undefined ? "Provider recommendation" : "Profile version"})` : "Profile browser"}: ${browserExecutablePath(browser)}` },
85
99
  ...await diagnoseRuntimeExecutable({ root: context.dataRoot, configured: configuredFfmpeg, fallback: "ffmpeg", subject: "FFmpeg" }),
86
100
  ...await diagnoseRuntimeExecutable({
87
101
  root: context.dataRoot,
@@ -0,0 +1,5 @@
1
+ import { installRenderBrowser } from "./browser.js";
2
+
3
+ const [cacheDir, version, baseUrl] = process.argv.slice(2);
4
+ if (process.argv.length < 4 || process.argv.length > 5 || !cacheDir || !version) throw new Error("Expected the render browser cache directory, exact version and optional archive base URL");
5
+ await installRenderBrowser(cacheDir, version, baseUrl);