@elpapi42/pi-fleet 0.1.0-beta.2 → 0.1.0-beta.5

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/CHANGELOG.md CHANGED
@@ -12,6 +12,47 @@
12
12
  - Added a nightly reliability workflow and an isolated soak suite covering 500 concurrent ordered sends and 100 name lifecycle cycles.
13
13
  - Reject invalid UTF-8 in stdin and Pi RPC stdout instead of silently replacing bytes at either protocol boundary.
14
14
 
15
+ ## 0.1.0-beta.5 — 2026-07-20
16
+
17
+ Release-pipeline reliability follow-up; includes the beta.4 receive and argument-validation fixes.
18
+
19
+ ### Fixed
20
+
21
+ - On Linux, process-group cleanup now treats zombie processes as exited by inspecting procfs instead of relying only on signal probes. This prevents conservative cleanup waits and CI failures when an orphaned descendant is awaiting reaping.
22
+ - Pi shutdown now waits for the child-process exit callback and coordinator transition before reporting completion, preventing a status read from briefly retaining `resident` after an idle release.
23
+ - Repeated the process-tree and real-Pi process suites five times locally before tagging after beta.4's publish workflow exposed the runner-specific zombie behavior.
24
+
25
+ ## 0.1.0-beta.4 — 2026-07-20
26
+
27
+ Manual edge-case testing fix. No intentional CLI contract change.
28
+
29
+ ### Fixed
30
+
31
+ - Prevented `receive --timeout 0` from rejecting an idle waiter before a handler was attached and terminating the runtime with an unhandled `Receive cancelled` rejection.
32
+ - Added explicit coordinator work tracking so promptless/repeated idle polling returns `no_response` or the latest response immediately, while managed work still waits for Pi settlement.
33
+ - Closed the abort-listener registration race and added regression coverage for timeout, disconnect, repeated receive, and settlement ordering.
34
+ - Return durable `invalid_arguments` errors for rejected Pi startup/positional arguments instead of a generic `internal_error`.
35
+
36
+ ### Manual validation
37
+
38
+ - Direct isolated lifecycle, timeout, repeated receive, raw watch, session mutation, concurrent startup/send, Pi/runtime crash recovery, capacity, and session-preservation scenarios passed against the beta.4 source build.
39
+ - Fresh-registry operational validation remains enforced by the tag workflow after publication.
40
+
41
+ ## 0.1.0-beta.3 — 2026-07-20
42
+
43
+ Reliability fix for fresh global npm installations. No intentional CLI or agent-lifecycle contract change.
44
+
45
+ ### Fixed
46
+
47
+ - Replaced the invalid assumption that npm recreates CI's byte-identical `node_modules` tree. Schema-3 runtime manifests retain exact hashes for Fleet-owned files, validate direct dependency name/version, derive the observed dependency-closure hash at materialization, and prove source-before, staged, and source-after closure equality before activation.
48
+ - Made immutable runtime release paths closure-specific, so different legitimate npm dependency layouts do not collide.
49
+ - Strengthened package testing to mutate a fresh installed dependency tree, require an operational `list`, remove the npm installation, stop the runtime, and restart through the materialized release.
50
+
51
+ ### Installation scope
52
+
53
+ - The documented global npm installation layout, where dependencies are package-local, is supported. Arbitrarily hoisted local-prefix, pnpm, and unusual `npx` dependency layouts are not yet supported.
54
+ - The tag workflow now performs a fresh registry-install operational `list` smoke after publishing. That smoke is a release gate and has not yet run for beta.3.
55
+
15
56
  ## 0.1.0-beta.2 — 2026-07-20
16
57
 
17
58
  Reliability-focused beta maintenance release. No intentional product-contract changes.
package/README.md CHANGED
@@ -8,7 +8,7 @@ create · send · receive · status · list · watch · destroy
8
8
 
9
9
  Fleet keeps a Pi process resident when possible, restores it from the same native Pi session when absent, accepts repeated steering input, and returns the latest assistant message after Pi becomes idle. Pi sessions remain under your control: Fleet references them but never copies or deletes them.
10
10
 
11
- > **Beta:** `0.1.0-beta.2` is validated on Linux x64 with Pi `0.80.10`, including deterministic fault, recovery, package-compatibility, and resource-stability testing. Actual logout/reboot recovery, macOS launchd/containment, automatic upgrades, and public service-management UX are not yet release-validated.
11
+ > **Beta:** `0.1.0-beta.5` has passed deterministic Linux x64 fault, recovery, package, compatibility, and resource-stability tests with Pi `0.80.10`. Its tag workflow additionally requires a fresh registry-install operational smoke after publication; that external check has not yet run for beta.3. Actual logout/reboot recovery, macOS launchd/containment, automatic upgrades, and public service-management UX are not yet release-validated.
12
12
 
13
13
  ## Install
14
14
 
@@ -173,7 +173,7 @@ Do not include API keys, message contents, session contents, or private paths un
173
173
 
174
174
  ## Beta limitations
175
175
 
176
- - Current deterministic reliability and package validation covers Linux x64 only.
176
+ - Current deterministic reliability and documented global-install package validation cover Linux x64 only. Arbitrarily hoisted local-prefix, pnpm, and unusual `npx` dependency layouts are not yet supported.
177
177
  - Actual logout and host reboot recovery remain unvalidated.
178
178
  - launchd and macOS descendant containment remain unvalidated.
179
179
  - Real disk-exhaustion behavior and multi-hour resource growth remain unvalidated; bounded SQLite and in-process resource tests are covered.
@@ -423,7 +423,7 @@
423
423
  "format": "esm"
424
424
  },
425
425
  "src/platform/install/tree-integrity.ts": {
426
- "bytes": 2225,
426
+ "bytes": 2529,
427
427
  "imports": [
428
428
  {
429
429
  "path": "node:crypto",
@@ -444,7 +444,7 @@
444
444
  "format": "esm"
445
445
  },
446
446
  "src/platform/install/runtime-release.ts": {
447
- "bytes": 9583,
447
+ "bytes": 15742,
448
448
  "imports": [
449
449
  {
450
450
  "path": "node:crypto",
@@ -593,7 +593,7 @@
593
593
  "imports": [],
594
594
  "exports": [],
595
595
  "inputs": {},
596
- "bytes": 260315
596
+ "bytes": 270001
597
597
  },
598
598
  "dist/cli.mjs": {
599
599
  "imports": [
@@ -828,16 +828,16 @@
828
828
  "bytesInOutput": 4658
829
829
  },
830
830
  "src/platform/install/runtime-release.ts": {
831
- "bytesInOutput": 8505
831
+ "bytesInOutput": 13651
832
832
  },
833
833
  "src/platform/install/tree-integrity.ts": {
834
- "bytesInOutput": 1879
834
+ "bytesInOutput": 2182
835
835
  },
836
836
  "src/platform/shared/paths.ts": {
837
837
  "bytesInOutput": 1191
838
838
  }
839
839
  },
840
- "bytes": 144920
840
+ "bytes": 150369
841
841
  }
842
842
  }
843
843
  }
package/dist/cli.mjs CHANGED
@@ -3385,7 +3385,7 @@ function splitArgv(argv) {
3385
3385
 
3386
3386
  // src/shared/product-identity.ts
3387
3387
  var PRODUCT_BINARY = "pifleet";
3388
- var PRODUCT_VERSION = "0.1.0-beta.2";
3388
+ var PRODUCT_VERSION = "0.1.0-beta.5";
3389
3389
 
3390
3390
  // src/cli/commands/create.ts
3391
3391
  import { resolve } from "node:path";
@@ -3907,12 +3907,12 @@ import { promisify } from "node:util";
3907
3907
 
3908
3908
  // src/platform/install/runtime-release.ts
3909
3909
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
3910
- import { chmod, cp, lstat as lstat2, mkdir, readFile as readFile2, rename, rm } from "node:fs/promises";
3910
+ import { chmod, cp, lstat as lstat2, mkdir, readFile as readFile2, rename, rm, writeFile } from "node:fs/promises";
3911
3911
  import { join as join2, posix, resolve as resolve3, sep as sep2 } from "node:path";
3912
3912
 
3913
3913
  // src/platform/install/tree-integrity.ts
3914
3914
  import { createHash } from "node:crypto";
3915
- import { lstat, readFile, readdir, stat } from "node:fs/promises";
3915
+ import { lstat, readFile, readdir, realpath, stat } from "node:fs/promises";
3916
3916
  import { join, relative, resolve as resolve2, sep } from "node:path";
3917
3917
  async function hashDirectoryTree(root, manifestPath) {
3918
3918
  const resolvedRoot = resolve2(root);
@@ -3952,6 +3952,14 @@ async function collectFiles(root, directory) {
3952
3952
  if (!info.isFile()) {
3953
3953
  throw new Error(`Runtime dependency tree contains an unsupported entry: ${absolutePath}`);
3954
3954
  }
3955
+ if (linkInfo.isSymbolicLink()) {
3956
+ const target = await realpath(absolutePath);
3957
+ if (target !== root && !target.startsWith(`${root}${sep}`)) {
3958
+ throw new Error(
3959
+ `Runtime dependency tree contains an external file symlink: ${absolutePath}`
3960
+ );
3961
+ }
3962
+ }
3955
3963
  const relativePath = relative(root, absolutePath).split(sep).join("/");
3956
3964
  if (relativePath.length === 0 || relativePath.startsWith("../")) {
3957
3965
  throw new Error(`Runtime dependency path escapes its root: ${absolutePath}`);
@@ -3970,19 +3978,33 @@ var requiredRuntimeArtifacts = /* @__PURE__ */ new Set([
3970
3978
  "dist/runtime.mjs",
3971
3979
  "dist/sqlite-worker.mjs"
3972
3980
  ]);
3981
+ var dependencyTreePath = "node_modules";
3973
3982
  async function materializeRuntime(options) {
3974
3983
  const sourceRoot = resolve3(options.sourceRoot);
3975
3984
  const manifestBytes = await readFile2(join2(sourceRoot, "dist", "runtime-manifest.json"));
3976
- const manifest = await parseRuntimeManifest(manifestBytes, sourceRoot);
3977
- await verifyRuntime(sourceRoot, manifest);
3985
+ const sourceManifest = await parseRuntimeManifest(manifestBytes, sourceRoot);
3986
+ if (sourceManifest.closure !== void 0) {
3987
+ await verifyRuntime(sourceRoot);
3988
+ return sourceRoot;
3989
+ }
3990
+ await verifyRuntimeFiles(sourceRoot, sourceManifest);
3991
+ await verifyDependencyIdentities(sourceRoot, sourceManifest.dependencies);
3992
+ const sourceTreeRoot = resolveInside(sourceRoot, dependencyTreePath);
3993
+ const sourceTreeBefore = await hashDirectoryTree(sourceTreeRoot, dependencyTreePath);
3994
+ const sourceManifestSha256 = createHash2("sha256").update(manifestBytes).digest("hex");
3995
+ const materializedManifest = {
3996
+ ...sourceManifest,
3997
+ closure: { sourceManifestSha256, tree: sourceTreeBefore }
3998
+ };
3999
+ const materializedManifestBytes = serializeManifest(materializedManifest);
4000
+ const closureHash = createHash2("sha256").update(sourceManifestSha256).update("\0").update(JSON.stringify(sourceTreeBefore)).digest("hex").slice(0, 16);
3978
4001
  const applicationRoot = resolve3(options.applicationRoot);
3979
4002
  await ensurePrivateDirectory(applicationRoot);
3980
- const manifestHash = createHash2("sha256").update(manifestBytes).digest("hex").slice(0, 16);
3981
4003
  const releasesRoot = join2(applicationRoot, "releases");
3982
4004
  await ensurePrivateDirectory(releasesRoot);
3983
- const destination = join2(releasesRoot, `${manifest.package.version}-${manifestHash}`);
4005
+ const destination = join2(releasesRoot, `${sourceManifest.package.version}-${closureHash}`);
3984
4006
  if (await pathExists(destination)) {
3985
- await verifyRuntime(destination, manifest);
4007
+ await verifyExpectedRuntime(destination, materializedManifest, materializedManifestBytes);
3986
4008
  return destination;
3987
4009
  }
3988
4010
  const staging = join2(releasesRoot, `.staging-${randomUUID2()}`);
@@ -3992,36 +4014,73 @@ async function materializeRuntime(options) {
3992
4014
  recursive: true,
3993
4015
  dereference: true
3994
4016
  });
3995
- await cp(join2(sourceRoot, "bin"), join2(staging, "bin"), { recursive: true, dereference: true });
4017
+ await cp(join2(sourceRoot, "bin"), join2(staging, "bin"), {
4018
+ recursive: true,
4019
+ dereference: true
4020
+ });
3996
4021
  await cp(join2(sourceRoot, "package.json"), join2(staging, "package.json"));
3997
- for (const tree of manifest.trees) {
3998
- const source = resolveInside(sourceRoot, tree.path);
3999
- const destination2 = resolveInside(staging, tree.path);
4000
- await mkdir(resolve3(destination2, ".."), { recursive: true, mode: 448 });
4001
- await cp(source, destination2, { recursive: true, dereference: true });
4002
- }
4003
- await verifyRuntime(staging, manifest);
4022
+ await cp(sourceTreeRoot, join2(staging, dependencyTreePath), {
4023
+ recursive: true,
4024
+ dereference: true
4025
+ });
4026
+ await options.hooks?.afterDependencyCopy?.();
4027
+ const stagedTree = await hashDirectoryTree(
4028
+ join2(staging, dependencyTreePath),
4029
+ dependencyTreePath
4030
+ );
4031
+ assertSameTree(sourceTreeBefore, stagedTree, "copied dependency closure");
4032
+ const sourceTreeAfter = await hashDirectoryTree(sourceTreeRoot, dependencyTreePath);
4033
+ assertSameTree(sourceTreeBefore, sourceTreeAfter, "source dependency closure");
4034
+ await writeFile(join2(staging, "dist", "runtime-manifest.json"), materializedManifestBytes);
4035
+ await verifyExpectedRuntime(staging, materializedManifest, materializedManifestBytes);
4004
4036
  await chmod(staging, 448);
4005
4037
  try {
4006
4038
  await rename(staging, destination);
4007
4039
  } catch (error) {
4008
4040
  if (!isDestinationRace(error) || !await pathExists(destination)) throw error;
4009
- await verifyRuntime(destination, manifest);
4041
+ await verifyExpectedRuntime(destination, materializedManifest, materializedManifestBytes);
4010
4042
  }
4011
4043
  return destination;
4012
4044
  } finally {
4013
4045
  await rm(staging, { recursive: true, force: true });
4014
4046
  }
4015
4047
  }
4016
- async function verifyRuntime(root, manifest) {
4048
+ async function verifyRuntime(root) {
4017
4049
  const resolvedRoot = resolve3(root);
4018
- const expected = manifest ?? await parseRuntimeManifest(
4019
- await readFile2(join2(resolvedRoot, "dist", "runtime-manifest.json")),
4020
- resolvedRoot
4050
+ const manifestBytes = await readFile2(join2(resolvedRoot, "dist", "runtime-manifest.json"));
4051
+ const manifest = await parseRuntimeManifest(manifestBytes, resolvedRoot);
4052
+ if (manifest.closure === void 0) {
4053
+ throw new Error("Runtime release manifest is missing its materialized closure");
4054
+ }
4055
+ await verifyRuntimeFiles(resolvedRoot, manifest);
4056
+ await verifyDependencyIdentities(resolvedRoot, manifest.dependencies);
4057
+ const actualTree = await hashDirectoryTree(
4058
+ resolveInside(resolvedRoot, dependencyTreePath),
4059
+ dependencyTreePath
4060
+ );
4061
+ assertSameTree(manifest.closure.tree, actualTree, "materialized dependency closure");
4062
+ }
4063
+ async function verifyExpectedRuntime(root, expected, expectedBytes) {
4064
+ const manifestPath = join2(root, "dist", "runtime-manifest.json");
4065
+ const actualBytes = await readFile2(manifestPath);
4066
+ if (!actualBytes.equals(expectedBytes)) {
4067
+ throw new Error("Materialized runtime manifest does not match the expected closure");
4068
+ }
4069
+ const actual = await parseRuntimeManifest(actualBytes, root);
4070
+ if (actual.closure === void 0 || expected.closure === void 0) {
4071
+ throw new Error("Materialized runtime manifest is missing its closure");
4072
+ }
4073
+ await verifyRuntimeFiles(root, actual);
4074
+ await verifyDependencyIdentities(root, actual.dependencies);
4075
+ const actualTree = await hashDirectoryTree(
4076
+ resolveInside(root, dependencyTreePath),
4077
+ dependencyTreePath
4021
4078
  );
4022
- if (manifest !== void 0) await validateRuntimeManifest(manifest, resolvedRoot);
4023
- for (const file of expected.files) {
4024
- const path2 = resolveInside(resolvedRoot, file.path);
4079
+ assertSameTree(expected.closure.tree, actualTree, "materialized dependency closure");
4080
+ }
4081
+ async function verifyRuntimeFiles(root, manifest) {
4082
+ for (const file of manifest.files) {
4083
+ const path2 = resolveInside(root, file.path);
4025
4084
  const info = await lstat2(path2);
4026
4085
  if (info.isSymbolicLink()) {
4027
4086
  throw new Error(`Runtime artifact ${file.path} must not be a symbolic link`);
@@ -4034,11 +4093,27 @@ async function verifyRuntime(root, manifest) {
4034
4093
  throw new Error(`Runtime artifact ${file.path} failed verification`);
4035
4094
  }
4036
4095
  }
4037
- for (const expectedTree of expected.trees) {
4038
- const treeRoot = resolveInside(resolvedRoot, expectedTree.path);
4039
- const actualTree = await hashDirectoryTree(treeRoot, expectedTree.path);
4040
- if (actualTree.files !== expectedTree.files || actualTree.bytes !== expectedTree.bytes || actualTree.sha256 !== expectedTree.sha256) {
4041
- throw new Error(`Runtime dependency tree ${expectedTree.path} failed verification`);
4096
+ }
4097
+ async function verifyDependencyIdentities(root, dependencies) {
4098
+ const nodeModules = resolveInside(root, dependencyTreePath);
4099
+ const modulesInfo = await lstat2(nodeModules);
4100
+ if (!modulesInfo.isDirectory() || modulesInfo.isSymbolicLink()) {
4101
+ throw new Error("Runtime dependency closure must be a regular directory");
4102
+ }
4103
+ for (const dependency of dependencies) {
4104
+ const packageRoot = resolveInside(root, dependency.path);
4105
+ const packageInfo = await lstat2(packageRoot);
4106
+ if (!packageInfo.isDirectory() || packageInfo.isSymbolicLink()) {
4107
+ throw new Error(`Runtime dependency ${dependency.name} must be a regular directory`);
4108
+ }
4109
+ const packageJsonPath = join2(packageRoot, "package.json");
4110
+ const packageJsonInfo = await lstat2(packageJsonPath);
4111
+ if (!packageJsonInfo.isFile() || packageJsonInfo.isSymbolicLink()) {
4112
+ throw new Error(`Runtime dependency ${dependency.name} has an unsafe package.json`);
4113
+ }
4114
+ const identity = await readPackageMetadata(packageRoot);
4115
+ if (identity.name !== dependency.name || identity.version !== dependency.version) {
4116
+ throw new Error(`Runtime dependency ${dependency.name} has an unexpected identity`);
4042
4117
  }
4043
4118
  }
4044
4119
  }
@@ -4053,17 +4128,17 @@ async function parseRuntimeManifest(bytes, root) {
4053
4128
  return candidate;
4054
4129
  }
4055
4130
  async function validateRuntimeManifest(candidate, root) {
4056
- if (!isRecord2(candidate) || candidate.schemaVersion !== 2) {
4131
+ if (!isRecord2(candidate) || candidate.schemaVersion !== 3) {
4057
4132
  throw new Error("Runtime manifest has an unsupported schema version");
4058
4133
  }
4059
- if (!isRecord2(candidate.package) || typeof candidate.package.name !== "string" || typeof candidate.package.version !== "string") {
4134
+ if (!isRecord2(candidate.package) || typeof candidate.package.name !== "string" || typeof candidate.package.version !== "string" || typeof candidate.managedPi !== "string") {
4060
4135
  throw new Error("Runtime manifest has an invalid package identity");
4061
4136
  }
4062
- const packageIdentity = await readPackageIdentity(root);
4063
- if (candidate.package.name !== packageIdentity.name || candidate.package.version !== packageIdentity.version) {
4137
+ const packageMetadata = await readPackageMetadata(root);
4138
+ if (candidate.package.name !== packageMetadata.name || candidate.package.version !== packageMetadata.version) {
4064
4139
  throw new Error("Runtime manifest package identity does not match package.json");
4065
4140
  }
4066
- if (!Array.isArray(candidate.files) || !Array.isArray(candidate.trees)) {
4141
+ if (!Array.isArray(candidate.files) || !Array.isArray(candidate.dependencies)) {
4067
4142
  throw new Error("Runtime manifest has invalid artifact lists");
4068
4143
  }
4069
4144
  const filePaths = /* @__PURE__ */ new Set();
@@ -4071,8 +4146,9 @@ async function validateRuntimeManifest(candidate, root) {
4071
4146
  if (!isRecord2(file) || !isManifestPath(file.path) || !isValidSize(file.bytes) || !isSha256(file.sha256)) {
4072
4147
  throw new Error("Runtime manifest contains an invalid artifact");
4073
4148
  }
4074
- if (filePaths.has(file.path))
4149
+ if (filePaths.has(file.path)) {
4075
4150
  throw new Error(`Runtime manifest has duplicate path ${file.path}`);
4151
+ }
4076
4152
  filePaths.add(file.path);
4077
4153
  }
4078
4154
  for (const required of requiredRuntimeArtifacts) {
@@ -4080,37 +4156,63 @@ async function validateRuntimeManifest(candidate, root) {
4080
4156
  throw new Error(`Runtime manifest is missing required artifact ${required}`);
4081
4157
  }
4082
4158
  }
4083
- const treePaths = /* @__PURE__ */ new Set();
4084
- for (const tree of candidate.trees) {
4085
- if (!isRecord2(tree) || !isManifestPath(tree.path) || !isValidSize(tree.files) || !isValidSize(tree.bytes) || !isSha256(tree.sha256)) {
4086
- throw new Error("Runtime manifest contains an invalid dependency tree");
4159
+ const dependencyPaths = /* @__PURE__ */ new Set();
4160
+ const dependencyNames = /* @__PURE__ */ new Set();
4161
+ for (const dependency of candidate.dependencies) {
4162
+ if (!isRecord2(dependency) || !isManifestPath(dependency.path) || typeof dependency.name !== "string" || dependency.name.length === 0 || typeof dependency.version !== "string" || dependency.version.length === 0 || dependency.path !== `node_modules/${dependency.name}`) {
4163
+ throw new Error("Runtime manifest contains an invalid dependency declaration");
4087
4164
  }
4088
- if (treePaths.has(tree.path))
4089
- throw new Error(`Runtime manifest has duplicate path ${tree.path}`);
4090
- treePaths.add(tree.path);
4091
- }
4092
- const allPaths = [...filePaths, ...treePaths];
4093
- for (let index = 0; index < allPaths.length; index += 1) {
4094
- for (let other = index + 1; other < allPaths.length; other += 1) {
4095
- const left = allPaths[index];
4096
- const right = allPaths[other];
4097
- if (left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`)) {
4098
- throw new Error(`Runtime manifest has overlapping paths ${left} and ${right}`);
4165
+ if (dependencyPaths.has(dependency.path) || dependencyNames.has(dependency.name)) {
4166
+ throw new Error(`Runtime manifest has duplicate dependency ${dependency.name}`);
4167
+ }
4168
+ dependencyPaths.add(dependency.path);
4169
+ dependencyNames.add(dependency.name);
4170
+ }
4171
+ const expectedDependencies = packageMetadata.dependencies;
4172
+ if (dependencyNames.size !== Object.keys(expectedDependencies).length || [...dependencyNames].some(
4173
+ (name) => expectedDependencies[name] !== candidate.dependencies.find(
4174
+ (dependency) => dependency.name === name
4175
+ )?.version
4176
+ )) {
4177
+ throw new Error("Runtime manifest dependencies do not match package.json");
4178
+ }
4179
+ for (const filePath of filePaths) {
4180
+ for (const dependencyPath of dependencyPaths) {
4181
+ if (filePath === dependencyPath || filePath.startsWith(`${dependencyPath}/`) || dependencyPath.startsWith(`${filePath}/`)) {
4182
+ throw new Error(`Runtime manifest has overlapping paths ${filePath} and ${dependencyPath}`);
4099
4183
  }
4100
4184
  }
4101
4185
  }
4186
+ if (candidate.closure !== void 0) {
4187
+ if (!isRecord2(candidate.closure) || !isSha256(candidate.closure.sourceManifestSha256) || !isTreeIntegrity(candidate.closure.tree) || candidate.closure.tree.path !== dependencyTreePath) {
4188
+ throw new Error("Runtime manifest contains an invalid materialized closure");
4189
+ }
4190
+ }
4102
4191
  }
4103
- async function readPackageIdentity(root) {
4192
+ async function readPackageMetadata(root) {
4104
4193
  let candidate;
4105
4194
  try {
4106
4195
  candidate = JSON.parse(await readFile2(join2(root, "package.json"), "utf8"));
4107
4196
  } catch {
4108
4197
  throw new Error("Runtime package.json is not valid JSON");
4109
4198
  }
4110
- if (!isRecord2(candidate) || typeof candidate.name !== "string" || typeof candidate.version !== "string") {
4111
- throw new Error("Runtime package.json has an invalid identity");
4199
+ if (!isRecord2(candidate) || typeof candidate.name !== "string" || typeof candidate.version !== "string" || candidate.dependencies !== void 0 && !isRecord2(candidate.dependencies) || isRecord2(candidate.dependencies) && Object.values(candidate.dependencies).some((version) => typeof version !== "string")) {
4200
+ throw new Error("Runtime package.json has invalid package metadata");
4201
+ }
4202
+ return {
4203
+ name: candidate.name,
4204
+ version: candidate.version,
4205
+ dependencies: isRecord2(candidate.dependencies) ? candidate.dependencies : {}
4206
+ };
4207
+ }
4208
+ function serializeManifest(manifest) {
4209
+ return Buffer.from(`${JSON.stringify(manifest, null, 2)}
4210
+ `);
4211
+ }
4212
+ function assertSameTree(expected, actual, label) {
4213
+ if (expected.path !== actual.path || expected.files !== actual.files || expected.bytes !== actual.bytes || expected.sha256 !== actual.sha256) {
4214
+ throw new Error(`Runtime ${label} changed during materialization`);
4112
4215
  }
4113
- return { name: candidate.name, version: candidate.version };
4114
4216
  }
4115
4217
  function resolveInside(root, path2) {
4116
4218
  if (!isManifestPath(path2)) throw new Error(`Runtime manifest contains an unsafe path ${path2}`);
@@ -4132,6 +4234,9 @@ function isValidSize(value) {
4132
4234
  function isSha256(value) {
4133
4235
  return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
4134
4236
  }
4237
+ function isTreeIntegrity(value) {
4238
+ return isRecord2(value) && isManifestPath(value.path) && isValidSize(value.files) && isValidSize(value.bytes) && isSha256(value.sha256);
4239
+ }
4135
4240
  function isDestinationRace(error) {
4136
4241
  const code = error.code;
4137
4242
  return code === "EEXIST" || code === "ENOTEMPTY";