bootproof 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { inferRepo } from "./infer.js";
5
+ import { buildPlan, composeFileFor, envExampleFor } from "./plan.js";
6
+ import { up } from "./run.js";
7
+ import { verifySignature, attestationPath, TOOL_ID } from "./proof.js";
8
+ import { pollHealth } from "./exec.js";
9
+ import { buildRegistryEntry, verifyRegistryEntry, writeRegistryEntry, registryEntryPath } from "./registry.js";
10
+ import { normalizeDockerBindPath, detectHostPlatform } from "./platform.js";
11
+ import { diagnoseFailure } from "./diagnosis.js";
12
+ import { cloneGithubRemote, isRemoteTarget, managedRemoteSource } from "./remote.js";
13
+ let GREEN = "\x1b[32m", YELLOW = "\x1b[33m", RED = "\x1b[31m", DIM = "\x1b[2m", BOLD = "\x1b[1m", RESET = "\x1b[0m";
14
+ const ok = (s) => console.log(`${GREEN}\u2713 ${s}${RESET}`);
15
+ const would = (s) => console.log(`${DIM}\u25cb would: ${s}${RESET}`);
16
+ const warn = (s) => console.log(`${YELLOW}! ${s}${RESET}`);
17
+ const bad = (s) => console.log(`${RED}\u2717 ${s}${RESET}`);
18
+ const disableColor = () => { GREEN = ""; YELLOW = ""; RED = ""; DIM = ""; BOLD = ""; RESET = ""; };
19
+ const COMMANDS = ["up", "analyze", "plan", "verify", "explain", "attest", "help", "version", "--help", "-h", "--version"];
20
+ void normalizeDockerBindPath;
21
+ void detectHostPlatform; // exported surface, used by docker provider work in progress
22
+ if (process.env.NO_COLOR !== undefined)
23
+ disableColor();
24
+ function parseFlags(argv) {
25
+ const flags = {};
26
+ const positional = [];
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i];
29
+ if (a.startsWith("--")) {
30
+ const key = a.slice(2);
31
+ const next = argv[i + 1];
32
+ if (next && !next.startsWith("--")) {
33
+ flags[key] = next;
34
+ i++;
35
+ }
36
+ else
37
+ flags[key] = true;
38
+ }
39
+ else
40
+ positional.push(a);
41
+ }
42
+ return { flags, positional };
43
+ }
44
+ function help() {
45
+ console.log(`${BOLD}bootproof${RESET} — Human diagnosis. Machine proof. One engine.
46
+
47
+ Usage:
48
+ bootproof analyze <path|github-url> [--workspace dir] [--json]
49
+ 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
54
+ bootproof attest export <path> redacted, re-signed shareable registry entry (never uploads)
55
+ bootproof attest check <path> verify a registry entry signature
56
+ bootproof version
57
+
58
+ Options for up:
59
+ --provider docker|local execution provider (default docker)
60
+ --unsafe-local required acknowledgement for --provider local
61
+ --install run the dependency install step (off by default)
62
+ --workspace <dir> pick a monorepo workspace
63
+ --port <n> override inferred port
64
+ --timeout <ms> health verification timeout (default 60000)
65
+ --dry-run show what would happen; executes nothing, writes nothing
66
+ --json one bootproof/result/v1 JSON object on stdout
67
+ --ci no prompts, colours, or interactive UI; fail closed
68
+
69
+ Honesty contract: no green check without an observed event; dry runs say "would";
70
+ .env/.env.local are never written; secrets are never invented.
71
+ Remote execution requires --provider local --unsafe-local. docs/HONESTY_CONTRACT.md`);
72
+ }
73
+ function printInference(inf) {
74
+ console.log(`${BOLD}Inference (evidence-based)${RESET}`);
75
+ console.log(` application: ${inf.isApplication ? "yes" : `no — ${inf.notAppReason}`}`);
76
+ if (inf.stack.length)
77
+ console.log(` stack: ${inf.stack.join(", ")}`);
78
+ if (inf.backendMarkers.length)
79
+ console.log(` backend markers: ${inf.backendMarkers.join(", ")}`);
80
+ if (inf.frontendMarkers.length)
81
+ console.log(` frontend markers: ${inf.frontendMarkers.join(", ")}`);
82
+ if (inf.serviceMarkers.length)
83
+ console.log(` service markers: ${inf.serviceMarkers.join(", ")}`);
84
+ console.log(` package manager: ${inf.packageManager} ${DIM}(${inf.packageManagerEvidence})${RESET}`);
85
+ if (inf.setupSteps.length)
86
+ console.log(` setup steps: ${inf.setupSteps.join("; ")}`);
87
+ if (inf.backendCommand)
88
+ console.log(` backend command: ${inf.backendCommand}`);
89
+ if (inf.frontendCommand)
90
+ console.log(` frontend command: ${inf.frontendCommand}`);
91
+ if (inf.workerCommand)
92
+ console.log(` worker command: ${inf.workerCommand}`);
93
+ if (inf.appCommand)
94
+ console.log(` selected command: ${inf.appCommand} ${DIM}(${inf.appCommandSource})${RESET}`);
95
+ console.log(` command scope: ${inf.commandScope}`);
96
+ console.log(` port: ${inf.port} ${DIM}(${inf.portEvidence})${RESET}`);
97
+ if (inf.healthCandidates.length)
98
+ console.log(` health candidates: ${inf.healthCandidates.join(", ")}`);
99
+ if (inf.services.length)
100
+ console.log(` services: ${inf.services.map(s => `${s.kind} (${s.evidence})`).join("; ")}`);
101
+ if (inf.envWithoutSafeDefault.length)
102
+ console.log(` secrets you must provide: ${inf.envWithoutSafeDefault.join(", ")}`);
103
+ if (inf.workspaces.length > 1) {
104
+ console.log(` monorepo candidates (ranked):`);
105
+ for (const w of inf.workspaces.slice(0, 8))
106
+ console.log(` ${w.score >= 3 ? "*" : " "} ${w.dir} ${DIM}(${w.name}; ${w.reason})${RESET}`);
107
+ }
108
+ console.log(` confidence: ${inf.confidence}% ${DIM}(heuristic score of evidence found, not a success prediction)${RESET}`);
109
+ }
110
+ function machineResult(outcome, evidencePath) {
111
+ const result = outcome.attestation?.result;
112
+ return {
113
+ schema: "bootproof/result/v1",
114
+ booted: result?.booted ?? false,
115
+ healthVerified: result?.healthVerified ?? false,
116
+ failureClass: result?.failureClass ?? outcome.refusal?.failureClass ?? null,
117
+ attestationPath: outcome.attestation ? evidencePath : null,
118
+ inference: outcome.inference,
119
+ plan: outcome.plan,
120
+ observed: outcome.attestation?.observed ?? [],
121
+ explanation: result?.explanation ?? outcome.refusal?.explanation ?? null,
122
+ trust: outcome.attestation?.trust ?? null,
123
+ writtenFiles: outcome.writtenFiles,
124
+ };
125
+ }
126
+ function machineFailure(explanation) {
127
+ return {
128
+ schema: "bootproof/result/v1",
129
+ booted: false,
130
+ healthVerified: false,
131
+ failureClass: "unknown_failure",
132
+ attestationPath: null,
133
+ inference: {},
134
+ plan: {},
135
+ observed: [],
136
+ explanation,
137
+ trust: null,
138
+ writtenFiles: [],
139
+ };
140
+ }
141
+ function printFailure(failureClass, diagnosis, evidencePath) {
142
+ bad(`${BOLD}NOT VERIFIED${RESET}${RED} — ${failureClass}`);
143
+ console.log(`What happened: ${diagnosis.whatHappened}`);
144
+ console.log(`Why BootProof refused: ${diagnosis.whyRefused}`);
145
+ console.log(`Safe next step: ${diagnosis.safeNextStep}`);
146
+ console.log(`Evidence: ${evidencePath}`);
147
+ }
148
+ async function main() {
149
+ const [cmd, ...rest] = process.argv.slice(2);
150
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h")
151
+ return help();
152
+ if (cmd === "version" || cmd === "--version")
153
+ return console.log(TOOL_ID);
154
+ if (!COMMANDS.includes(cmd)) {
155
+ bad(`unknown command: ${cmd}`);
156
+ console.log(`Run ${BOLD}bootproof help${RESET}. Bootproof never guesses what you meant.`);
157
+ process.exitCode = 1;
158
+ return;
159
+ }
160
+ const { flags, positional } = parseFlags(rest);
161
+ if (flags.ci || flags.json)
162
+ disableColor();
163
+ const targetInput = String(positional[0] ?? ".");
164
+ let target = path.resolve(targetInput);
165
+ let remote = null;
166
+ let remoteSource = null;
167
+ if (["analyze", "plan", "up"].includes(cmd) && isRemoteTarget(targetInput)) {
168
+ if (flags["dry-run"]) {
169
+ const explanation = "Remote dry runs are refused because cloning would write files, while BootProof dry runs promise to write nothing.";
170
+ if (flags.json)
171
+ console.log(JSON.stringify(machineFailure(explanation)));
172
+ else
173
+ bad(explanation);
174
+ process.exitCode = 1;
175
+ return;
176
+ }
177
+ try {
178
+ remote = cloneGithubRemote(targetInput, process.cwd());
179
+ target = remote.repoPath;
180
+ remoteSource = remote.canonicalUrl;
181
+ if (!flags.json) {
182
+ console.log(`${DIM}Remote source: ${remote.canonicalUrl}${RESET}`);
183
+ console.log(`${DIM}Clone retained at: ${path.relative(process.cwd(), remote.repoPath)}${RESET}`);
184
+ }
185
+ }
186
+ catch (error) {
187
+ const explanation = error instanceof Error ? error.message : String(error);
188
+ if (flags.json)
189
+ console.log(JSON.stringify(machineFailure(explanation)));
190
+ else
191
+ bad(explanation);
192
+ process.exitCode = 1;
193
+ return;
194
+ }
195
+ }
196
+ if (!remote && ["analyze", "plan", "up"].includes(cmd)) {
197
+ remoteSource = managedRemoteSource(target);
198
+ if (remoteSource && !flags.json) {
199
+ console.log(`${DIM}Managed remote source: ${remoteSource}${RESET}`);
200
+ }
201
+ }
202
+ const evidencePath = remote
203
+ ? path.relative(process.cwd(), attestationPath(target))
204
+ : ".bootproof/attestation.json";
205
+ if (cmd === "analyze") {
206
+ const inf = inferRepo(target, { workspace: flags.workspace });
207
+ if (flags.json)
208
+ return console.log(JSON.stringify(inf, null, 2));
209
+ return printInference(inf);
210
+ }
211
+ if (cmd === "plan") {
212
+ const inf = inferRepo(target, { workspace: flags.workspace });
213
+ printInference(inf);
214
+ const plan = buildPlan(inf, flags.provider ?? "docker");
215
+ console.log(`\n${BOLD}Plan (nothing has been executed or written)${RESET}`);
216
+ for (const s of plan.steps)
217
+ would(s.command ? `${s.description} — ${DIM}${s.command}${RESET}` : s.description);
218
+ for (const f of plan.generatedFiles)
219
+ would(`generate ${f.path} (${f.purpose})`);
220
+ if (composeFileFor(inf))
221
+ console.log(`\n${DIM}--- docker-compose.bootproof.yml (preview) ---\n${composeFileFor(inf)}${RESET}`);
222
+ if (envExampleFor(inf))
223
+ console.log(`${DIM}--- .env.bootproof.example (preview) ---\n${envExampleFor(inf)}${RESET}`);
224
+ return;
225
+ }
226
+ if (cmd === "up") {
227
+ const provider = flags.provider ?? "docker";
228
+ const timeoutMs = Number(flags.timeout ?? 60_000);
229
+ const port = flags.port === undefined ? undefined : Number(flags.port);
230
+ const optionError = provider !== "docker" && provider !== "local"
231
+ ? `invalid --provider value: ${String(provider)} (expected docker or local)`
232
+ : !Number.isFinite(timeoutMs) || timeoutMs <= 0
233
+ ? `invalid --timeout value: ${String(flags.timeout)} (expected a positive number)`
234
+ : port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535)
235
+ ? `invalid --port value: ${String(flags.port)} (expected an integer from 1 to 65535)`
236
+ : null;
237
+ if (optionError) {
238
+ if (flags.json)
239
+ console.log(JSON.stringify(machineFailure(optionError)));
240
+ else
241
+ bad(optionError);
242
+ process.exitCode = 1;
243
+ return;
244
+ }
245
+ const opts = {
246
+ provider: provider,
247
+ unsafeLocal: Boolean(flags["unsafe-local"]),
248
+ dryRun: Boolean(flags["dry-run"]),
249
+ remoteSource: remoteSource ?? undefined,
250
+ workspace: flags.workspace,
251
+ timeoutMs,
252
+ install: Boolean(flags.install),
253
+ port,
254
+ };
255
+ const outcome = await up(target, opts);
256
+ const verified = outcome.attestation?.result.booted === true && outcome.attestation.result.healthVerified === true;
257
+ if (flags.json) {
258
+ console.log(JSON.stringify(machineResult(outcome, evidencePath)));
259
+ if (flags.ci || !opts.dryRun)
260
+ process.exitCode = verified ? 0 : 1;
261
+ return;
262
+ }
263
+ printInference(outcome.inference);
264
+ console.log("");
265
+ if (outcome.refusal) {
266
+ for (const o of outcome.attestation?.observed ?? [])
267
+ (o.observation.startsWith("skipped") ? warn : o.ok ? ok : bad)(`${o.id}: ${o.observation}`);
268
+ const diagnosis = diagnoseFailure(outcome.refusal.failureClass, outcome.attestation?.result.failureEvidence ?? null, outcome.refusal.explanation, outcome.inference);
269
+ printFailure(outcome.refusal.failureClass, diagnosis, evidencePath);
270
+ process.exitCode = 1;
271
+ return;
272
+ }
273
+ if (opts.dryRun) {
274
+ console.log(`${BOLD}Dry run — nothing was executed, nothing was written, no proof exists.${RESET}`);
275
+ for (const s of outcome.plan.steps)
276
+ would(s.command ? `${s.description} — ${DIM}${s.command}${RESET}` : s.description);
277
+ for (const f of outcome.plan.generatedFiles)
278
+ would(`generate ${f.path}`);
279
+ if (flags.ci)
280
+ process.exitCode = 1;
281
+ return;
282
+ }
283
+ for (const o of outcome.attestation.observed)
284
+ (o.observation.startsWith("skipped") ? warn : o.ok ? ok : bad)(`${o.id}: ${o.observation}`);
285
+ const r = outcome.attestation.result;
286
+ console.log("");
287
+ if (r.healthVerified) {
288
+ ok(`${BOLD}BOOTED${RESET}${GREEN} — ${r.healthObservation} (observed, signed)`);
289
+ console.log(`Evidence: ${evidencePath}`);
290
+ }
291
+ else {
292
+ const diagnosis = diagnoseFailure(r.failureClass, r.failureEvidence, r.explanation, outcome.inference);
293
+ printFailure(r.failureClass, diagnosis, evidencePath);
294
+ process.exitCode = 1;
295
+ }
296
+ return;
297
+ }
298
+ if (cmd === "verify") {
299
+ const p = path.extname(target) === ".json" ? target : attestationPath(target);
300
+ if (!fs.existsSync(p)) {
301
+ bad(`no attestation at ${p} — run bootproof up first, or this repo has no committed proof yet`);
302
+ process.exitCode = 1;
303
+ return;
304
+ }
305
+ const att = JSON.parse(fs.readFileSync(p, "utf8"));
306
+ const sig = verifySignature(att);
307
+ (sig ? ok : bad)(`signature ${sig ? "valid" : "INVALID"} (ed25519, trust-on-first-use)`);
308
+ console.log(`Trust level: ${att.trust?.level ?? "legacy_unspecified"}`);
309
+ console.log(`${DIM}attested: booted=${att.result.booted} at commit ${att.repo.commit ?? "unknown"} on ${att.environment.os} node ${att.environment.node}${RESET}`);
310
+ const retainedRemote = managedRemoteSource(att.repo.path);
311
+ if (retainedRemote) {
312
+ console.log(`Retained remote source: ${retainedRemote}`);
313
+ console.log("Replay requires explicit host execution acknowledgement: bootproof up <clone-path> --provider local --unsafe-local.");
314
+ }
315
+ else {
316
+ console.log(`Replaying attested plan with bootproof up --provider ${att.plan.provider} would re-verify it on this machine.`);
317
+ }
318
+ if (att.result.booted) {
319
+ const live = await pollHealth(att.plan.healthUrl, 3000);
320
+ if (live.responded)
321
+ ok(`bonus observation: ${att.plan.healthUrl} is responding right now (HTTP ${live.status})`);
322
+ else
323
+ console.log(`${DIM}(app not currently running — attestation describes a past verified run)${RESET}`);
324
+ }
325
+ if (!sig)
326
+ process.exitCode = 1;
327
+ return;
328
+ }
329
+ if (cmd === "attest") {
330
+ const sub = positional[0];
331
+ const repo = path.resolve(String(positional[1] ?? "."));
332
+ if (sub === "export") {
333
+ const ap = attestationPath(repo);
334
+ if (!fs.existsSync(ap)) {
335
+ bad(`no attestation at ${ap} — run bootproof up first`);
336
+ process.exitCode = 1;
337
+ return;
338
+ }
339
+ const att = JSON.parse(fs.readFileSync(ap, "utf8"));
340
+ const entry = buildRegistryEntry(att);
341
+ const out = writeRegistryEntry(repo, entry);
342
+ ok(`wrote redacted registry entry: ${out}`);
343
+ console.log(`${DIM}redactions applied: ${entry.redactionsApplied.length ? entry.redactionsApplied.join(", ") : "none needed"}${RESET}`);
344
+ console.log(`Nothing has been uploaded. Bootproof never uploads. To share this proof:`);
345
+ console.log(` 1. review the file above — it is exactly what others will see;`);
346
+ console.log(` 2. commit .bootproof/ to your repo (git is the registry), or attach it to a PR/issue.`);
347
+ return;
348
+ }
349
+ if (sub === "check") {
350
+ const ep = registryEntryPath(repo);
351
+ if (!fs.existsSync(ep)) {
352
+ bad(`no registry entry at ${ep}`);
353
+ process.exitCode = 1;
354
+ return;
355
+ }
356
+ const entry = JSON.parse(fs.readFileSync(ep, "utf8"));
357
+ const valid = verifyRegistryEntry(entry);
358
+ (valid ? ok : bad)(`registry entry signature ${valid ? "valid" : "INVALID"}`);
359
+ console.log(`${DIM}booted=${entry.result.booted} class=${entry.result.failureClass ?? "none"} commit=${entry.repo.commit?.slice(0, 8) ?? "?"}${RESET}`);
360
+ if (!valid)
361
+ process.exitCode = 1;
362
+ return;
363
+ }
364
+ bad(`unknown attest subcommand: ${sub ?? "(none)"} — use export or check`);
365
+ process.exitCode = 1;
366
+ return;
367
+ }
368
+ if (cmd === "explain") {
369
+ const p = positional[0] ? path.resolve(positional[0]) : attestationPath(target);
370
+ const att = JSON.parse(fs.readFileSync(p, "utf8"));
371
+ console.log(`${BOLD}Attestation explained${RESET}`);
372
+ console.log(att.result.booted ? `This run BOOTED: ${att.result.healthObservation}.` : `This run did NOT verify. Failure class: ${att.result.failureClass}.`);
373
+ console.log(`Trust level: ${att.trust?.level ?? "legacy_unspecified"}`);
374
+ if (!att.result.booted && att.result.failureClass) {
375
+ const diagnosis = diagnoseFailure(att.result.failureClass, att.result.failureEvidence, att.result.explanation);
376
+ console.log(`What happened: ${diagnosis.whatHappened}`);
377
+ console.log(`Why BootProof refused: ${diagnosis.whyRefused}`);
378
+ console.log(`Safe next step: ${diagnosis.safeNextStep}`);
379
+ console.log(`Evidence: ${p}`);
380
+ }
381
+ else {
382
+ console.log(att.result.explanation);
383
+ }
384
+ if (att.plan.healthCandidates?.length)
385
+ console.log(`Health candidates: ${att.plan.healthCandidates.join(", ")}`);
386
+ if (att.result.observedHealthCandidates?.length)
387
+ console.log(`Observed health candidates: ${att.result.observedHealthCandidates.join(", ")}`);
388
+ for (const o of att.observed)
389
+ console.log(` ${o.ok ? "\u2713" : "\u2717"} ${o.id}: ${o.observation}`);
390
+ return;
391
+ }
392
+ }
393
+ main().catch(err => {
394
+ const argv = process.argv.slice(2);
395
+ if (argv[0] === "up" && argv.includes("--json")) {
396
+ console.log(JSON.stringify(machineFailure(String(err?.message ?? err))));
397
+ }
398
+ else {
399
+ bad(String(err?.message ?? err));
400
+ }
401
+ process.exitCode = 1;
402
+ });
@@ -0,0 +1,7 @@
1
+ import type { FailureClass, Inference } from "./types.js";
2
+ export interface FailureDiagnosis {
3
+ whatHappened: string;
4
+ whyRefused: string;
5
+ safeNextStep: string;
6
+ }
7
+ export declare function diagnoseFailure(failureClass: FailureClass | null, evidence: string | null, explanation: string, inference?: Inference): FailureDiagnosis;
@@ -0,0 +1,139 @@
1
+ function packageManagerMismatch(evidence, inference) {
2
+ const expected = evidence?.match(/expected version:\s*([^\n]+)/i)?.[1]?.trim() ?? inference?.packageManagerVersion ?? "the declared version";
3
+ const actual = evidence?.match(/Got:\s*([^\n]+)/i)?.[1]?.trim() ?? "a different version";
4
+ const evidencedManager = evidence?.match(/engines\.(npm|pnpm|yarn|bun)/i)?.[1]?.toLowerCase();
5
+ const manager = inference?.packageManager && inference.packageManager !== "unknown"
6
+ ? inference.packageManager
7
+ : evidencedManager ?? "package manager";
8
+ const activation = manager === "pnpm"
9
+ ? `Run corepack enable && corepack prepare pnpm@${expected} --activate, then rerun BootProof.`
10
+ : `Install or activate ${manager} ${expected}, then rerun BootProof.`;
11
+ return {
12
+ whatHappened: `The repository requires ${manager} ${expected}, but this environment has ${manager} ${actual}.`,
13
+ whyRefused: "The dependency install cannot be trusted with the wrong package manager version.",
14
+ safeNextStep: activation,
15
+ };
16
+ }
17
+ export function diagnoseFailure(failureClass, evidence, explanation, inference) {
18
+ switch (failureClass) {
19
+ case "package_manager_version_mismatch":
20
+ return packageManagerMismatch(evidence, inference);
21
+ case "dependency_install_skipped":
22
+ return {
23
+ whatHappened: "The inferred application depends on project packages, but dependency installation was not requested.",
24
+ whyRefused: "Starting the application without its declared dependencies would not be a trustworthy boot attempt.",
25
+ safeNextStep: "Review the inferred install command, then rerun with --install if you want BootProof to execute it.",
26
+ };
27
+ case "python_flask_setup_required":
28
+ return {
29
+ whatHappened: "BootProof detected a Python/Flask application with migration, initialization, frontend, or worker setup steps.",
30
+ whyRefused: "BootProof cannot yet orchestrate that multi-step application safely enough to claim a verified boot.",
31
+ safeNextStep: "Review the detected setup and service commands, complete the repository's documented initialization, then rerun when orchestration support is available.",
32
+ };
33
+ case "workspace_ambiguous":
34
+ if (/multiple workspaces in parallel|starts multiple workspaces in parallel/i.test(explanation)) {
35
+ return {
36
+ whatHappened: "The root command starts multiple workspaces in parallel, so there is no single application verdict.",
37
+ whyRefused: "One responding workspace would not prove that the whole repository booted.",
38
+ safeNextStep: "Choose the intended application with --workspace <dir>, then rerun BootProof.",
39
+ };
40
+ }
41
+ return {
42
+ whatHappened: "More than one plausible application or health target was detected.",
43
+ whyRefused: "Choosing one automatically could verify the wrong workspace or mistake one responding service for the whole repository.",
44
+ safeNextStep: "Choose the intended application with --workspace <dir>, then rerun BootProof.",
45
+ };
46
+ case "service_port_allocated":
47
+ return {
48
+ whatHappened: "Docker reached the daemon, but a required service port could not be bound.",
49
+ whyRefused: "The planned service did not start, so the application boot could not be verified.",
50
+ safeNextStep: "Stop the process or container using the reported port, then rerun BootProof.",
51
+ };
52
+ case "health_http_error":
53
+ return {
54
+ whatHappened: "The application responded to a health candidate with HTTP 5xx.",
55
+ whyRefused: "A responding server is not a verified healthy boot when the observed response is a server error.",
56
+ safeNextStep: "Inspect the application logs and failing health route, fix the server error, then rerun BootProof.",
57
+ };
58
+ case "health_check_timeout":
59
+ return {
60
+ whatHappened: "No successful HTTP response was observed before the health timeout.",
61
+ whyRefused: "A running process alone is not proof that the application became reachable and healthy.",
62
+ safeNextStep: "Check the reported health candidates and application logs, then rerun with the correct port or a longer --timeout if justified.",
63
+ };
64
+ case "postgres_auth_env_missing":
65
+ return {
66
+ whatHappened: "Postgres was reached, but authentication or database environment configuration did not match.",
67
+ whyRefused: "The application could not establish the database connection required for a trustworthy boot.",
68
+ safeNextStep: "Check the repository's real database configuration and credentials. BootProof will not edit .env or invent a password.",
69
+ };
70
+ case "not_an_application":
71
+ return {
72
+ whatHappened: "No trustworthy runnable application entrypoint was found.",
73
+ whyRefused: "BootProof will not invent a command or advertise a localhost URL for a library or unrecognized repository.",
74
+ safeNextStep: "Point BootProof at a runnable workspace, or add an explicit documented start command to the repository.",
75
+ };
76
+ case "missing_package_manager":
77
+ return {
78
+ whatHappened: "The package manager required by the repository is not available.",
79
+ whyRefused: "BootProof cannot run the declared install or start command without that executable.",
80
+ safeNextStep: "Enable Corepack or install the repository's declared package manager, then rerun BootProof.",
81
+ };
82
+ case "runtime_engine_mismatch":
83
+ return {
84
+ whatHappened: "The available Node.js runtime does not satisfy the repository's declared engine requirement.",
85
+ whyRefused: "Continuing under an unsupported runtime would make install and boot evidence unreliable.",
86
+ safeNextStep: "Switch to a compatible Node.js version, then rerun BootProof.",
87
+ };
88
+ case "missing_env_var":
89
+ return {
90
+ whatHappened: "The application reported required environment configuration that is missing.",
91
+ whyRefused: "BootProof will not invent secrets or write protected .env files to force startup.",
92
+ safeNextStep: "Provide the real required values using the repository's documented configuration path, then rerun BootProof.",
93
+ };
94
+ case "port_in_use":
95
+ return {
96
+ whatHappened: "The application port is already in use.",
97
+ whyRefused: "BootProof could not observe the inferred application owning and serving that port.",
98
+ safeNextStep: "Stop the process using the port or rerun with an explicit --port value supported by the application.",
99
+ };
100
+ case "docker_unavailable":
101
+ return {
102
+ whatHappened: "The run plan requires Docker, but the Docker daemon or command is unavailable.",
103
+ whyRefused: "Required services could not be started, so BootProof could not verify the application.",
104
+ safeNextStep: "Start Docker and rerun, or explicitly choose local execution only when it is safe with --provider local --unsafe-local.",
105
+ };
106
+ case "install_failed":
107
+ return {
108
+ whatHappened: "The dependency install command exited unsuccessfully.",
109
+ whyRefused: "BootProof cannot trust an application boot when its declared dependency installation failed.",
110
+ safeNextStep: "Inspect the preserved install evidence, fix the underlying package or environment problem, then rerun BootProof.",
111
+ };
112
+ case "app_exited_early":
113
+ return {
114
+ whatHappened: "The application process exited before any health response was observed.",
115
+ whyRefused: "No live application health signal was available to verify.",
116
+ safeNextStep: "Inspect the preserved process output, fix the startup error, then rerun BootProof.",
117
+ };
118
+ default:
119
+ if (/cloned .* but will not execute remote repository code/i.test(explanation)) {
120
+ return {
121
+ whatHappened: explanation,
122
+ whyRefused: "A remote clone is untrusted code, and BootProof requires explicit acknowledgement before running it on the host.",
123
+ safeNextStep: "Review the cloned repository, then rerun with --provider local --unsafe-local only if you accept host execution.",
124
+ };
125
+ }
126
+ if (/Local provider runs repository code directly/i.test(explanation)) {
127
+ return {
128
+ whatHappened: explanation,
129
+ whyRefused: "Host execution was selected without the required explicit acknowledgement.",
130
+ safeNextStep: "Review the inferred commands, then rerun with --provider local --unsafe-local only if you accept host execution.",
131
+ };
132
+ }
133
+ return {
134
+ whatHappened: explanation,
135
+ whyRefused: "BootProof did not observe enough evidence to issue a verified boot result.",
136
+ safeNextStep: "Inspect the signed attestation and raw evidence, address the reported cause, then rerun BootProof.",
137
+ };
138
+ }
139
+ }
package/dist/exec.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ export interface ExecResult {
2
+ exitCode: number | null;
3
+ timedOut: boolean;
4
+ stdout: string;
5
+ stderr: string;
6
+ }
7
+ export declare function runToCompletion(command: string, cwd: string, timeoutMs: number, env: NodeJS.ProcessEnv): Promise<ExecResult>;
8
+ export interface SupervisedApp {
9
+ stop: () => Promise<void>;
10
+ exited: () => {
11
+ code: number | null;
12
+ early: boolean;
13
+ } | null;
14
+ output: () => string;
15
+ }
16
+ export declare function superviseApp(command: string, cwd: string, env: NodeJS.ProcessEnv): SupervisedApp;
17
+ export interface HealthObservation {
18
+ responded: boolean;
19
+ status: number | null;
20
+ attempts: number;
21
+ elapsedMs: number;
22
+ url: string | null;
23
+ candidates: string[];
24
+ discoveredCandidates: string[];
25
+ }
26
+ export declare function extractHealthCandidates(output: string): string[];
27
+ export declare function pollHealthCandidates(initialUrls: string[], timeoutMs: number, output?: () => string, intervalMs?: number): Promise<HealthObservation>;
28
+ export declare function pollHealth(url: string, timeoutMs: number, intervalMs?: number): Promise<HealthObservation>;
29
+ export declare function minimalEnv(extra?: Record<string, string>): NodeJS.ProcessEnv;