@elpapi42/pi-fleet 0.1.0-beta.1 → 0.1.0-beta.3

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
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Reliability
6
+
7
+ - Added deterministic fault-injection coverage for receive timeout/cancellation, delivery recovery, runtime crashes, Pi RPC failures, SQLite worker/storage failures, protocol framing, command races, raw session boundaries, and public-error redaction.
8
+ - Serialized create, send, and destroy transitions per agent so cross-command races cannot orphan or resurrect an agent generation.
9
+ - Made SQLite-worker failures terminal for current and future calls and made send recovery safe when an operation exists but no send record was committed.
10
+ - Redacted Pi stderr and unexpected internal exceptions from public errors while retaining stable typed error codes.
11
+ - Contained asynchronous coordinator event/store failures by stopping the affected Pi process and rejecting waiters instead of producing unhandled promise rejections.
12
+ - Added a nightly reliability workflow and an isolated soak suite covering 500 concurrent ordered sends and 100 name lifecycle cycles.
13
+ - Reject invalid UTF-8 in stdin and Pi RPC stdout instead of silently replacing bytes at either protocol boundary.
14
+
15
+ ## 0.1.0-beta.3 — 2026-07-20
16
+
17
+ Reliability fix for fresh global npm installations. No intentional CLI or agent-lifecycle contract change.
18
+
19
+ ### Fixed
20
+
21
+ - 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.
22
+ - Made immutable runtime release paths closure-specific, so different legitimate npm dependency layouts do not collide.
23
+ - 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.
24
+
25
+ ### Installation scope
26
+
27
+ - 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.
28
+ - 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.
29
+
30
+ ## 0.1.0-beta.2 — 2026-07-20
31
+
32
+ Reliability-focused beta maintenance release. No intentional product-contract changes.
33
+
34
+ ### Reliability
35
+
36
+ - Added deterministic fault coverage and reproducible randomized lifecycle-race soak testing.
37
+ - Fixed timeout/cancellation classification, ambiguous-delivery recovery, no-replay behavior, and coordinator/store failure containment.
38
+ - Validated inbound protocol majors, strict and concurrent runtime materialization, and released beta.0/beta.1 CLI-runtime compatibility.
39
+ - Added unclean SQLite `quick_check` validation, redacted error handling, fatal UTF-8 checks, process cleanup coverage, and nightly resource-stability checks.
40
+
41
+ ### Validation limits
42
+
43
+ - Real disk exhaustion, host logout/reboot, macOS launchd/containment, and multi-hour platform resource soak remain unvalidated.
44
+
3
45
  ## 0.1.0-beta.1 — 2026-07-20
4
46
 
5
47
  Release-pipeline validation beta. No product-contract changes from `0.1.0-beta.0`.
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.0` is release-validated on Linux x64 with Pi `0.80.10`. macOS, actual logout/reboot recovery, automatic upgrades, and public service-management UX are not yet release-validated.
11
+ > **Beta:** `0.1.0-beta.3` 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,9 +173,10 @@ Do not include API keys, message contents, session contents, or private paths un
173
173
 
174
174
  ## Beta limitations
175
175
 
176
- - Release validation currently 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
+ - Real disk-exhaustion behavior and multi-hour resource growth remain unvalidated; bounded SQLite and in-process resource tests are covered.
179
180
  - Native service install/repair/uninstall exists internally but has no supported public UX yet.
180
181
  - Runtime upgrades are not automatic; active runtimes are not silently replaced.
181
182
  - Session tails cannot promise exactly-once behavior under arbitrary external mutation.
@@ -189,12 +190,14 @@ npm run typecheck
189
190
  npm run lint
190
191
  npm run format:check
191
192
  npm test
193
+ npm run test:faults
192
194
  npm run build
193
195
  npm run test:package
194
196
  npm run test:platform
197
+ npm run test:soak
195
198
  ```
196
199
 
197
- See [`IMPLEMENTATION_PLAN.md`](./IMPLEMENTATION_PLAN.md) for architecture, completed evidence, and remaining release gates.
200
+ See [`IMPLEMENTATION_PLAN.md`](./IMPLEMENTATION_PLAN.md) for architecture and release gates, and [`TESTING_PLAN.md`](./TESTING_PLAN.md) for failure injection, crash recovery, concurrency, privacy, and soak testing. Deterministic fault tests run in the full suite; heavier reliability checks run nightly.
198
201
 
199
202
  ## License
200
203
 
@@ -156,7 +156,7 @@
156
156
  "format": "esm"
157
157
  },
158
158
  "src/cli/input.ts": {
159
- "bytes": 824,
159
+ "bytes": 977,
160
160
  "imports": [],
161
161
  "format": "esm"
162
162
  },
@@ -392,7 +392,7 @@
392
392
  "format": "esm"
393
393
  },
394
394
  "src/client/socket-fleet-client.ts": {
395
- "bytes": 8696,
395
+ "bytes": 9121,
396
396
  "imports": [
397
397
  {
398
398
  "path": "node:crypto",
@@ -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": 4620,
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": 251175
596
+ "bytes": 270001
597
597
  },
598
598
  "dist/cli.mjs": {
599
599
  "imports": [
@@ -780,7 +780,7 @@
780
780
  "bytesInOutput": 140
781
781
  },
782
782
  "src/cli/input.ts": {
783
- "bytesInOutput": 696
783
+ "bytesInOutput": 841
784
784
  },
785
785
  "src/cli/output.ts": {
786
786
  "bytesInOutput": 1016
@@ -816,7 +816,7 @@
816
816
  "bytesInOutput": 2440
817
817
  },
818
818
  "src/client/socket-fleet-client.ts": {
819
- "bytesInOutput": 6835
819
+ "bytesInOutput": 7258
820
820
  },
821
821
  "src/protocol/version.ts": {
822
822
  "bytesInOutput": 70
@@ -828,16 +828,16 @@
828
828
  "bytesInOutput": 4658
829
829
  },
830
830
  "src/platform/install/runtime-release.ts": {
831
- "bytesInOutput": 4008
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": 139855
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.1";
3388
+ var PRODUCT_VERSION = "0.1.0-beta.3";
3389
3389
 
3390
3390
  // src/cli/commands/create.ts
3391
3391
  import { resolve } from "node:path";
@@ -3408,7 +3408,13 @@ async function resolveMessageInput(value, stdin, maxBytes = DEFAULT_MAX_INPUT_BY
3408
3408
  if (bytes > maxBytes) throw new Error(`stdin exceeds the ${maxBytes}-byte limit`);
3409
3409
  chunks.push(buffer);
3410
3410
  }
3411
- return requireContent(Buffer.concat(chunks).toString("utf8"));
3411
+ let decoded;
3412
+ try {
3413
+ decoded = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
3414
+ } catch {
3415
+ throw new Error("stdin must be valid UTF-8");
3416
+ }
3417
+ return requireContent(decoded);
3412
3418
  }
3413
3419
  function requireContent(value) {
3414
3420
  if (value.trim().length === 0) throw new Error("message must not be empty or whitespace-only");
@@ -3693,6 +3699,13 @@ var SocketFleetClient = class {
3693
3699
  try {
3694
3700
  for await (const frame of frames) {
3695
3701
  if (!isRecord(frame) || frame.requestId !== requestId) continue;
3702
+ if (frame.v !== PROTOCOL_VERSION) {
3703
+ yield err({
3704
+ code: "protocol_error",
3705
+ message: "Runtime protocol version is incompatible with this client."
3706
+ });
3707
+ return;
3708
+ }
3696
3709
  if (frame.stream === "ready") continue;
3697
3710
  if (frame.stream === "end") {
3698
3711
  endedExplicitly = true;
@@ -3746,6 +3759,12 @@ var SocketFleetClient = class {
3746
3759
  if (!isRecord(frame) || frame.requestId !== requestId || typeof frame.ok !== "boolean") {
3747
3760
  return err({ code: "protocol_error", message: "Invalid runtime response." });
3748
3761
  }
3762
+ if (frame.v !== PROTOCOL_VERSION) {
3763
+ return err({
3764
+ code: "protocol_error",
3765
+ message: "Runtime protocol version is incompatible with this client."
3766
+ });
3767
+ }
3749
3768
  if (frame.ok) return ok(frame.result);
3750
3769
  if (isErrorRecord(frame.error)) return err(frame.error);
3751
3770
  return err({ code: "protocol_error", message: "Runtime returned an invalid error." });
@@ -3888,12 +3907,12 @@ import { promisify } from "node:util";
3888
3907
 
3889
3908
  // src/platform/install/runtime-release.ts
3890
3909
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
3891
- import { chmod, cp, lstat as lstat2, mkdir, readFile as readFile2, rename, rm, stat as stat2 } from "node:fs/promises";
3892
- import { join as join2, resolve as resolve3, sep as sep2 } from "node:path";
3910
+ import { chmod, cp, lstat as lstat2, mkdir, readFile as readFile2, rename, rm, writeFile } from "node:fs/promises";
3911
+ import { join as join2, posix, resolve as resolve3, sep as sep2 } from "node:path";
3893
3912
 
3894
3913
  // src/platform/install/tree-integrity.ts
3895
3914
  import { createHash } from "node:crypto";
3896
- import { lstat, readFile, readdir, stat } from "node:fs/promises";
3915
+ import { lstat, readFile, readdir, realpath, stat } from "node:fs/promises";
3897
3916
  import { join, relative, resolve as resolve2, sep } from "node:path";
3898
3917
  async function hashDirectoryTree(root, manifestPath) {
3899
3918
  const resolvedRoot = resolve2(root);
@@ -3933,6 +3952,14 @@ async function collectFiles(root, directory) {
3933
3952
  if (!info.isFile()) {
3934
3953
  throw new Error(`Runtime dependency tree contains an unsupported entry: ${absolutePath}`);
3935
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
+ }
3936
3963
  const relativePath = relative(root, absolutePath).split(sep).join("/");
3937
3964
  if (relativePath.length === 0 || relativePath.startsWith("../")) {
3938
3965
  throw new Error(`Runtime dependency path escapes its root: ${absolutePath}`);
@@ -3943,18 +3970,41 @@ async function collectFiles(root, directory) {
3943
3970
  }
3944
3971
 
3945
3972
  // src/platform/install/runtime-release.ts
3973
+ var requiredRuntimeArtifacts = /* @__PURE__ */ new Set([
3974
+ "package.json",
3975
+ "bin/pifleet.mjs",
3976
+ "bin/pifleet-runtime.mjs",
3977
+ "dist/cli.mjs",
3978
+ "dist/runtime.mjs",
3979
+ "dist/sqlite-worker.mjs"
3980
+ ]);
3981
+ var dependencyTreePath = "node_modules";
3946
3982
  async function materializeRuntime(options) {
3947
3983
  const sourceRoot = resolve3(options.sourceRoot);
3984
+ const manifestBytes = await readFile2(join2(sourceRoot, "dist", "runtime-manifest.json"));
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);
3948
4001
  const applicationRoot = resolve3(options.applicationRoot);
3949
4002
  await ensurePrivateDirectory(applicationRoot);
3950
- const manifestBytes = await readFile2(join2(sourceRoot, "dist", "runtime-manifest.json"));
3951
- const manifest = JSON.parse(manifestBytes.toString("utf8"));
3952
- const manifestHash = createHash2("sha256").update(manifestBytes).digest("hex").slice(0, 16);
3953
4003
  const releasesRoot = join2(applicationRoot, "releases");
3954
4004
  await ensurePrivateDirectory(releasesRoot);
3955
- const destination = join2(releasesRoot, `${manifest.package.version}-${manifestHash}`);
4005
+ const destination = join2(releasesRoot, `${sourceManifest.package.version}-${closureHash}`);
3956
4006
  if (await pathExists(destination)) {
3957
- await verifyRuntime(destination, manifest);
4007
+ await verifyExpectedRuntime(destination, materializedManifest, materializedManifestBytes);
3958
4008
  return destination;
3959
4009
  }
3960
4010
  const staging = join2(releasesRoot, `.staging-${randomUUID2()}`);
@@ -3964,31 +4014,77 @@ async function materializeRuntime(options) {
3964
4014
  recursive: true,
3965
4015
  dereference: true
3966
4016
  });
3967
- 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
+ });
3968
4021
  await cp(join2(sourceRoot, "package.json"), join2(staging, "package.json"));
3969
- for (const tree of manifest.trees) {
3970
- const source = resolveInside(sourceRoot, tree.path);
3971
- const destination2 = resolveInside(staging, tree.path);
3972
- await mkdir(resolve3(destination2, ".."), { recursive: true, mode: 448 });
3973
- await cp(source, destination2, { recursive: true, dereference: true });
3974
- }
3975
- 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);
3976
4036
  await chmod(staging, 448);
3977
- await rename(staging, destination);
4037
+ try {
4038
+ await rename(staging, destination);
4039
+ } catch (error) {
4040
+ if (!isDestinationRace(error) || !await pathExists(destination)) throw error;
4041
+ await verifyExpectedRuntime(destination, materializedManifest, materializedManifestBytes);
4042
+ }
3978
4043
  return destination;
3979
- } catch (error) {
4044
+ } finally {
3980
4045
  await rm(staging, { recursive: true, force: true });
3981
- throw error;
3982
4046
  }
3983
4047
  }
3984
- async function verifyRuntime(root, manifest) {
3985
- const expected = manifest ?? JSON.parse(
3986
- await readFile2(join2(root, "dist", "runtime-manifest.json"), "utf8")
3987
- );
4048
+ async function verifyRuntime(root) {
3988
4049
  const resolvedRoot = resolve3(root);
3989
- for (const file of expected.files) {
3990
- const path2 = resolveInside(resolvedRoot, file.path);
3991
- const info = await stat2(path2);
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
4078
+ );
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);
4084
+ const info = await lstat2(path2);
4085
+ if (info.isSymbolicLink()) {
4086
+ throw new Error(`Runtime artifact ${file.path} must not be a symbolic link`);
4087
+ }
3992
4088
  if (!info.isFile() || info.size !== file.bytes) {
3993
4089
  throw new Error(`Runtime artifact ${file.path} has changed`);
3994
4090
  }
@@ -3997,21 +4093,154 @@ async function verifyRuntime(root, manifest) {
3997
4093
  throw new Error(`Runtime artifact ${file.path} failed verification`);
3998
4094
  }
3999
4095
  }
4000
- for (const expectedTree of expected.trees) {
4001
- const treeRoot = resolveInside(resolvedRoot, expectedTree.path);
4002
- const actualTree = await hashDirectoryTree(treeRoot, expectedTree.path);
4003
- if (actualTree.files !== expectedTree.files || actualTree.bytes !== expectedTree.bytes || actualTree.sha256 !== expectedTree.sha256) {
4004
- 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`);
4117
+ }
4118
+ }
4119
+ }
4120
+ async function parseRuntimeManifest(bytes, root) {
4121
+ let candidate;
4122
+ try {
4123
+ candidate = JSON.parse(bytes.toString("utf8"));
4124
+ } catch {
4125
+ throw new Error("Runtime manifest is not valid JSON");
4126
+ }
4127
+ await validateRuntimeManifest(candidate, root);
4128
+ return candidate;
4129
+ }
4130
+ async function validateRuntimeManifest(candidate, root) {
4131
+ if (!isRecord2(candidate) || candidate.schemaVersion !== 3) {
4132
+ throw new Error("Runtime manifest has an unsupported schema version");
4133
+ }
4134
+ if (!isRecord2(candidate.package) || typeof candidate.package.name !== "string" || typeof candidate.package.version !== "string" || typeof candidate.managedPi !== "string") {
4135
+ throw new Error("Runtime manifest has an invalid package identity");
4136
+ }
4137
+ const packageMetadata = await readPackageMetadata(root);
4138
+ if (candidate.package.name !== packageMetadata.name || candidate.package.version !== packageMetadata.version) {
4139
+ throw new Error("Runtime manifest package identity does not match package.json");
4140
+ }
4141
+ if (!Array.isArray(candidate.files) || !Array.isArray(candidate.dependencies)) {
4142
+ throw new Error("Runtime manifest has invalid artifact lists");
4143
+ }
4144
+ const filePaths = /* @__PURE__ */ new Set();
4145
+ for (const file of candidate.files) {
4146
+ if (!isRecord2(file) || !isManifestPath(file.path) || !isValidSize(file.bytes) || !isSha256(file.sha256)) {
4147
+ throw new Error("Runtime manifest contains an invalid artifact");
4148
+ }
4149
+ if (filePaths.has(file.path)) {
4150
+ throw new Error(`Runtime manifest has duplicate path ${file.path}`);
4151
+ }
4152
+ filePaths.add(file.path);
4153
+ }
4154
+ for (const required of requiredRuntimeArtifacts) {
4155
+ if (!filePaths.has(required)) {
4156
+ throw new Error(`Runtime manifest is missing required artifact ${required}`);
4157
+ }
4158
+ }
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");
4164
+ }
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}`);
4183
+ }
4184
+ }
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");
4005
4189
  }
4006
4190
  }
4007
4191
  }
4192
+ async function readPackageMetadata(root) {
4193
+ let candidate;
4194
+ try {
4195
+ candidate = JSON.parse(await readFile2(join2(root, "package.json"), "utf8"));
4196
+ } catch {
4197
+ throw new Error("Runtime package.json is not valid JSON");
4198
+ }
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`);
4215
+ }
4216
+ }
4008
4217
  function resolveInside(root, path2) {
4218
+ if (!isManifestPath(path2)) throw new Error(`Runtime manifest contains an unsafe path ${path2}`);
4009
4219
  const resolved = resolve3(root, path2);
4010
4220
  if (!resolved.startsWith(`${root}${sep2}`)) {
4011
4221
  throw new Error(`Runtime manifest contains an unsafe path ${path2}`);
4012
4222
  }
4013
4223
  return resolved;
4014
4224
  }
4225
+ function isRecord2(value) {
4226
+ return typeof value === "object" && value !== null;
4227
+ }
4228
+ function isManifestPath(value) {
4229
+ return typeof value === "string" && value.length > 0 && !value.includes("\\") && !value.startsWith("/") && posix.normalize(value) === value && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
4230
+ }
4231
+ function isValidSize(value) {
4232
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
4233
+ }
4234
+ function isSha256(value) {
4235
+ return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
4236
+ }
4237
+ function isTreeIntegrity(value) {
4238
+ return isRecord2(value) && isManifestPath(value.path) && isValidSize(value.files) && isValidSize(value.bytes) && isSha256(value.sha256);
4239
+ }
4240
+ function isDestinationRace(error) {
4241
+ const code = error.code;
4242
+ return code === "EEXIST" || code === "ENOTEMPTY";
4243
+ }
4015
4244
  async function ensurePrivateDirectory(path2) {
4016
4245
  await mkdir(path2, { recursive: true, mode: 448 });
4017
4246
  const info = await lstat2(path2);