@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 +42 -0
- package/README.md +6 -3
- package/dist/cli-meta.json +10 -10
- package/dist/cli.mjs +262 -33
- package/dist/cli.mjs.map +3 -3
- package/dist/installer-meta.json +6 -6
- package/dist/installer.mjs +241 -31
- package/dist/installer.mjs.map +3 -3
- package/dist/runtime-manifest.json +34 -37
- package/dist/runtime-meta.json +12 -12
- package/dist/runtime.mjs +149 -46
- package/dist/runtime.mjs.map +2 -2
- package/dist/sqlite-worker-meta.json +4 -4
- package/dist/sqlite-worker.mjs +45 -38
- package/dist/sqlite-worker.mjs.map +2 -2
- package/package.json +4 -1
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.
|
|
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
|
-
-
|
|
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,
|
|
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
|
|
package/dist/cli-meta.json
CHANGED
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
"format": "esm"
|
|
157
157
|
},
|
|
158
158
|
"src/cli/input.ts": {
|
|
159
|
-
"bytes":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
831
|
+
"bytesInOutput": 13651
|
|
832
832
|
},
|
|
833
833
|
"src/platform/install/tree-integrity.ts": {
|
|
834
|
-
"bytesInOutput":
|
|
834
|
+
"bytesInOutput": 2182
|
|
835
835
|
},
|
|
836
836
|
"src/platform/shared/paths.ts": {
|
|
837
837
|
"bytesInOutput": 1191
|
|
838
838
|
}
|
|
839
839
|
},
|
|
840
|
-
"bytes":
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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, `${
|
|
4005
|
+
const destination = join2(releasesRoot, `${sourceManifest.package.version}-${closureHash}`);
|
|
3956
4006
|
if (await pathExists(destination)) {
|
|
3957
|
-
await
|
|
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"), {
|
|
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
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
4044
|
+
} finally {
|
|
3980
4045
|
await rm(staging, { recursive: true, force: true });
|
|
3981
|
-
throw error;
|
|
3982
4046
|
}
|
|
3983
4047
|
}
|
|
3984
|
-
async function verifyRuntime(root
|
|
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
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
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
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
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);
|