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

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,41 @@
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.2 — 2026-07-20
16
+
17
+ Reliability-focused beta maintenance release. No intentional product-contract changes.
18
+
19
+ ### Reliability
20
+
21
+ - Added deterministic fault coverage and reproducible randomized lifecycle-race soak testing.
22
+ - Fixed timeout/cancellation classification, ambiguous-delivery recovery, no-replay behavior, and coordinator/store failure containment.
23
+ - Validated inbound protocol majors, strict and concurrent runtime materialization, and released beta.0/beta.1 CLI-runtime compatibility.
24
+ - Added unclean SQLite `quick_check` validation, redacted error handling, fatal UTF-8 checks, process cleanup coverage, and nightly resource-stability checks.
25
+
26
+ ### Validation limits
27
+
28
+ - Real disk exhaustion, host logout/reboot, macOS launchd/containment, and multi-hour platform resource soak remain unvalidated.
29
+
30
+ ## 0.1.0-beta.1 — 2026-07-20
31
+
32
+ Release-pipeline validation beta. No product-contract changes from `0.1.0-beta.0`.
33
+
34
+ ### Changed
35
+
36
+ - Publishes through the tag-driven GitHub Actions workflow using npm trusted publishing and provenance.
37
+ - Retains the same Linux x64 validation scope, beta limitations, and user-owned Pi session guarantees as `0.1.0-beta.0`.
38
+
3
39
  ## 0.1.0-beta.0 — 2026-07-20
4
40
 
5
41
  First public beta of Pi Fleet.
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.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.
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 package validation covers Linux x64 only.
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",
@@ -444,7 +444,7 @@
444
444
  "format": "esm"
445
445
  },
446
446
  "src/platform/install/runtime-release.ts": {
447
- "bytes": 4620,
447
+ "bytes": 9583,
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": 260315
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,7 +828,7 @@
828
828
  "bytesInOutput": 4658
829
829
  },
830
830
  "src/platform/install/runtime-release.ts": {
831
- "bytesInOutput": 4008
831
+ "bytesInOutput": 8505
832
832
  },
833
833
  "src/platform/install/tree-integrity.ts": {
834
834
  "bytesInOutput": 1879
@@ -837,7 +837,7 @@
837
837
  "bytesInOutput": 1191
838
838
  }
839
839
  },
840
- "bytes": 139855
840
+ "bytes": 144920
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.0";
3388
+ var PRODUCT_VERSION = "0.1.0-beta.2";
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,8 +3907,8 @@ 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 } 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";
@@ -3943,12 +3962,21 @@ async function collectFiles(root, directory) {
3943
3962
  }
3944
3963
 
3945
3964
  // src/platform/install/runtime-release.ts
3965
+ var requiredRuntimeArtifacts = /* @__PURE__ */ new Set([
3966
+ "package.json",
3967
+ "bin/pifleet.mjs",
3968
+ "bin/pifleet-runtime.mjs",
3969
+ "dist/cli.mjs",
3970
+ "dist/runtime.mjs",
3971
+ "dist/sqlite-worker.mjs"
3972
+ ]);
3946
3973
  async function materializeRuntime(options) {
3947
3974
  const sourceRoot = resolve3(options.sourceRoot);
3975
+ const manifestBytes = await readFile2(join2(sourceRoot, "dist", "runtime-manifest.json"));
3976
+ const manifest = await parseRuntimeManifest(manifestBytes, sourceRoot);
3977
+ await verifyRuntime(sourceRoot, manifest);
3948
3978
  const applicationRoot = resolve3(options.applicationRoot);
3949
3979
  await ensurePrivateDirectory(applicationRoot);
3950
- const manifestBytes = await readFile2(join2(sourceRoot, "dist", "runtime-manifest.json"));
3951
- const manifest = JSON.parse(manifestBytes.toString("utf8"));
3952
3980
  const manifestHash = createHash2("sha256").update(manifestBytes).digest("hex").slice(0, 16);
3953
3981
  const releasesRoot = join2(applicationRoot, "releases");
3954
3982
  await ensurePrivateDirectory(releasesRoot);
@@ -3974,21 +4002,30 @@ async function materializeRuntime(options) {
3974
4002
  }
3975
4003
  await verifyRuntime(staging, manifest);
3976
4004
  await chmod(staging, 448);
3977
- await rename(staging, destination);
4005
+ try {
4006
+ await rename(staging, destination);
4007
+ } catch (error) {
4008
+ if (!isDestinationRace(error) || !await pathExists(destination)) throw error;
4009
+ await verifyRuntime(destination, manifest);
4010
+ }
3978
4011
  return destination;
3979
- } catch (error) {
4012
+ } finally {
3980
4013
  await rm(staging, { recursive: true, force: true });
3981
- throw error;
3982
4014
  }
3983
4015
  }
3984
4016
  async function verifyRuntime(root, manifest) {
3985
- const expected = manifest ?? JSON.parse(
3986
- await readFile2(join2(root, "dist", "runtime-manifest.json"), "utf8")
3987
- );
3988
4017
  const resolvedRoot = resolve3(root);
4018
+ const expected = manifest ?? await parseRuntimeManifest(
4019
+ await readFile2(join2(resolvedRoot, "dist", "runtime-manifest.json")),
4020
+ resolvedRoot
4021
+ );
4022
+ if (manifest !== void 0) await validateRuntimeManifest(manifest, resolvedRoot);
3989
4023
  for (const file of expected.files) {
3990
4024
  const path2 = resolveInside(resolvedRoot, file.path);
3991
- const info = await stat2(path2);
4025
+ const info = await lstat2(path2);
4026
+ if (info.isSymbolicLink()) {
4027
+ throw new Error(`Runtime artifact ${file.path} must not be a symbolic link`);
4028
+ }
3992
4029
  if (!info.isFile() || info.size !== file.bytes) {
3993
4030
  throw new Error(`Runtime artifact ${file.path} has changed`);
3994
4031
  }
@@ -4005,13 +4042,100 @@ async function verifyRuntime(root, manifest) {
4005
4042
  }
4006
4043
  }
4007
4044
  }
4045
+ async function parseRuntimeManifest(bytes, root) {
4046
+ let candidate;
4047
+ try {
4048
+ candidate = JSON.parse(bytes.toString("utf8"));
4049
+ } catch {
4050
+ throw new Error("Runtime manifest is not valid JSON");
4051
+ }
4052
+ await validateRuntimeManifest(candidate, root);
4053
+ return candidate;
4054
+ }
4055
+ async function validateRuntimeManifest(candidate, root) {
4056
+ if (!isRecord2(candidate) || candidate.schemaVersion !== 2) {
4057
+ throw new Error("Runtime manifest has an unsupported schema version");
4058
+ }
4059
+ if (!isRecord2(candidate.package) || typeof candidate.package.name !== "string" || typeof candidate.package.version !== "string") {
4060
+ throw new Error("Runtime manifest has an invalid package identity");
4061
+ }
4062
+ const packageIdentity = await readPackageIdentity(root);
4063
+ if (candidate.package.name !== packageIdentity.name || candidate.package.version !== packageIdentity.version) {
4064
+ throw new Error("Runtime manifest package identity does not match package.json");
4065
+ }
4066
+ if (!Array.isArray(candidate.files) || !Array.isArray(candidate.trees)) {
4067
+ throw new Error("Runtime manifest has invalid artifact lists");
4068
+ }
4069
+ const filePaths = /* @__PURE__ */ new Set();
4070
+ for (const file of candidate.files) {
4071
+ if (!isRecord2(file) || !isManifestPath(file.path) || !isValidSize(file.bytes) || !isSha256(file.sha256)) {
4072
+ throw new Error("Runtime manifest contains an invalid artifact");
4073
+ }
4074
+ if (filePaths.has(file.path))
4075
+ throw new Error(`Runtime manifest has duplicate path ${file.path}`);
4076
+ filePaths.add(file.path);
4077
+ }
4078
+ for (const required of requiredRuntimeArtifacts) {
4079
+ if (!filePaths.has(required)) {
4080
+ throw new Error(`Runtime manifest is missing required artifact ${required}`);
4081
+ }
4082
+ }
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");
4087
+ }
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}`);
4099
+ }
4100
+ }
4101
+ }
4102
+ }
4103
+ async function readPackageIdentity(root) {
4104
+ let candidate;
4105
+ try {
4106
+ candidate = JSON.parse(await readFile2(join2(root, "package.json"), "utf8"));
4107
+ } catch {
4108
+ throw new Error("Runtime package.json is not valid JSON");
4109
+ }
4110
+ if (!isRecord2(candidate) || typeof candidate.name !== "string" || typeof candidate.version !== "string") {
4111
+ throw new Error("Runtime package.json has an invalid identity");
4112
+ }
4113
+ return { name: candidate.name, version: candidate.version };
4114
+ }
4008
4115
  function resolveInside(root, path2) {
4116
+ if (!isManifestPath(path2)) throw new Error(`Runtime manifest contains an unsafe path ${path2}`);
4009
4117
  const resolved = resolve3(root, path2);
4010
4118
  if (!resolved.startsWith(`${root}${sep2}`)) {
4011
4119
  throw new Error(`Runtime manifest contains an unsafe path ${path2}`);
4012
4120
  }
4013
4121
  return resolved;
4014
4122
  }
4123
+ function isRecord2(value) {
4124
+ return typeof value === "object" && value !== null;
4125
+ }
4126
+ function isManifestPath(value) {
4127
+ return typeof value === "string" && value.length > 0 && !value.includes("\\") && !value.startsWith("/") && posix.normalize(value) === value && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
4128
+ }
4129
+ function isValidSize(value) {
4130
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
4131
+ }
4132
+ function isSha256(value) {
4133
+ return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
4134
+ }
4135
+ function isDestinationRace(error) {
4136
+ const code = error.code;
4137
+ return code === "EEXIST" || code === "ENOTEMPTY";
4138
+ }
4015
4139
  async function ensurePrivateDirectory(path2) {
4016
4140
  await mkdir(path2, { recursive: true, mode: 448 });
4017
4141
  const info = await lstat2(path2);