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 +84 -8
- package/dist/cli.js +230 -16
- package/dist/diagnosis.js +13 -1
- package/dist/exec.js +21 -4
- package/dist/infer.js +281 -32
- package/dist/plan.d.ts +2 -0
- package/dist/plan.js +47 -7
- package/dist/proof.d.ts +1 -1
- package/dist/proof.js +2 -2
- package/dist/remote.d.ts +12 -1
- package/dist/remote.js +62 -18
- package/dist/repair.d.ts +110 -0
- package/dist/repair.js +857 -0
- package/dist/run.d.ts +3 -1
- package/dist/run.js +182 -20
- package/dist/taxonomy.d.ts +1 -0
- package/dist/taxonomy.js +28 -4
- package/dist/types.d.ts +18 -2
- package/docs/CI_ACTION.md +4 -3
- package/docs/FAILURE_TAXONOMY.md +3 -1
- package/docs/HONESTY_CONTRACT.md +30 -1
- package/docs/REAL_REPO_EVIDENCE.md +77 -0
- package/docs/RELEASE_CHECKLIST.md +9 -1
- package/docs/REPAIR_RECEIPT.md +178 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
# BootProof
|
|
2
2
|
|
|
3
|
+
[](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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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|
|
|
50
|
+
bootproof analyze <path|git-url> [--workspace dir] [--json]
|
|
49
51
|
inspect a repo, show evidence-based inference
|
|
50
|
-
bootproof plan <path|
|
|
51
|
-
bootproof up <path|
|
|
52
|
-
bootproof
|
|
53
|
-
bootproof
|
|
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 =
|
|
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: ${
|
|
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
|
-
?
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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:
|
|
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
|
-
|
|
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 = [
|
|
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])
|