bootproof 0.1.0 → 0.3.0

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/README.md CHANGED
@@ -1,12 +1,34 @@
1
1
  # BootProof
2
2
 
3
+ [![CI](https://github.com/bootproof/bootproof/actions/workflows/ci.yml/badge.svg)](https://github.com/bootproof/bootproof/actions/workflows/ci.yml)
4
+
3
5
  > **The honest Run Button for repos — with proof, not vibes.**
4
6
 
5
7
  **Human diagnosis. Machine proof. One engine.**
6
8
 
7
- <p align="center">
8
- <img src="https://raw.githubusercontent.com/rossbuckley1990-hash/bootproof/main/assets/bootproof_viral_demo.gif" alt="BootProof demo" width="900">
9
- </p>
9
+ ```text
10
+ bootproof up https://github.com/dubinc/dub
11
+
12
+ Remote source: https://github.com/dubinc/dub.git
13
+ Clone retained at: .bootproof/remotes/github.com/dubinc/dub-*/repo
14
+
15
+ Inference (evidence-based)
16
+ application: yes
17
+ package manager: pnpm.15.9
18
+ selected command: pnpm dev
19
+
20
+ ✗ NOT VERIFIED — remote_code_execution_blocked
21
+ Why BootProof refused: remote repositories are untrusted code and require explicit consent.
22
+
23
+ bootproof up . --provider local --unsafe-local --install
24
+
25
+ ✓ install: dependencies installed
26
+ ✓ start-app: app process started and was supervised
27
+ ✓ health: observed HTTP 200 at http://localhost:3333
28
+
29
+ ✓ BOOTED — HTTP 200 at http://localhost:3333
30
+ Evidence: .bootproof/attestation.json
31
+ ```
10
32
 
11
33
  BootProof inspects a local repository, builds an evidence-based run plan, executes only what it can justify, observes HTTP health, and writes a signed attestation for success or failure.
12
34
 
@@ -36,6 +58,42 @@ They get a signed verdict and a deterministic exit code.
36
58
 
37
59
  The same engine powers both.
38
60
 
61
+ ## Verified Repairs
62
+
63
+ For the small deterministic repair registry:
64
+
65
+ ```bash
66
+ bootproof fix .
67
+ ```
68
+
69
+ BootProof reuses a signature-valid failure only at the exact clean Git commit; otherwise it reproduces the failure in a temporary copy. It applies one known remediation there and reruns full verification. It emits a signed `bootproof/repair-receipt/v1` only when the before run failed and the after run observed successful HTTP health.
70
+
71
+ The original working tree is not edited. File changes are written as a reviewable patch under `.bootproof/`; the human decides whether to apply it.
72
+
73
+ Machine mode is:
74
+
75
+ ```bash
76
+ bootproof fix . --json
77
+ ```
78
+
79
+ It emits one `bootproof/repair-result/v1` object and exits `0` only when a verified receipt exists.
80
+
81
+ Public GitHub, GitLab, Bitbucket, and Codeberg repositories use the same retained managed workspace and execution gate as `up`:
82
+
83
+ ```bash
84
+ bootproof fix https://github.com/user/repo --provider local --unsafe-local
85
+ ```
86
+
87
+ `fix` never applies its patch. To explicitly apply a signature-valid file repair to a local working tree:
88
+
89
+ ```bash
90
+ bootproof apply-repair .
91
+ ```
92
+
93
+ Application checks the receipt signature, allowed file scope, signed content hashes, and exact current preimages before writing. Environment-only and plan-only receipts have no file change to apply.
94
+
95
+ See [docs/REPAIR_RECEIPT.md](docs/REPAIR_RECEIPT.md).
96
+
39
97
  ## What It Tells Humans
40
98
 
41
99
  A failed run is still useful:
@@ -48,7 +106,7 @@ Safe next step: Run corepack enable && corepack prepare pnpm@10.24.0 --activate,
48
106
  Evidence: .bootproof/attestation.json
49
107
  ```
50
108
 
51
- BootProof distinguishes diagnosis from proof. Detecting Python, Flask, React, Celery, Go, or a monorepo does not mean BootProof claims full orchestration support for that stack.
109
+ BootProof distinguishes diagnosis from proof. It can execute a narrow explicit Go main package, Rails `bin/rails` entrypoint, or Make run target, but detection alone never implies general support for every Go, Ruby, Make, Python, or monorepo architecture.
52
110
 
53
111
  ## What It Gives Machines
54
112
 
@@ -100,13 +158,13 @@ npx bootproof explain .bootproof/attestation.json
100
158
  npx bootproof verify .bootproof/attestation.json
101
159
  ```
102
160
 
103
- Run against a public GitHub repository:
161
+ Run against a public HTTPS Git repository on GitHub, GitLab, Bitbucket, or Codeberg:
104
162
 
105
163
  ```bash
106
164
  npx bootproof up https://github.com/user/repo
107
165
  ```
108
166
 
109
- BootProof clones credential-free HTTPS GitHub URLs into `.bootproof/remotes/` and retains the clone so its evidence and any generated files continue to exist. It inspects the clone but refuses to execute remote code until host execution is explicitly acknowledged:
167
+ BootProof clones credential-free HTTPS URLs from those named providers into `.bootproof/remotes/` and retains the clone so its evidence and any generated files continue to exist. It inspects the clone but refuses to execute remote code until host execution is explicitly acknowledged:
110
168
 
111
169
  ```bash
112
170
  npx bootproof up https://github.com/user/repo --provider local --unsafe-local
@@ -121,6 +179,7 @@ Contributors working from this source repository can use `npm ci`, `npm run buil
121
179
  BootProof is constrained on purpose:
122
180
 
123
181
  - no verified boot without an observed health signal
182
+ - no Docker-to-host execution fallback; host commands require `--provider local --unsafe-local`
124
183
  - no success rendering for skipped steps
125
184
  - no invented secrets
126
185
  - no writes to `.env`, `.env.local`, `.env.development`, or `.env.production`
@@ -138,20 +197,35 @@ See [docs/HONESTY_CONTRACT.md](docs/HONESTY_CONTRACT.md).
138
197
  BootProof currently provides:
139
198
 
140
199
  - Node package-manager and start-command inference
200
+ - conservative Go main-package, Rails `bin/rails`, and explicit Make run-target execution
141
201
  - Python/Flask and Go/Node hybrid detection
142
202
  - monorepo candidate ranking
143
203
  - Docker service dependency detection and scaffolding
204
+ - repository Compose execution when a web service builds the checked-out source and publishes an HTTP port
144
205
  - localhost health-candidate discovery from repository evidence and app logs
145
206
  - classified failures
146
207
  - signed Ed25519 attestations
147
208
  - strict JSON and fail-closed CI output
148
209
  - redacted registry-entry export
210
+ - deterministic sandboxed repairs with signed before/after receipts for the registered v0.3 classes
211
+ - explicit repair application with signature, scope, and stale-preimage checks
212
+ - marker-and-evidence-backed migration repair for Prisma, Django, Rails, Knex, and Drizzle
149
213
 
150
214
  Detection is broader than orchestration. For example:
151
215
 
152
216
  - Superset-like Python/Flask/React/Celery repos are detected, then honestly refused with `python_flask_setup_required`.
153
217
  - Grafana-like Go/Node hybrids are detected without pretending a frontend watcher is the whole application.
154
218
  - Parallel monorepo root commands are refused until a specific workspace is selected.
219
+ - Image-only or infrastructure-only Compose services are not accepted as proof of the checked-out source.
220
+
221
+ The supported repository entrypoints are deliberately narrow:
222
+
223
+ - Go: exactly one `main.go` or `cmd/*/main.go`
224
+ - Ruby: `Gemfile` plus `bin/rails`
225
+ - Make: an explicit `run`, `serve`, `server`, `start`, or `dev` target
226
+ - Compose: a service with a repository-local build context and a published HTTP port
227
+
228
+ Each path still requires an observed HTTP response. A successful Compose `up -d`, process spawn, or command exit is not a green result by itself.
155
229
 
156
230
  ## Files Written
157
231
 
@@ -160,6 +234,7 @@ Depending on the observed plan, BootProof may write:
160
234
  ```text
161
235
  .bootproof/attestation.json
162
236
  .bootproof/registry-entry.json
237
+ .bootproof/runtime/
163
238
  docker-compose.bootproof.yml
164
239
  .env.bootproof.example
165
240
  ```
@@ -252,9 +327,10 @@ BootProof is early alpha.
252
327
 
253
328
  Near-term work includes:
254
329
 
255
- - additional remote source providers beyond public HTTPS GitHub repositories
330
+ - additional remote source providers beyond GitHub, GitLab, Bitbucket, and Codeberg
331
+ - broader deterministic remediation coverage
256
332
  - stronger multi-service orchestration
257
- - broader Python and Go execution support
333
+ - broader Python, Go, Ruby, and Make execution support
258
334
  - CI/OIDC-backed signing
259
335
  - proof-linked badges and a verified public index
260
336
 
package/dist/cli.js CHANGED
@@ -9,14 +9,16 @@ import { pollHealth } from "./exec.js";
9
9
  import { buildRegistryEntry, verifyRegistryEntry, writeRegistryEntry, registryEntryPath } from "./registry.js";
10
10
  import { normalizeDockerBindPath, detectHostPlatform } from "./platform.js";
11
11
  import { diagnoseFailure } from "./diagnosis.js";
12
- import { cloneGithubRemote, isRemoteTarget, managedRemoteSource } from "./remote.js";
12
+ import { cloneRemoteTarget, isRemoteTarget, managedRemoteSource } from "./remote.js";
13
+ import { applyVerifiedRepair, repairRepo, verifyRepairReceipt, } from "./repair.js";
13
14
  let GREEN = "\x1b[32m", YELLOW = "\x1b[33m", RED = "\x1b[31m", DIM = "\x1b[2m", BOLD = "\x1b[1m", RESET = "\x1b[0m";
14
15
  const ok = (s) => console.log(`${GREEN}\u2713 ${s}${RESET}`);
15
16
  const would = (s) => console.log(`${DIM}\u25cb would: ${s}${RESET}`);
16
17
  const warn = (s) => console.log(`${YELLOW}! ${s}${RESET}`);
17
18
  const bad = (s) => console.log(`${RED}\u2717 ${s}${RESET}`);
18
19
  const disableColor = () => { GREEN = ""; YELLOW = ""; RED = ""; DIM = ""; BOLD = ""; RESET = ""; };
19
- const COMMANDS = ["up", "analyze", "plan", "verify", "explain", "attest", "help", "version", "--help", "-h", "--version"];
20
+ const portableRelative = (from, to) => path.relative(from, to).replace(/\\/g, "/");
21
+ const COMMANDS = ["up", "fix", "apply-repair", "analyze", "plan", "verify", "explain", "attest", "help", "version", "--help", "-h", "--version"];
20
22
  void normalizeDockerBindPath;
21
23
  void detectHostPlatform; // exported surface, used by docker provider work in progress
22
24
  if (process.env.NO_COLOR !== undefined)
@@ -45,12 +47,14 @@ function help() {
45
47
  console.log(`${BOLD}bootproof${RESET} — Human diagnosis. Machine proof. One engine.
46
48
 
47
49
  Usage:
48
- bootproof analyze <path|github-url> [--workspace dir] [--json]
50
+ bootproof analyze <path|git-url> [--workspace dir] [--json]
49
51
  inspect a repo, show evidence-based inference
50
- bootproof plan <path|github-url> [--workspace dir] show the run plan and files that WOULD be generated
51
- bootproof up <path|github-url> [options] execute the plan, verify localhost, write signed proof
52
- bootproof verify <path|attestation.json> validate an attestation signature and inspect its claim
53
- bootproof explain <attestation.json> human explanation of an attestation
52
+ bootproof plan <path|git-url> [--workspace dir] show the run plan and files that WOULD be generated
53
+ bootproof up <path|git-url> [options] execute the plan, verify localhost, write signed proof
54
+ bootproof fix <path|git-url> [options] test a deterministic repair in a sandbox
55
+ bootproof apply-repair <path> [--receipt proof.json] explicitly apply a signature-valid verified file change
56
+ bootproof verify <path|proof.json> validate an attestation or repair-receipt signature
57
+ bootproof explain <proof.json> explain an attestation or repair receipt
54
58
  bootproof attest export <path> redacted, re-signed shareable registry entry (never uploads)
55
59
  bootproof attest check <path> verify a registry entry signature
56
60
  bootproof version
@@ -66,6 +70,14 @@ Options for up:
66
70
  --json one bootproof/result/v1 JSON object on stdout
67
71
  --ci no prompts, colours, or interactive UI; fail closed
68
72
 
73
+ Options for fix:
74
+ --provider docker|local execution provider (default docker)
75
+ --unsafe-local required acknowledgement for local sandbox execution
76
+ --port <n> override inferred application port
77
+ --timeout <ms> before/after health timeout (default 60000)
78
+ --dry-run execute nothing, write nothing, produce no repair proof
79
+ --json one bootproof/repair-result/v1 object on stdout
80
+
69
81
  Honesty contract: no green check without an observed event; dry runs say "would";
70
82
  .env/.env.local are never written; secrets are never invented.
71
83
  Remote execution requires --provider local --unsafe-local. docs/HONESTY_CONTRACT.md`);
@@ -81,6 +93,11 @@ function printInference(inf) {
81
93
  console.log(` frontend markers: ${inf.frontendMarkers.join(", ")}`);
82
94
  if (inf.serviceMarkers.length)
83
95
  console.log(` service markers: ${inf.serviceMarkers.join(", ")}`);
96
+ if (inf.repoComposeFile)
97
+ console.log(` repo compose: ${inf.repoComposeFile} (bootproof defers to it)`);
98
+ if (inf.composeApplicationServices.length) {
99
+ console.log(` compose HTTP services: ${inf.composeApplicationServices.map(service => `${service.name} (${service.source === "build" ? "builds checked-out source" : "image only"})`).join("; ")}`);
100
+ }
84
101
  console.log(` package manager: ${inf.packageManager} ${DIM}(${inf.packageManagerEvidence})${RESET}`);
85
102
  if (inf.setupSteps.length)
86
103
  console.log(` setup steps: ${inf.setupSteps.join("; ")}`);
@@ -92,6 +109,8 @@ function printInference(inf) {
92
109
  console.log(` worker command: ${inf.workerCommand}`);
93
110
  if (inf.appCommand)
94
111
  console.log(` selected command: ${inf.appCommand} ${DIM}(${inf.appCommandSource})${RESET}`);
112
+ if (inf.preparationCommands.length)
113
+ console.log(` preparation: ${inf.preparationCommands.map(command => command.command).join("; ")}`);
95
114
  console.log(` command scope: ${inf.commandScope}`);
96
115
  console.log(` port: ${inf.port} ${DIM}(${inf.portEvidence})${RESET}`);
97
116
  if (inf.healthCandidates.length)
@@ -145,6 +164,43 @@ function printFailure(failureClass, diagnosis, evidencePath) {
145
164
  console.log(`Safe next step: ${diagnosis.safeNextStep}`);
146
165
  console.log(`Evidence: ${evidencePath}`);
147
166
  }
167
+ function isRepairReceipt(value) {
168
+ return Boolean(value && typeof value === "object" && value.schema === "bootproof/repair-receipt/v1");
169
+ }
170
+ function printRepairResult(result) {
171
+ if (result.repaired) {
172
+ ok(`${BOLD}VERIFIED REPAIR${RESET}${GREEN} — ${result.repairId}`);
173
+ console.log(result.explanation);
174
+ if (result.patchPath)
175
+ console.log(`Patch: ${result.patchPath}`);
176
+ console.log(`Receipt: ${result.receiptPath}`);
177
+ console.log(`After attestation: ${result.afterAttestationPath}`);
178
+ return;
179
+ }
180
+ bad(`${BOLD}NO VERIFIED REPAIR${RESET}${RED}${result.failureClass ? ` — ${result.failureClass}` : ""}`);
181
+ console.log(result.explanation);
182
+ }
183
+ function printRepairApplyResult(result) {
184
+ if (result.applied) {
185
+ ok(`${BOLD}APPLIED VERIFIED REPAIR${RESET}`);
186
+ console.log(result.explanation);
187
+ console.log(`Receipt: ${result.receiptPath}`);
188
+ return;
189
+ }
190
+ bad(`${BOLD}REPAIR NOT APPLIED${RESET}`);
191
+ console.log(result.explanation);
192
+ }
193
+ function rebaseRemoteRepairPaths(result, repo) {
194
+ const rebase = (value) => value
195
+ ? portableRelative(process.cwd(), path.join(repo, value))
196
+ : null;
197
+ return {
198
+ ...result,
199
+ receiptPath: rebase(result.receiptPath),
200
+ patchPath: rebase(result.patchPath),
201
+ afterAttestationPath: rebase(result.afterAttestationPath),
202
+ };
203
+ }
148
204
  async function main() {
149
205
  const [cmd, ...rest] = process.argv.slice(2);
150
206
  if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h")
@@ -164,7 +220,7 @@ async function main() {
164
220
  let target = path.resolve(targetInput);
165
221
  let remote = null;
166
222
  let remoteSource = null;
167
- if (["analyze", "plan", "up"].includes(cmd) && isRemoteTarget(targetInput)) {
223
+ if (["analyze", "plan", "up", "fix"].includes(cmd) && isRemoteTarget(targetInput)) {
168
224
  if (flags["dry-run"]) {
169
225
  const explanation = "Remote dry runs are refused because cloning would write files, while BootProof dry runs promise to write nothing.";
170
226
  if (flags.json)
@@ -175,12 +231,12 @@ async function main() {
175
231
  return;
176
232
  }
177
233
  try {
178
- remote = cloneGithubRemote(targetInput, process.cwd());
234
+ remote = cloneRemoteTarget(targetInput, process.cwd());
179
235
  target = remote.repoPath;
180
236
  remoteSource = remote.canonicalUrl;
181
237
  if (!flags.json) {
182
238
  console.log(`${DIM}Remote source: ${remote.canonicalUrl}${RESET}`);
183
- console.log(`${DIM}Clone retained at: ${path.relative(process.cwd(), remote.repoPath)}${RESET}`);
239
+ console.log(`${DIM}Clone retained at: ${portableRelative(process.cwd(), remote.repoPath)}${RESET}`);
184
240
  }
185
241
  }
186
242
  catch (error) {
@@ -193,14 +249,14 @@ async function main() {
193
249
  return;
194
250
  }
195
251
  }
196
- if (!remote && ["analyze", "plan", "up"].includes(cmd)) {
252
+ if (!remote && ["analyze", "plan", "up", "fix"].includes(cmd)) {
197
253
  remoteSource = managedRemoteSource(target);
198
254
  if (remoteSource && !flags.json) {
199
255
  console.log(`${DIM}Managed remote source: ${remoteSource}${RESET}`);
200
256
  }
201
257
  }
202
258
  const evidencePath = remote
203
- ? path.relative(process.cwd(), attestationPath(target))
259
+ ? portableRelative(process.cwd(), attestationPath(target))
204
260
  : ".bootproof/attestation.json";
205
261
  if (cmd === "analyze") {
206
262
  const inf = inferRepo(target, { workspace: flags.workspace });
@@ -223,6 +279,110 @@ async function main() {
223
279
  console.log(`${DIM}--- .env.bootproof.example (preview) ---\n${envExampleFor(inf)}${RESET}`);
224
280
  return;
225
281
  }
282
+ if (cmd === "apply-repair") {
283
+ if (isRemoteTarget(targetInput)) {
284
+ const result = {
285
+ schema: "bootproof/repair-apply-result/v1",
286
+ applied: false,
287
+ receiptPath: String(flags.receipt ?? ".bootproof/repair-receipt.json"),
288
+ filesChanged: [],
289
+ explanation: "apply-repair requires a local working tree; use the retained managed clone path for a remote repair",
290
+ };
291
+ if (flags.json)
292
+ console.log(JSON.stringify(result));
293
+ else
294
+ printRepairApplyResult(result);
295
+ process.exitCode = 1;
296
+ return;
297
+ }
298
+ if (flags["dry-run"]) {
299
+ const result = {
300
+ schema: "bootproof/repair-apply-result/v1",
301
+ applied: false,
302
+ receiptPath: String(flags.receipt ?? ".bootproof/repair-receipt.json"),
303
+ filesChanged: [],
304
+ explanation: "Dry run — no repair files were applied.",
305
+ };
306
+ if (flags.json)
307
+ console.log(JSON.stringify(result));
308
+ else
309
+ printRepairApplyResult(result);
310
+ process.exitCode = 1;
311
+ return;
312
+ }
313
+ const receipt = flags.receipt
314
+ ? path.resolve(String(flags.receipt))
315
+ : path.join(target, ".bootproof", "repair-receipt.json");
316
+ const result = applyVerifiedRepair(target, receipt);
317
+ if (flags.json)
318
+ console.log(JSON.stringify(result));
319
+ else
320
+ printRepairApplyResult(result);
321
+ process.exitCode = result.applied ? 0 : 1;
322
+ return;
323
+ }
324
+ if (cmd === "fix") {
325
+ if (flags["dry-run"]) {
326
+ const result = {
327
+ schema: "bootproof/repair-result/v1",
328
+ repaired: false,
329
+ failureClass: null,
330
+ repairId: null,
331
+ receiptPath: null,
332
+ patchPath: null,
333
+ afterAttestationPath: null,
334
+ explanation: "Dry run — nothing was executed, nothing was written, and no repair proof exists.",
335
+ };
336
+ if (flags.json)
337
+ console.log(JSON.stringify(result));
338
+ else
339
+ printRepairResult(result);
340
+ process.exitCode = 1;
341
+ return;
342
+ }
343
+ const provider = flags.provider;
344
+ const timeoutMs = Number(flags.timeout ?? 60_000);
345
+ const port = flags.port === undefined ? undefined : Number(flags.port);
346
+ const optionError = provider !== undefined && provider !== "docker" && provider !== "local"
347
+ ? `invalid --provider value: ${String(provider)} (expected docker or local)`
348
+ : !Number.isFinite(timeoutMs) || timeoutMs <= 0
349
+ ? `invalid --timeout value: ${String(flags.timeout)} (expected a positive number)`
350
+ : port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535)
351
+ ? `invalid --port value: ${String(flags.port)} (expected an integer from 1 to 65535)`
352
+ : null;
353
+ if (optionError) {
354
+ const result = {
355
+ schema: "bootproof/repair-result/v1",
356
+ repaired: false,
357
+ failureClass: null,
358
+ repairId: null,
359
+ receiptPath: null,
360
+ patchPath: null,
361
+ afterAttestationPath: null,
362
+ explanation: optionError,
363
+ };
364
+ if (flags.json)
365
+ console.log(JSON.stringify(result));
366
+ else
367
+ printRepairResult(result);
368
+ process.exitCode = 1;
369
+ return;
370
+ }
371
+ const repairResult = await repairRepo(target, {
372
+ provider: provider,
373
+ unsafeLocal: Boolean(flags["unsafe-local"]),
374
+ timeoutMs,
375
+ port,
376
+ remoteSource: remoteSource ?? undefined,
377
+ });
378
+ const result = remote ? rebaseRemoteRepairPaths(repairResult, target) : repairResult;
379
+ if (flags.json)
380
+ console.log(JSON.stringify(result));
381
+ else
382
+ printRepairResult(result);
383
+ process.exitCode = result.repaired ? 0 : 1;
384
+ return;
385
+ }
226
386
  if (cmd === "up") {
227
387
  const provider = flags.provider ?? "docker";
228
388
  const timeoutMs = Number(flags.timeout ?? 60_000);
@@ -298,11 +458,20 @@ async function main() {
298
458
  if (cmd === "verify") {
299
459
  const p = path.extname(target) === ".json" ? target : attestationPath(target);
300
460
  if (!fs.existsSync(p)) {
301
- bad(`no attestation at ${p} — run bootproof up first, or this repo has no committed proof yet`);
461
+ bad(`no proof at ${p} — run bootproof up or bootproof fix first`);
302
462
  process.exitCode = 1;
303
463
  return;
304
464
  }
305
- const att = JSON.parse(fs.readFileSync(p, "utf8"));
465
+ const proof = JSON.parse(fs.readFileSync(p, "utf8"));
466
+ if (isRepairReceipt(proof)) {
467
+ const valid = verifyRepairReceipt(proof);
468
+ (valid ? ok : bad)(`repair receipt signature ${valid ? "valid" : "INVALID"} (ed25519, trust-on-first-use)`);
469
+ console.log(`${DIM}failure=${proof.verification.before.failureClass} repair=${proof.repair.id} after=${proof.verification.after.healthObservation}${RESET}`);
470
+ if (!valid)
471
+ process.exitCode = 1;
472
+ return;
473
+ }
474
+ const att = proof;
306
475
  const sig = verifySignature(att);
307
476
  (sig ? ok : bad)(`signature ${sig ? "valid" : "INVALID"} (ed25519, trust-on-first-use)`);
308
477
  console.log(`Trust level: ${att.trust?.level ?? "legacy_unspecified"}`);
@@ -367,7 +536,29 @@ async function main() {
367
536
  }
368
537
  if (cmd === "explain") {
369
538
  const p = positional[0] ? path.resolve(positional[0]) : attestationPath(target);
370
- const att = JSON.parse(fs.readFileSync(p, "utf8"));
539
+ const proof = JSON.parse(fs.readFileSync(p, "utf8"));
540
+ if (isRepairReceipt(proof)) {
541
+ const valid = verifyRepairReceipt(proof);
542
+ console.log(`${BOLD}Repair receipt explained${RESET}`);
543
+ console.log(`Signature: ${valid ? "valid" : "INVALID"}`);
544
+ if (!valid) {
545
+ console.log("The receipt has been tampered with or is malformed. Its repair claims are not trusted.");
546
+ process.exitCode = 1;
547
+ return;
548
+ }
549
+ console.log(`Before: NOT VERIFIED — ${proof.verification.before.failureClass}.`);
550
+ console.log(`Repair: ${proof.repair.id} (${proof.repair.kind}).`);
551
+ console.log(`After: BOOTED — ${proof.verification.after.healthObservation}.`);
552
+ console.log(`Description: ${proof.repair.description}`);
553
+ if (proof.repair.filesChanged.length)
554
+ console.log(`Files changed in sandbox: ${proof.repair.filesChanged.join(", ")}`);
555
+ if (proof.repair.planDelta)
556
+ console.log(`Plan delta: ${proof.repair.planDelta}`);
557
+ if (proof.repair.envDelta)
558
+ console.log(`Environment delta: ${proof.repair.envDelta}`);
559
+ return;
560
+ }
561
+ const att = proof;
371
562
  console.log(`${BOLD}Attestation explained${RESET}`);
372
563
  console.log(att.result.booted ? `This run BOOTED: ${att.result.healthObservation}.` : `This run did NOT verify. Failure class: ${att.result.failureClass}.`);
373
564
  console.log(`Trust level: ${att.trust?.level ?? "legacy_unspecified"}`);
@@ -392,9 +583,32 @@ async function main() {
392
583
  }
393
584
  main().catch(err => {
394
585
  const argv = process.argv.slice(2);
395
- if (argv[0] === "up" && argv.includes("--json")) {
586
+ if (argv.includes("--json") && argv[0] === "up") {
396
587
  console.log(JSON.stringify(machineFailure(String(err?.message ?? err))));
397
588
  }
589
+ else if (argv.includes("--json") && argv[0] === "fix") {
590
+ const result = {
591
+ schema: "bootproof/repair-result/v1",
592
+ repaired: false,
593
+ failureClass: null,
594
+ repairId: null,
595
+ receiptPath: null,
596
+ patchPath: null,
597
+ afterAttestationPath: null,
598
+ explanation: String(err?.message ?? err),
599
+ };
600
+ console.log(JSON.stringify(result));
601
+ }
602
+ else if (argv.includes("--json") && argv[0] === "apply-repair") {
603
+ const result = {
604
+ schema: "bootproof/repair-apply-result/v1",
605
+ applied: false,
606
+ receiptPath: ".bootproof/repair-receipt.json",
607
+ filesChanged: [],
608
+ explanation: String(err?.message ?? err),
609
+ };
610
+ console.log(JSON.stringify(result));
611
+ }
398
612
  else {
399
613
  bad(String(err?.message ?? err));
400
614
  }
package/dist/diagnosis.js CHANGED
@@ -30,6 +30,12 @@ export function diagnoseFailure(failureClass, evidence, explanation, inference)
30
30
  whyRefused: "BootProof cannot yet orchestrate that multi-step application safely enough to claim a verified boot.",
31
31
  safeNextStep: "Review the detected setup and service commands, complete the repository's documented initialization, then rerun when orchestration support is available.",
32
32
  };
33
+ case "orchestration_not_supported":
34
+ return {
35
+ whatHappened: explanation,
36
+ whyRefused: "The detected application requires backend/frontend or repository-specific orchestration that BootProof cannot yet execute safely.",
37
+ safeNextStep: "Use the repository's documented runbook. Treat this attestation as diagnosis only, not proof of a localhost boot.",
38
+ };
33
39
  case "workspace_ambiguous":
34
40
  if (/multiple workspaces in parallel|starts multiple workspaces in parallel/i.test(explanation)) {
35
41
  return {
@@ -79,6 +85,12 @@ export function diagnoseFailure(failureClass, evidence, explanation, inference)
79
85
  whyRefused: "BootProof cannot run the declared install or start command without that executable.",
80
86
  safeNextStep: "Enable Corepack or install the repository's declared package manager, then rerun BootProof.",
81
87
  };
88
+ case "missing_runtime_tool":
89
+ return {
90
+ whatHappened: explanation,
91
+ whyRefused: "BootProof cannot execute the repository's explicit run path without its declared runtime or build tool.",
92
+ safeNextStep: "Install the required tool at a version supported by the repository, then rerun BootProof.",
93
+ };
82
94
  case "runtime_engine_mismatch":
83
95
  return {
84
96
  whatHappened: "The available Node.js runtime does not satisfy the repository's declared engine requirement.",
@@ -87,7 +99,7 @@ export function diagnoseFailure(failureClass, evidence, explanation, inference)
87
99
  };
88
100
  case "missing_env_var":
89
101
  return {
90
- whatHappened: "The application reported required environment configuration that is missing.",
102
+ whatHappened: explanation,
91
103
  whyRefused: "BootProof will not invent secrets or write protected .env files to force startup.",
92
104
  safeNextStep: "Provide the real required values using the repository's documented configuration path, then rerun BootProof.",
93
105
  };
package/dist/exec.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn, spawnSync } from "node:child_process";
2
2
  import http from "node:http";
3
3
  const TAIL = 4000;
4
4
  const tail = (s) => (s.length > TAIL ? s.slice(-TAIL) : s);
@@ -39,8 +39,9 @@ function killTree(pid, signal = "SIGTERM") {
39
39
  if (!pid)
40
40
  return;
41
41
  try {
42
- if (process.platform === "win32")
43
- process.kill(pid, signal);
42
+ if (process.platform === "win32") {
43
+ spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
44
+ }
44
45
  else
45
46
  process.kill(-pid, signal); // negative pid = whole process group
46
47
  }
@@ -116,7 +117,23 @@ function probe(url) {
116
117
  });
117
118
  }
118
119
  export function minimalEnv(extra = {}) {
119
- const keep = ["PATH", "HOME", "USER", "SHELL", "TMPDIR", "TEMP", "LANG", "TERM", "NODE_OPTIONS", "COREPACK_HOME", "npm_config_cache"];
120
+ const keep = [
121
+ "PATH",
122
+ "HOME",
123
+ "USER",
124
+ "SHELL",
125
+ "TMPDIR",
126
+ "TEMP",
127
+ "LANG",
128
+ "TERM",
129
+ "NODE_OPTIONS",
130
+ "COREPACK_HOME",
131
+ "npm_config_cache",
132
+ "SystemRoot",
133
+ "SYSTEMROOT",
134
+ "ComSpec",
135
+ "PATHEXT",
136
+ ];
120
137
  const env = {};
121
138
  for (const k of keep)
122
139
  if (process.env[k])