bootproof 0.1.0 → 0.4.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 +873 -109
- package/dist/agent-plan.d.ts +44 -0
- package/dist/agent-plan.js +826 -0
- package/dist/agent-run.d.ts +117 -0
- package/dist/agent-run.js +459 -0
- package/dist/ai-repair.d.ts +58 -0
- package/dist/ai-repair.js +380 -0
- package/dist/cli.js +936 -38
- package/dist/diagnosis.js +114 -17
- package/dist/diff.d.ts +29 -0
- package/dist/diff.js +569 -0
- package/dist/exec.d.ts +30 -2
- package/dist/exec.js +332 -37
- package/dist/external-health.d.ts +16 -0
- package/dist/external-health.js +214 -0
- package/dist/infer.js +489 -41
- package/dist/plan.d.ts +2 -0
- package/dist/plan.js +49 -7
- package/dist/proof.d.ts +78 -2
- package/dist/proof.js +266 -13
- package/dist/receipt.d.ts +52 -0
- package/dist/receipt.js +356 -0
- package/dist/redact.d.ts +4 -0
- package/dist/redact.js +86 -2
- package/dist/registry.d.ts +82 -30
- package/dist/registry.js +355 -53
- package/dist/remote.d.ts +12 -1
- package/dist/remote.js +62 -18
- package/dist/repair-playbooks.d.ts +24 -0
- package/dist/repair-playbooks.js +593 -0
- package/dist/repair-safety.d.ts +130 -0
- package/dist/repair-safety.js +766 -0
- package/dist/repair.d.ts +142 -0
- package/dist/repair.js +1566 -0
- package/dist/run.d.ts +6 -1
- package/dist/run.js +385 -46
- package/dist/sbom.d.ts +22 -0
- package/dist/sbom.js +99 -0
- package/dist/taxonomy.d.ts +8 -2
- package/dist/taxonomy.js +428 -8
- package/dist/types.d.ts +57 -2
- package/docs/AGENT_IN_THE_LOOP.md +171 -0
- package/docs/AGENT_RUN_RECEIPTS.md +38 -0
- package/docs/CI_ACTION.md +71 -5
- package/docs/DETERMINISTIC_REPAIR_SAFETY_MODEL.md +705 -0
- package/docs/FAILURE_TAXONOMY.md +30 -1
- package/docs/HONESTY_CONTRACT.md +55 -4
- package/docs/LAUNCH_PLAYBOOK.md +232 -0
- package/docs/REAL_REPO_EVIDENCE.md +77 -0
- package/docs/REAL_WORLD_FIXTURES.md +105 -0
- package/docs/REGISTRY.md +48 -28
- package/docs/RELEASE_CHECKLIST.md +9 -1
- package/docs/REPAIR_RECEIPT.md +224 -0
- package/docs/agent-loop-gap-analysis.md +188 -0
- package/docs/examples/registry-seeds/advertised-port-mismatch.json +28 -0
- package/docs/examples/registry-seeds/airbyte-abctl-external-orchestrator.json +36 -0
- package/docs/examples/registry-seeds/go-ollama-service.json +36 -0
- package/docs/examples/registry-seeds/laravel-vite-sqlite.json +36 -0
- package/docs/examples/registry-seeds/monorepo-ambiguous-health.json +29 -0
- package/docs/examples/registry-seeds/php-composer.json +33 -0
- package/docs/examples/registry-seeds/rails-bundler.json +32 -0
- package/docs/examples/registry-seeds/sentry-devenv-direnv.json +41 -0
- package/docs/schemas/action-verdict-v1.schema.json +64 -0
- package/docs/schemas/agent-plan-v1.schema.json +148 -0
- package/docs/schemas/agent-run-receipts-v1.schema.json +192 -0
- package/docs/schemas/ai-repair-suggestion-v1.schema.json +70 -0
- package/docs/schemas/ci-context-v1.schema.json +63 -0
- package/docs/schemas/diff-result-v1.schema.json +66 -0
- package/docs/schemas/federated-receipt-v1.schema.json +51 -0
- package/docs/schemas/registry-entry-v1.schema.json +95 -0
- package/docs/schemas/registry-seed-example-v1.schema.json +102 -0
- package/docs/schemas/repair-action-v1.schema.json +136 -0
- package/docs/schemas/repair-receipt-v1.schema.json +221 -0
- package/package.json +13 -6
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import * as readline from "node:readline/promises";
|
|
4
5
|
import { inferRepo } from "./infer.js";
|
|
5
6
|
import { buildPlan, composeFileFor, envExampleFor } from "./plan.js";
|
|
6
7
|
import { up } from "./run.js";
|
|
7
|
-
import {
|
|
8
|
+
import { attestationPath, currentGitHead, evaluateAttestationSignature, trustSigner, rotateSigner, writeAttestation, TOOL_ID, } from "./proof.js";
|
|
9
|
+
import { emitLivingReceipt } from "./receipt.js";
|
|
8
10
|
import { pollHealth } from "./exec.js";
|
|
9
|
-
import {
|
|
11
|
+
import { buildExternalHealthAttestation } from "./external-health.js";
|
|
12
|
+
import { agentPlanPath, buildAgentPlan, writeAgentPlan, } from "./agent-plan.js";
|
|
13
|
+
import { agentRunDirectory, appendAgentVerification, createAgentRun, explainAgentRun, latestAgentRunId, readAgentRun, } from "./agent-run.js";
|
|
14
|
+
import { buildFederatedReceipt, buildRegistryEntry, currentGitBranch, evaluateRegistryEntrySignature, writeFederatedReceipt, writeRegistryEntry, registryEntryPath, } from "./registry.js";
|
|
10
15
|
import { normalizeDockerBindPath, detectHostPlatform } from "./platform.js";
|
|
11
16
|
import { diagnoseFailure } from "./diagnosis.js";
|
|
12
|
-
import {
|
|
17
|
+
import { cloneRemoteTarget, isRemoteTarget, managedRemoteSource } from "./remote.js";
|
|
18
|
+
import { applyVerifiedRepair, executeAiSuggestedRepair, evaluateRepairReceiptSignature, latestDeterministicRepairCandidate, latestFailedAttestation, repairRepo, registeredRemediationsFor, verifyRepairReceipt, } from "./repair.js";
|
|
19
|
+
import { requestAiRepairSuggestion, resolveAiProvider, } from "./ai-repair.js";
|
|
20
|
+
import { exportSbom } from "./sbom.js";
|
|
21
|
+
import { diffRefs } from "./diff.js";
|
|
13
22
|
let GREEN = "\x1b[32m", YELLOW = "\x1b[33m", RED = "\x1b[31m", DIM = "\x1b[2m", BOLD = "\x1b[1m", RESET = "\x1b[0m";
|
|
14
23
|
const ok = (s) => console.log(`${GREEN}\u2713 ${s}${RESET}`);
|
|
15
24
|
const would = (s) => console.log(`${DIM}\u25cb would: ${s}${RESET}`);
|
|
16
25
|
const warn = (s) => console.log(`${YELLOW}! ${s}${RESET}`);
|
|
17
26
|
const bad = (s) => console.log(`${RED}\u2717 ${s}${RESET}`);
|
|
18
27
|
const disableColor = () => { GREEN = ""; YELLOW = ""; RED = ""; DIM = ""; BOLD = ""; RESET = ""; };
|
|
19
|
-
const
|
|
28
|
+
const portableRelative = (from, to) => path.relative(from, to).replace(/\\/g, "/");
|
|
29
|
+
const COMMANDS = ["up", "verify-url", "plan-agent", "explain-run", "fix", "apply-repair", "diff", "analyze", "plan", "verify", "explain", "attest", "registry", "export-sbom", "rotate-keys", "help", "version", "--help", "-h", "--version"];
|
|
30
|
+
const SUPPORTED_FLAGS = {
|
|
31
|
+
diff: new Set(["base", "head", "json", "ci"]),
|
|
32
|
+
analyze: new Set(["workspace", "json", "ci"]),
|
|
33
|
+
plan: new Set(["workspace", "provider", "ci"]),
|
|
34
|
+
"plan-agent": new Set(["json", "ci"]),
|
|
35
|
+
"explain-run": new Set(["ci"]),
|
|
36
|
+
"apply-repair": new Set(["receipt", "dry-run", "json", "ci"]),
|
|
37
|
+
fix: new Set(["provider", "unsafe-local", "port", "timeout", "dry-run", "json", "ci", "ai"]),
|
|
38
|
+
up: new Set(["provider", "unsafe-local", "install", "workspace", "port", "timeout", "dry-run", "json", "ci", "command", "external-health", "receipt", "health-path", "ci-oidc"]),
|
|
39
|
+
"verify-url": new Set(["timeout", "json", "ci"]),
|
|
40
|
+
verify: new Set(["ci", "trust-signer", "require-known-signer", "strict"]),
|
|
41
|
+
attest: new Set(["ci", "trust-signer", "require-known-signer", "strict"]),
|
|
42
|
+
registry: new Set(["mode", "federated", "ci"]),
|
|
43
|
+
"export-sbom": new Set(["format", "json", "ci"]),
|
|
44
|
+
"rotate-keys": new Set(["repo", "resign", "no-backup", "json", "ci"]),
|
|
45
|
+
};
|
|
20
46
|
void normalizeDockerBindPath;
|
|
21
47
|
void detectHostPlatform; // exported surface, used by docker provider work in progress
|
|
22
48
|
if (process.env.NO_COLOR !== undefined)
|
|
@@ -45,26 +71,68 @@ function help() {
|
|
|
45
71
|
console.log(`${BOLD}bootproof${RESET} — Human diagnosis. Machine proof. One engine.
|
|
46
72
|
|
|
47
73
|
Usage:
|
|
48
|
-
bootproof analyze <path|
|
|
74
|
+
bootproof analyze <path|git-url> [--workspace dir] [--json]
|
|
49
75
|
inspect a repo, show evidence-based inference
|
|
50
|
-
bootproof plan <path|
|
|
51
|
-
bootproof
|
|
52
|
-
bootproof
|
|
53
|
-
bootproof
|
|
54
|
-
bootproof
|
|
55
|
-
bootproof
|
|
76
|
+
bootproof plan <path|git-url> [--workspace dir] show the run plan and files that WOULD be generated
|
|
77
|
+
bootproof plan-agent <path|git-url> [--json] write a risk-classified plan; execute nothing
|
|
78
|
+
bootproof explain-run <run-id> verify and explain a local agent receipt chain
|
|
79
|
+
bootproof up <path|git-url> [options] execute the plan, verify localhost, write signed proof
|
|
80
|
+
bootproof verify-url <url> [--timeout ms] verify an externally managed HTTP service
|
|
81
|
+
bootproof fix <path|git-url> [options] test a deterministic repair in a sandbox
|
|
82
|
+
bootproof fix <path|git-url> --ai optionally request a BYOK AI suggestion after deterministic refusal
|
|
83
|
+
bootproof apply-repair <path> [--receipt proof.json] explicitly apply a signature-valid verified file change
|
|
84
|
+
bootproof diff [--base ref] [--head ref] [--json] statically detect infrastructure drift between Git refs
|
|
85
|
+
bootproof verify <path|proof.json> validate an attestation or repair-receipt signature
|
|
86
|
+
bootproof explain <proof.json> explain an attestation or repair receipt
|
|
87
|
+
bootproof registry export <path> explicitly write a redacted local registry export
|
|
88
|
+
bootproof registry export <path> --federated explicitly write a public-candidate receipt
|
|
89
|
+
bootproof attest export <path> compatibility alias for local registry export
|
|
90
|
+
bootproof attest check <path> verify a registry entry signature
|
|
91
|
+
bootproof export-sbom <path> [--format cyclonedx-json] export a CycloneDX SBOM from package-lock.json
|
|
92
|
+
bootproof rotate-keys [--repo <path>] [--resign] rotate the local ed25519 signing key (back up old, generate new)
|
|
56
93
|
bootproof version
|
|
57
94
|
|
|
58
95
|
Options for up:
|
|
59
96
|
--provider docker|local execution provider (default docker)
|
|
60
|
-
|
|
97
|
+
docker: only works for source-built Compose applications; refuses host commands for plain repos
|
|
98
|
+
local: runs install/start commands directly on your host machine (requires --unsafe-local)
|
|
99
|
+
--unsafe-local required consent for --provider local; confirms you accept host execution
|
|
61
100
|
--install run the dependency install step (off by default)
|
|
62
101
|
--workspace <dir> pick a monorepo workspace
|
|
102
|
+
--command <command> override the inferred application start command
|
|
103
|
+
--external-health <url> verify an externally managed service; do not start the app
|
|
63
104
|
--port <n> override inferred port
|
|
105
|
+
--health-path <path> override inferred health endpoint path (e.g. /health, /healthz)
|
|
64
106
|
--timeout <ms> health verification timeout (default 60000)
|
|
65
107
|
--dry-run show what would happen; executes nothing, writes nothing
|
|
66
108
|
--json one bootproof/result/v1 JSON object on stdout
|
|
67
109
|
--ci no prompts, colours, or interactive UI; fail closed
|
|
110
|
+
--receipt write a self-verifying Living Receipt HTML to .bootproof/living-receipt.html
|
|
111
|
+
--ci-oidc fetch GitHub Actions OIDC token and sign at ci_oidc_signed trust level (requires permissions: id-token: write)
|
|
112
|
+
|
|
113
|
+
Options for fix:
|
|
114
|
+
--provider docker|local execution provider (default docker; docker only works for Compose apps)
|
|
115
|
+
--unsafe-local required consent for --provider local; confirms you accept host execution
|
|
116
|
+
--port <n> override inferred application port
|
|
117
|
+
--timeout <ms> before/after health timeout (default 60000)
|
|
118
|
+
--ai optional BYOK suggestion after no deterministic repair is known
|
|
119
|
+
--dry-run execute nothing, write nothing, produce no repair proof
|
|
120
|
+
--json one bootproof/repair-result/v1 object on stdout
|
|
121
|
+
|
|
122
|
+
Command repairs show the exact command and require the literal response Y before execution.
|
|
123
|
+
JSON and CI modes never prompt and never approve a command.
|
|
124
|
+
AI uses OPENAI_API_KEY or ANTHROPIC_API_KEY directly with native fetch. It sends only
|
|
125
|
+
redacted structured failure evidence after explicit consent and never runs inside bootproof up.
|
|
126
|
+
|
|
127
|
+
Options for diff:
|
|
128
|
+
--base <ref> base Git commit (default HEAD^)
|
|
129
|
+
--head <ref> head Git commit (default HEAD)
|
|
130
|
+
--json one bootproof/diff-result/v1 JSON object on stdout
|
|
131
|
+
|
|
132
|
+
Options for verify and attest check:
|
|
133
|
+
--trust-signer explicitly pin an intact foreign signer in ~/.bootproof/known_signers.json
|
|
134
|
+
--require-known-signer fail when the signer is not this machine or explicitly pinned
|
|
135
|
+
--strict require a known signer and fail directory verification on commit mismatch
|
|
68
136
|
|
|
69
137
|
Honesty contract: no green check without an observed event; dry runs say "would";
|
|
70
138
|
.env/.env.local are never written; secrets are never invented.
|
|
@@ -81,6 +149,11 @@ function printInference(inf) {
|
|
|
81
149
|
console.log(` frontend markers: ${inf.frontendMarkers.join(", ")}`);
|
|
82
150
|
if (inf.serviceMarkers.length)
|
|
83
151
|
console.log(` service markers: ${inf.serviceMarkers.join(", ")}`);
|
|
152
|
+
if (inf.repoComposeFile)
|
|
153
|
+
console.log(` repo compose: ${inf.repoComposeFile} (bootproof defers to it)`);
|
|
154
|
+
if (inf.composeApplicationServices.length) {
|
|
155
|
+
console.log(` compose HTTP services: ${inf.composeApplicationServices.map(service => `${service.name} (${service.source === "build" ? "builds checked-out source" : "image only"})`).join("; ")}`);
|
|
156
|
+
}
|
|
84
157
|
console.log(` package manager: ${inf.packageManager} ${DIM}(${inf.packageManagerEvidence})${RESET}`);
|
|
85
158
|
if (inf.setupSteps.length)
|
|
86
159
|
console.log(` setup steps: ${inf.setupSteps.join("; ")}`);
|
|
@@ -88,12 +161,19 @@ function printInference(inf) {
|
|
|
88
161
|
console.log(` backend command: ${inf.backendCommand}`);
|
|
89
162
|
if (inf.frontendCommand)
|
|
90
163
|
console.log(` frontend command: ${inf.frontendCommand}`);
|
|
164
|
+
if (inf.asset_dev_server_command)
|
|
165
|
+
console.log(` asset dev server command: ${inf.asset_dev_server_command}`);
|
|
91
166
|
if (inf.workerCommand)
|
|
92
167
|
console.log(` worker command: ${inf.workerCommand}`);
|
|
93
168
|
if (inf.appCommand)
|
|
94
169
|
console.log(` selected command: ${inf.appCommand} ${DIM}(${inf.appCommandSource})${RESET}`);
|
|
170
|
+
if (inf.preparationCommands.length)
|
|
171
|
+
console.log(` preparation: ${inf.preparationCommands.map(command => command.command).join("; ")}`);
|
|
95
172
|
console.log(` command scope: ${inf.commandScope}`);
|
|
96
173
|
console.log(` port: ${inf.port} ${DIM}(${inf.portEvidence})${RESET}`);
|
|
174
|
+
if (inf.observedPort !== null)
|
|
175
|
+
console.log(` observed port: ${inf.observedPort}`);
|
|
176
|
+
console.log(` health candidate source: ${inf.healthCandidateSource}`);
|
|
97
177
|
if (inf.healthCandidates.length)
|
|
98
178
|
console.log(` health candidates: ${inf.healthCandidates.join(", ")}`);
|
|
99
179
|
if (inf.services.length)
|
|
@@ -138,6 +218,84 @@ function machineFailure(explanation) {
|
|
|
138
218
|
writtenFiles: [],
|
|
139
219
|
};
|
|
140
220
|
}
|
|
221
|
+
function externalMachineResult(attestation, evidencePath) {
|
|
222
|
+
return {
|
|
223
|
+
schema: "bootproof/result/v1",
|
|
224
|
+
booted: false,
|
|
225
|
+
healthVerified: attestation.result.healthVerified,
|
|
226
|
+
failureClass: attestation.result.failureClass,
|
|
227
|
+
classification: attestation.classification,
|
|
228
|
+
verificationMode: attestation.verificationMode,
|
|
229
|
+
bootproofOrchestrated: attestation.bootproofOrchestrated,
|
|
230
|
+
externalHealthUrl: attestation.externalHealthUrl,
|
|
231
|
+
observedStatus: attestation.observedStatus,
|
|
232
|
+
observedFinalUrl: attestation.observedFinalUrl,
|
|
233
|
+
observedAt: attestation.observedAt,
|
|
234
|
+
responseSnippet: attestation.responseSnippet,
|
|
235
|
+
attestationPath: evidencePath,
|
|
236
|
+
plan: attestation.plan,
|
|
237
|
+
observed: attestation.observed,
|
|
238
|
+
explanation: attestation.result.explanation,
|
|
239
|
+
trust: attestation.trust,
|
|
240
|
+
writtenFiles: evidencePath ? [evidencePath] : [],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function printExternalHealthResult(attestation, evidencePath) {
|
|
244
|
+
const result = attestation.result;
|
|
245
|
+
if (result.healthVerified) {
|
|
246
|
+
ok(`${BOLD}EXTERNAL SERVICE VERIFIED${RESET}${GREEN} — ${result.healthObservation} (observed, signed)`);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
bad(`${BOLD}NOT VERIFIED${RESET}${RED} — ${attestation.classification}`);
|
|
250
|
+
if (attestation.observedStatus !== null) {
|
|
251
|
+
console.log(`Observed: HTTP ${attestation.observedStatus} at ${attestation.observedFinalUrl}`);
|
|
252
|
+
}
|
|
253
|
+
const connectionError = result.healthEvidence?.connectionError;
|
|
254
|
+
if (connectionError)
|
|
255
|
+
console.log(`Connection error: ${connectionError}`);
|
|
256
|
+
}
|
|
257
|
+
console.log("Ownership: externally managed (bootproofOrchestrated=false).");
|
|
258
|
+
if (evidencePath)
|
|
259
|
+
console.log(`Evidence: ${evidencePath}`);
|
|
260
|
+
}
|
|
261
|
+
function printAgentPlan(plan, outputPath, runId, runPath) {
|
|
262
|
+
console.log(`${BOLD}Agent plan (planning only)${RESET}`);
|
|
263
|
+
if (plan.classifications.length)
|
|
264
|
+
console.log(`Classifications: ${plan.classifications.join(", ")}`);
|
|
265
|
+
console.log(`Current failure class: ${plan.currentFailureClass || "none established"}`);
|
|
266
|
+
console.log(`Suspected stack: ${plan.suspectedStack.length ? plan.suspectedStack.join(", ") : "none established"}`);
|
|
267
|
+
console.log(`BootProof direct orchestration: ${plan.canBootProofOrchestrateDirectly ? "available" : "not established"}`);
|
|
268
|
+
console.log(`External health verification: ${plan.canBootProofVerifyExternally ? "available" : "not established"}`);
|
|
269
|
+
if (plan.missingTools.length)
|
|
270
|
+
console.log(`Missing tools: ${plan.missingTools.join(", ")}`);
|
|
271
|
+
if (plan.observedEvidence.length) {
|
|
272
|
+
console.log("Observed evidence:");
|
|
273
|
+
for (const evidence of plan.observedEvidence)
|
|
274
|
+
console.log(` - ${evidence}`);
|
|
275
|
+
}
|
|
276
|
+
if (plan.candidateNextActions.length) {
|
|
277
|
+
console.log("Candidate next actions:");
|
|
278
|
+
for (const [index, candidate] of plan.candidateNextActions.entries()) {
|
|
279
|
+
console.log(` ${index + 1}. ${candidate.classification}`);
|
|
280
|
+
if (candidate.command)
|
|
281
|
+
console.log(` Command: ${candidate.command}`);
|
|
282
|
+
console.log(` Reason: ${candidate.reason}`);
|
|
283
|
+
console.log(` Risk: ${candidate.riskLevel}`);
|
|
284
|
+
console.log(` Mutation scope: ${candidate.mutationScope}`);
|
|
285
|
+
console.log(` Approval required: ${candidate.requiresApproval ? "yes" : "no"}`);
|
|
286
|
+
console.log(` Approval: ${candidate.approvalPrompt}`);
|
|
287
|
+
if (candidate.blockedReason)
|
|
288
|
+
console.log(` Blocked: ${candidate.blockedReason}`);
|
|
289
|
+
if (candidate.secretSensitive)
|
|
290
|
+
console.log(" Secret-sensitive: yes; command output must not be saved");
|
|
291
|
+
console.log(` Verify after action: ${candidate.verificationStep}`);
|
|
292
|
+
console.log(` Stop condition: ${candidate.stopCondition}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
console.log(`Plan: ${outputPath}`);
|
|
296
|
+
console.log(`Agent run: ${runId} (${runPath})`);
|
|
297
|
+
console.log("No candidate action was executed. Verification remains pending.");
|
|
298
|
+
}
|
|
141
299
|
function printFailure(failureClass, diagnosis, evidencePath) {
|
|
142
300
|
bad(`${BOLD}NOT VERIFIED${RESET}${RED} — ${failureClass}`);
|
|
143
301
|
console.log(`What happened: ${diagnosis.whatHappened}`);
|
|
@@ -145,6 +303,216 @@ function printFailure(failureClass, diagnosis, evidencePath) {
|
|
|
145
303
|
console.log(`Safe next step: ${diagnosis.safeNextStep}`);
|
|
146
304
|
console.log(`Evidence: ${evidencePath}`);
|
|
147
305
|
}
|
|
306
|
+
function isRepairReceipt(value) {
|
|
307
|
+
return Boolean(value && typeof value === "object" && value.schema === "bootproof/repair-receipt/v1");
|
|
308
|
+
}
|
|
309
|
+
function signatureTrustText(result) {
|
|
310
|
+
if (!result.integrityValid || result.tier === "invalid")
|
|
311
|
+
return "signature INVALID";
|
|
312
|
+
if (result.tier === "self")
|
|
313
|
+
return "signature intact, signer: this machine";
|
|
314
|
+
if (result.tier === "known") {
|
|
315
|
+
return `signature intact, signer: known (${result.label ?? result.fingerprint})`;
|
|
316
|
+
}
|
|
317
|
+
return "signature intact, signer: UNKNOWN — integrity only, not a trusted signer";
|
|
318
|
+
}
|
|
319
|
+
function printSignatureTrust(result) {
|
|
320
|
+
const message = signatureTrustText(result);
|
|
321
|
+
if (!result.integrityValid)
|
|
322
|
+
bad(message);
|
|
323
|
+
else if (result.tier === "unknown-foreign")
|
|
324
|
+
warn(message);
|
|
325
|
+
else
|
|
326
|
+
ok(message);
|
|
327
|
+
}
|
|
328
|
+
function maybeTrustSigner(result, publicKey, requested, reevaluate) {
|
|
329
|
+
if (!requested || !result.integrityValid || result.tier !== "unknown-foreign" || !publicKey)
|
|
330
|
+
return result;
|
|
331
|
+
console.log(`Trusting signer: ${result.fingerprint}`);
|
|
332
|
+
trustSigner(publicKey);
|
|
333
|
+
return reevaluate();
|
|
334
|
+
}
|
|
335
|
+
function signerTrustFails(result, requireKnown) {
|
|
336
|
+
return !result.integrityValid || (requireKnown && result.tier === "unknown-foreign");
|
|
337
|
+
}
|
|
338
|
+
function commitMismatchMessage(attestation, repo) {
|
|
339
|
+
const head = currentGitHead(repo);
|
|
340
|
+
if (!head || attestation.repo.commit === head)
|
|
341
|
+
return null;
|
|
342
|
+
const attested = attestation.repo.commit?.slice(0, 8) ?? "none";
|
|
343
|
+
return `This attestation does not describe the repository's current commit (${attested} vs ${head.slice(0, 8)}); it is not proof of the current working tree.`;
|
|
344
|
+
}
|
|
345
|
+
function optionalRepairReceipt(repo) {
|
|
346
|
+
const receipt = path.join(repo, ".bootproof", "repair-receipt.json");
|
|
347
|
+
if (!fs.existsSync(receipt))
|
|
348
|
+
return null;
|
|
349
|
+
try {
|
|
350
|
+
const value = JSON.parse(fs.readFileSync(receipt, "utf8"));
|
|
351
|
+
return isRepairReceipt(value) && verifyRepairReceipt(value) ? value : null;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function registryEntryFor(repo, registryMode) {
|
|
358
|
+
const ap = attestationPath(repo);
|
|
359
|
+
if (!fs.existsSync(ap))
|
|
360
|
+
return null;
|
|
361
|
+
const att = JSON.parse(fs.readFileSync(ap, "utf8"));
|
|
362
|
+
return buildRegistryEntry(att, {
|
|
363
|
+
registryMode,
|
|
364
|
+
inference: inferRepo(repo),
|
|
365
|
+
repairReceipt: optionalRepairReceipt(repo),
|
|
366
|
+
branch: currentGitBranch(repo),
|
|
367
|
+
sign: true,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function printRepairResult(result) {
|
|
371
|
+
if (result.repaired) {
|
|
372
|
+
ok(`${BOLD}VERIFIED REPAIR${RESET}${GREEN} — ${result.repairId}`);
|
|
373
|
+
console.log(result.explanation);
|
|
374
|
+
if (result.patchPath)
|
|
375
|
+
console.log(`Patch: ${result.patchPath}`);
|
|
376
|
+
console.log(`Receipt: ${result.receiptPath}`);
|
|
377
|
+
console.log(`After attestation: ${result.afterAttestationPath}`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
bad(`${BOLD}NO VERIFIED REPAIR${RESET}${RED}${result.failureClass ? ` — ${result.failureClass}` : ""}`);
|
|
381
|
+
console.log(result.explanation);
|
|
382
|
+
if (result.receiptPath)
|
|
383
|
+
console.log(`Receipt: ${result.receiptPath}`);
|
|
384
|
+
}
|
|
385
|
+
async function commandRepairApproval(action) {
|
|
386
|
+
console.log(action.approvalPrompt);
|
|
387
|
+
console.log(`Command: ${action.command.display}`);
|
|
388
|
+
console.log(`Mutation scope: ${action.mutationScope}`);
|
|
389
|
+
console.log(`Risk: ${action.riskLevel}`);
|
|
390
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
391
|
+
try {
|
|
392
|
+
return await prompt.question("Run this command? Type Y to approve: ") === "Y";
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
prompt.close();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function patchRepairApproval(action) {
|
|
399
|
+
console.log(action.approvalPrompt);
|
|
400
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
401
|
+
try {
|
|
402
|
+
return await prompt.question("Test this patch in the repair sandbox? Type Y to approve: ") === "Y";
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
prompt.close();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function printAiRepairCandidate(requested) {
|
|
409
|
+
const action = requested.action;
|
|
410
|
+
console.log(`${BOLD}AI-suggested unverified repair${RESET}`);
|
|
411
|
+
console.log(`Provider: ${requested.provider} (${requested.model})`);
|
|
412
|
+
console.log(`Failure: ${requested.suggestion.failure_class}`);
|
|
413
|
+
console.log(`Confidence reported by provider: ${requested.suggestion.confidence}`);
|
|
414
|
+
console.log(`Explanation: ${requested.suggestion.explanation_for_user}`);
|
|
415
|
+
console.log(`Provider safety rationale (unverified): ${requested.suggestion.why_this_is_safe}`);
|
|
416
|
+
if (action.command)
|
|
417
|
+
console.log(`Command: ${action.command.display}`);
|
|
418
|
+
if (action.instruction)
|
|
419
|
+
console.log(`Instruction: ${action.instruction}`);
|
|
420
|
+
if (action.patch)
|
|
421
|
+
console.log(`Patch preview:\n${action.patch.content}`);
|
|
422
|
+
console.log(`Mutation scope: ${action.mutationScope}`);
|
|
423
|
+
console.log(`Risk: ${action.riskLevel}`);
|
|
424
|
+
console.log(`Approval: ${action.approvalPrompt}`);
|
|
425
|
+
console.log(`Verify after action: ${action.verificationStep}`);
|
|
426
|
+
console.log("Source: ai_suggested_unverified. AI has not proved this repair.");
|
|
427
|
+
}
|
|
428
|
+
async function aiRepairApproval(action, prompt) {
|
|
429
|
+
if (action.actionType === "command") {
|
|
430
|
+
return await prompt.question("Run this AI-suggested command? Type Y to approve: ") === "Y";
|
|
431
|
+
}
|
|
432
|
+
if (action.actionType === "patch") {
|
|
433
|
+
return await prompt.question("Test this AI-suggested patch in the repair sandbox? Type Y to approve: ") === "Y";
|
|
434
|
+
}
|
|
435
|
+
return await prompt.question("Approve recording this AI-suggested instruction? Type Y to approve: ") === "Y";
|
|
436
|
+
}
|
|
437
|
+
function printRepairCandidate(candidate) {
|
|
438
|
+
if (!candidate)
|
|
439
|
+
return;
|
|
440
|
+
const action = candidate.candidate.action;
|
|
441
|
+
console.log(`${BOLD}Deterministic repair candidate${RESET}`);
|
|
442
|
+
console.log(`Failure: ${candidate.candidate.failureClass}`);
|
|
443
|
+
if (action.command)
|
|
444
|
+
console.log(`Command: ${action.command.display}`);
|
|
445
|
+
if (action.instruction)
|
|
446
|
+
console.log(`Instruction: ${action.instruction}`);
|
|
447
|
+
if (action.patch)
|
|
448
|
+
console.log(`Patch preview:\n${action.patch.content}`);
|
|
449
|
+
console.log(`Mutation scope: ${action.mutationScope}`);
|
|
450
|
+
console.log(`Risk: ${action.riskLevel}`);
|
|
451
|
+
console.log(`Approval: ${action.approvalPrompt}`);
|
|
452
|
+
console.log(`Verify after action: ${action.verificationStep}`);
|
|
453
|
+
for (const followUp of candidate.candidate.followUpActions ?? []) {
|
|
454
|
+
if (followUp.command)
|
|
455
|
+
console.log(`Later separately approved command: ${followUp.command.display}`);
|
|
456
|
+
if (followUp.instruction)
|
|
457
|
+
console.log(`Follow-up instruction: ${followUp.instruction}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function printRepairApplyResult(result) {
|
|
461
|
+
if (result.applied) {
|
|
462
|
+
ok(`${BOLD}APPLIED VERIFIED REPAIR${RESET}`);
|
|
463
|
+
console.log(result.explanation);
|
|
464
|
+
console.log(`Receipt: ${result.receiptPath}`);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
bad(`${BOLD}REPAIR NOT APPLIED${RESET}`);
|
|
468
|
+
console.log(result.explanation);
|
|
469
|
+
}
|
|
470
|
+
function printDiffResult(result) {
|
|
471
|
+
console.log(`${BOLD}Static infrastructure diff${RESET}`);
|
|
472
|
+
console.log(`Base: ${result.base}`);
|
|
473
|
+
console.log(`Head: ${result.head}`);
|
|
474
|
+
console.log(`Risk: ${result.riskLevel}`);
|
|
475
|
+
console.log(`Fresh boot proof required: ${result.proofRequired ? "yes" : "no detected infrastructure drift"}`);
|
|
476
|
+
const sections = [
|
|
477
|
+
["Changed files", result.changedFiles],
|
|
478
|
+
["Added services", result.addedServices],
|
|
479
|
+
["Removed services", result.removedServices],
|
|
480
|
+
["Added ports", result.addedPorts],
|
|
481
|
+
["Removed ports", result.removedPorts],
|
|
482
|
+
["Added env vars", result.addedEnvVars],
|
|
483
|
+
["Removed env vars", result.removedEnvVars],
|
|
484
|
+
[
|
|
485
|
+
"Changed commands",
|
|
486
|
+
result.changedCommands.map(change => `${change.source}: ${change.before ?? "(absent)"} -> ${change.after ?? "(absent)"}`),
|
|
487
|
+
],
|
|
488
|
+
[
|
|
489
|
+
"Changed package managers",
|
|
490
|
+
result.changedPackageManagers.map(change => `${change.source}: ${change.before ?? "(absent)"} -> ${change.after ?? "(absent)"}`),
|
|
491
|
+
],
|
|
492
|
+
];
|
|
493
|
+
for (const [label, values] of sections) {
|
|
494
|
+
if (!values.length)
|
|
495
|
+
continue;
|
|
496
|
+
console.log(`${label}:`);
|
|
497
|
+
for (const value of values)
|
|
498
|
+
console.log(` - ${value}`);
|
|
499
|
+
}
|
|
500
|
+
console.log("Review notes:");
|
|
501
|
+
for (const note of result.suggestedReviewNotes)
|
|
502
|
+
console.log(` - ${note}`);
|
|
503
|
+
console.log("Static analysis only. No repository code was executed and no data was uploaded.");
|
|
504
|
+
}
|
|
505
|
+
function rebaseRemoteRepairPaths(result, repo) {
|
|
506
|
+
const rebase = (value) => value
|
|
507
|
+
? portableRelative(process.cwd(), path.join(repo, value))
|
|
508
|
+
: null;
|
|
509
|
+
return {
|
|
510
|
+
...result,
|
|
511
|
+
receiptPath: rebase(result.receiptPath),
|
|
512
|
+
patchPath: rebase(result.patchPath),
|
|
513
|
+
afterAttestationPath: rebase(result.afterAttestationPath),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
148
516
|
async function main() {
|
|
149
517
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
150
518
|
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h")
|
|
@@ -160,11 +528,123 @@ async function main() {
|
|
|
160
528
|
const { flags, positional } = parseFlags(rest);
|
|
161
529
|
if (flags.ci || flags.json)
|
|
162
530
|
disableColor();
|
|
531
|
+
const unsupportedFlag = Object.keys(flags).find(flag => !SUPPORTED_FLAGS[cmd]?.has(flag));
|
|
532
|
+
if (unsupportedFlag) {
|
|
533
|
+
const explanation = `unsupported flag for ${cmd}: --${unsupportedFlag}`;
|
|
534
|
+
if (cmd === "up" && flags.json)
|
|
535
|
+
console.log(JSON.stringify(machineFailure(explanation)));
|
|
536
|
+
else
|
|
537
|
+
bad(explanation);
|
|
538
|
+
process.exitCode = 1;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
163
541
|
const targetInput = String(positional[0] ?? ".");
|
|
542
|
+
if (cmd === "diff") {
|
|
543
|
+
const optionError = positional.length
|
|
544
|
+
? "diff does not accept a path; run it from the Git repository to compare"
|
|
545
|
+
: flags.base !== undefined && typeof flags.base !== "string"
|
|
546
|
+
? "--base requires a Git ref"
|
|
547
|
+
: flags.head !== undefined && typeof flags.head !== "string"
|
|
548
|
+
? "--head requires a Git ref"
|
|
549
|
+
: null;
|
|
550
|
+
if (optionError) {
|
|
551
|
+
bad(optionError);
|
|
552
|
+
process.exitCode = 1;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const result = diffRefs(process.cwd(), {
|
|
556
|
+
base: flags.base,
|
|
557
|
+
head: flags.head,
|
|
558
|
+
});
|
|
559
|
+
if (flags.json)
|
|
560
|
+
console.log(JSON.stringify(result));
|
|
561
|
+
else
|
|
562
|
+
printDiffResult(result);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (cmd === "explain-run") {
|
|
566
|
+
if (!positional[0] || positional.length > 1) {
|
|
567
|
+
bad("explain-run requires exactly one run id");
|
|
568
|
+
process.exitCode = 1;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const run = readAgentRun(process.cwd(), positional[0]);
|
|
572
|
+
for (const line of explainAgentRun(process.cwd(), positional[0]))
|
|
573
|
+
console.log(line);
|
|
574
|
+
process.exitCode = run.chainValid ? 0 : 1;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (cmd === "verify-url") {
|
|
578
|
+
if (!positional[0] || positional.length > 1) {
|
|
579
|
+
const explanation = "verify-url requires exactly one HTTP or HTTPS URL";
|
|
580
|
+
if (flags.json)
|
|
581
|
+
console.log(JSON.stringify(machineFailure(explanation)));
|
|
582
|
+
else
|
|
583
|
+
bad(explanation);
|
|
584
|
+
process.exitCode = 1;
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const timeoutMs = Number(flags.timeout ?? 5000);
|
|
588
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
589
|
+
const explanation = `invalid --timeout value: ${String(flags.timeout)} (expected a positive number)`;
|
|
590
|
+
if (flags.json)
|
|
591
|
+
console.log(JSON.stringify(machineFailure(explanation)));
|
|
592
|
+
else
|
|
593
|
+
bad(explanation);
|
|
594
|
+
process.exitCode = 1;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const attestation = await buildExternalHealthAttestation(process.cwd(), targetInput, timeoutMs);
|
|
598
|
+
if (flags.json)
|
|
599
|
+
console.log(JSON.stringify(attestation));
|
|
600
|
+
else
|
|
601
|
+
printExternalHealthResult(attestation, null);
|
|
602
|
+
process.exitCode = attestation.result.healthVerified ? 0 : 1;
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (cmd === "up" && flags["external-health"] !== undefined) {
|
|
606
|
+
const externalHealthUrl = flags["external-health"];
|
|
607
|
+
const incompatibleFlag = ["provider", "unsafe-local", "install", "workspace", "port", "dry-run", "command"]
|
|
608
|
+
.find(flag => flags[flag] !== undefined);
|
|
609
|
+
const timeoutMs = Number(flags.timeout ?? 5000);
|
|
610
|
+
const optionError = typeof externalHealthUrl !== "string" || externalHealthUrl.trim().length === 0
|
|
611
|
+
? "--external-health requires an HTTP or HTTPS URL"
|
|
612
|
+
: incompatibleFlag
|
|
613
|
+
? `--external-health cannot be combined with --${incompatibleFlag}`
|
|
614
|
+
: !Number.isFinite(timeoutMs) || timeoutMs <= 0
|
|
615
|
+
? `invalid --timeout value: ${String(flags.timeout)} (expected a positive number)`
|
|
616
|
+
: isRemoteTarget(targetInput)
|
|
617
|
+
? "--external-health requires a local evidence directory, not a remote repository URL"
|
|
618
|
+
: null;
|
|
619
|
+
if (optionError) {
|
|
620
|
+
if (flags.json)
|
|
621
|
+
console.log(JSON.stringify(machineFailure(optionError)));
|
|
622
|
+
else
|
|
623
|
+
bad(optionError);
|
|
624
|
+
process.exitCode = 1;
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const target = path.resolve(targetInput);
|
|
628
|
+
const evidencePath = ".bootproof/attestation.json";
|
|
629
|
+
const attestation = await buildExternalHealthAttestation(target, externalHealthUrl, timeoutMs);
|
|
630
|
+
writeAttestation(target, attestation);
|
|
631
|
+
const runId = latestAgentRunId(target);
|
|
632
|
+
if (runId)
|
|
633
|
+
appendAgentVerification(target, runId, attestation);
|
|
634
|
+
if (flags.json)
|
|
635
|
+
console.log(JSON.stringify(externalMachineResult(attestation, evidencePath)));
|
|
636
|
+
else {
|
|
637
|
+
printExternalHealthResult(attestation, evidencePath);
|
|
638
|
+
if (runId)
|
|
639
|
+
console.log(`Agent run verification: ${runId}`);
|
|
640
|
+
}
|
|
641
|
+
process.exitCode = attestation.result.healthVerified ? 0 : 1;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
164
644
|
let target = path.resolve(targetInput);
|
|
165
645
|
let remote = null;
|
|
166
646
|
let remoteSource = null;
|
|
167
|
-
if (["analyze", "plan", "up"].includes(cmd) && isRemoteTarget(targetInput)) {
|
|
647
|
+
if (["analyze", "plan", "plan-agent", "up", "fix"].includes(cmd) && isRemoteTarget(targetInput)) {
|
|
168
648
|
if (flags["dry-run"]) {
|
|
169
649
|
const explanation = "Remote dry runs are refused because cloning would write files, while BootProof dry runs promise to write nothing.";
|
|
170
650
|
if (flags.json)
|
|
@@ -175,12 +655,12 @@ async function main() {
|
|
|
175
655
|
return;
|
|
176
656
|
}
|
|
177
657
|
try {
|
|
178
|
-
remote =
|
|
658
|
+
remote = cloneRemoteTarget(targetInput, process.cwd());
|
|
179
659
|
target = remote.repoPath;
|
|
180
660
|
remoteSource = remote.canonicalUrl;
|
|
181
661
|
if (!flags.json) {
|
|
182
662
|
console.log(`${DIM}Remote source: ${remote.canonicalUrl}${RESET}`);
|
|
183
|
-
console.log(`${DIM}Clone retained at: ${
|
|
663
|
+
console.log(`${DIM}Clone retained at: ${portableRelative(process.cwd(), remote.repoPath)}${RESET}`);
|
|
184
664
|
}
|
|
185
665
|
}
|
|
186
666
|
catch (error) {
|
|
@@ -193,14 +673,14 @@ async function main() {
|
|
|
193
673
|
return;
|
|
194
674
|
}
|
|
195
675
|
}
|
|
196
|
-
if (!remote && ["analyze", "plan", "up"].includes(cmd)) {
|
|
676
|
+
if (!remote && ["analyze", "plan", "plan-agent", "up", "fix"].includes(cmd)) {
|
|
197
677
|
remoteSource = managedRemoteSource(target);
|
|
198
678
|
if (remoteSource && !flags.json) {
|
|
199
679
|
console.log(`${DIM}Managed remote source: ${remoteSource}${RESET}`);
|
|
200
680
|
}
|
|
201
681
|
}
|
|
202
682
|
const evidencePath = remote
|
|
203
|
-
?
|
|
683
|
+
? portableRelative(process.cwd(), attestationPath(target))
|
|
204
684
|
: ".bootproof/attestation.json";
|
|
205
685
|
if (cmd === "analyze") {
|
|
206
686
|
const inf = inferRepo(target, { workspace: flags.workspace });
|
|
@@ -223,17 +703,209 @@ async function main() {
|
|
|
223
703
|
console.log(`${DIM}--- .env.bootproof.example (preview) ---\n${envExampleFor(inf)}${RESET}`);
|
|
224
704
|
return;
|
|
225
705
|
}
|
|
706
|
+
if (cmd === "plan-agent") {
|
|
707
|
+
const plan = buildAgentPlan(target);
|
|
708
|
+
const output = writeAgentPlan(target, plan);
|
|
709
|
+
const summary = createAgentRun(target, plan);
|
|
710
|
+
const displayedOutput = remote
|
|
711
|
+
? portableRelative(process.cwd(), output)
|
|
712
|
+
: portableRelative(process.cwd(), agentPlanPath(target));
|
|
713
|
+
const displayedRun = portableRelative(process.cwd(), agentRunDirectory(target, summary.runId));
|
|
714
|
+
if (flags.json)
|
|
715
|
+
console.log(JSON.stringify(plan));
|
|
716
|
+
else
|
|
717
|
+
printAgentPlan(plan, displayedOutput, summary.runId, displayedRun);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (cmd === "apply-repair") {
|
|
721
|
+
if (isRemoteTarget(targetInput)) {
|
|
722
|
+
const result = {
|
|
723
|
+
schema: "bootproof/repair-apply-result/v1",
|
|
724
|
+
applied: false,
|
|
725
|
+
receiptPath: String(flags.receipt ?? ".bootproof/repair-receipt.json"),
|
|
726
|
+
filesChanged: [],
|
|
727
|
+
explanation: "apply-repair requires a local working tree; use the retained managed clone path for a remote repair",
|
|
728
|
+
};
|
|
729
|
+
if (flags.json)
|
|
730
|
+
console.log(JSON.stringify(result));
|
|
731
|
+
else
|
|
732
|
+
printRepairApplyResult(result);
|
|
733
|
+
process.exitCode = 1;
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (flags["dry-run"]) {
|
|
737
|
+
const result = {
|
|
738
|
+
schema: "bootproof/repair-apply-result/v1",
|
|
739
|
+
applied: false,
|
|
740
|
+
receiptPath: String(flags.receipt ?? ".bootproof/repair-receipt.json"),
|
|
741
|
+
filesChanged: [],
|
|
742
|
+
explanation: "Dry run — no repair files were applied.",
|
|
743
|
+
};
|
|
744
|
+
if (flags.json)
|
|
745
|
+
console.log(JSON.stringify(result));
|
|
746
|
+
else
|
|
747
|
+
printRepairApplyResult(result);
|
|
748
|
+
process.exitCode = 1;
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const receipt = flags.receipt
|
|
752
|
+
? path.resolve(String(flags.receipt))
|
|
753
|
+
: path.join(target, ".bootproof", "repair-receipt.json");
|
|
754
|
+
const result = applyVerifiedRepair(target, receipt);
|
|
755
|
+
if (flags.json)
|
|
756
|
+
console.log(JSON.stringify(result));
|
|
757
|
+
else
|
|
758
|
+
printRepairApplyResult(result);
|
|
759
|
+
process.exitCode = result.applied ? 0 : 1;
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (cmd === "fix") {
|
|
763
|
+
if (flags["dry-run"]) {
|
|
764
|
+
const result = {
|
|
765
|
+
schema: "bootproof/repair-result/v1",
|
|
766
|
+
repaired: false,
|
|
767
|
+
failureClass: null,
|
|
768
|
+
repairId: null,
|
|
769
|
+
receiptPath: null,
|
|
770
|
+
patchPath: null,
|
|
771
|
+
afterAttestationPath: null,
|
|
772
|
+
explanation: "Dry run — nothing was executed, nothing was written, and no repair proof exists.",
|
|
773
|
+
};
|
|
774
|
+
if (flags.json)
|
|
775
|
+
console.log(JSON.stringify(result));
|
|
776
|
+
else
|
|
777
|
+
printRepairResult(result);
|
|
778
|
+
process.exitCode = 1;
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const provider = flags.provider;
|
|
782
|
+
const timeoutMs = Number(flags.timeout ?? 60_000);
|
|
783
|
+
const port = flags.port === undefined ? undefined : Number(flags.port);
|
|
784
|
+
const optionError = provider !== undefined && provider !== "docker" && provider !== "local"
|
|
785
|
+
? `invalid --provider value: ${String(provider)} (expected docker or local)`
|
|
786
|
+
: !Number.isFinite(timeoutMs) || timeoutMs <= 0
|
|
787
|
+
? `invalid --timeout value: ${String(flags.timeout)} (expected a positive number)`
|
|
788
|
+
: port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
789
|
+
? `invalid --port value: ${String(flags.port)} (expected an integer from 1 to 65535)`
|
|
790
|
+
: null;
|
|
791
|
+
if (optionError) {
|
|
792
|
+
const result = {
|
|
793
|
+
schema: "bootproof/repair-result/v1",
|
|
794
|
+
repaired: false,
|
|
795
|
+
failureClass: null,
|
|
796
|
+
repairId: null,
|
|
797
|
+
receiptPath: null,
|
|
798
|
+
patchPath: null,
|
|
799
|
+
afterAttestationPath: null,
|
|
800
|
+
explanation: optionError,
|
|
801
|
+
};
|
|
802
|
+
if (flags.json)
|
|
803
|
+
console.log(JSON.stringify(result));
|
|
804
|
+
else
|
|
805
|
+
printRepairResult(result);
|
|
806
|
+
process.exitCode = 1;
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const latestCandidate = latestDeterministicRepairCandidate(target, provider);
|
|
810
|
+
const latestFailure = latestFailedAttestation(target, provider);
|
|
811
|
+
const hasRegisteredDeterministicRepair = Boolean(latestFailure?.result.failureClass &&
|
|
812
|
+
registeredRemediationsFor(latestFailure.result.failureClass).length);
|
|
813
|
+
if (!latestCandidate && !hasRegisteredDeterministicRepair && flags.ai) {
|
|
814
|
+
resolveAiProvider();
|
|
815
|
+
const before = latestFailure;
|
|
816
|
+
if (!before) {
|
|
817
|
+
throw new Error("AI-assisted repair requires a signature-valid failed attestation. Run bootproof up first.");
|
|
818
|
+
}
|
|
819
|
+
if (flags.json || flags.ci) {
|
|
820
|
+
throw new Error("AI-assisted repair requires interactive consent and approval; --json and --ci never prompt or approve actions.");
|
|
821
|
+
}
|
|
822
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
823
|
+
try {
|
|
824
|
+
const requestApproved = await prompt.question("No verified deterministic remediation is known. Request an AI suggestion using your BYOK provider? Type Y to approve: ") === "Y";
|
|
825
|
+
if (!requestApproved) {
|
|
826
|
+
const result = {
|
|
827
|
+
schema: "bootproof/repair-result/v1",
|
|
828
|
+
repaired: false,
|
|
829
|
+
failureClass: before.result.failureClass,
|
|
830
|
+
repairId: null,
|
|
831
|
+
receiptPath: null,
|
|
832
|
+
patchPath: null,
|
|
833
|
+
afterAttestationPath: null,
|
|
834
|
+
explanation: "AI suggestion request was declined. No AI provider request, command, or patch occurred.",
|
|
835
|
+
};
|
|
836
|
+
printRepairResult(result);
|
|
837
|
+
process.exitCode = 1;
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const requested = await requestAiRepairSuggestion(before, { timeoutMs });
|
|
841
|
+
printAiRepairCandidate(requested);
|
|
842
|
+
const actionApproved = await aiRepairApproval(requested.action, prompt);
|
|
843
|
+
const repairResult = await executeAiSuggestedRepair(target, before, requested.action, {
|
|
844
|
+
provider: provider,
|
|
845
|
+
unsafeLocal: Boolean(flags["unsafe-local"]),
|
|
846
|
+
timeoutMs,
|
|
847
|
+
port,
|
|
848
|
+
remoteSource: remoteSource ?? undefined,
|
|
849
|
+
actionApproved,
|
|
850
|
+
aiRepair: requested,
|
|
851
|
+
});
|
|
852
|
+
const result = remote ? rebaseRemoteRepairPaths(repairResult, target) : repairResult;
|
|
853
|
+
printRepairResult(result);
|
|
854
|
+
process.exitCode = result.repaired ? 0 : 1;
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
finally {
|
|
858
|
+
prompt.close();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
let actionApproved = false;
|
|
862
|
+
if (latestCandidate && !flags.json) {
|
|
863
|
+
printRepairCandidate(latestCandidate);
|
|
864
|
+
}
|
|
865
|
+
if (latestCandidate &&
|
|
866
|
+
latestCandidate.candidate.action.actionType !== "instruction" &&
|
|
867
|
+
!flags.json &&
|
|
868
|
+
!flags.ci) {
|
|
869
|
+
const effectiveProvider = provider ?? latestCandidate.attestation.plan.provider;
|
|
870
|
+
if (effectiveProvider !== "local" || flags["unsafe-local"]) {
|
|
871
|
+
actionApproved = latestCandidate.candidate.action.actionType === "command"
|
|
872
|
+
? await commandRepairApproval(latestCandidate.candidate.action)
|
|
873
|
+
: await patchRepairApproval(latestCandidate.candidate.action);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const repairResult = await repairRepo(target, {
|
|
877
|
+
provider: provider,
|
|
878
|
+
unsafeLocal: Boolean(flags["unsafe-local"]),
|
|
879
|
+
timeoutMs,
|
|
880
|
+
port,
|
|
881
|
+
remoteSource: remoteSource ?? undefined,
|
|
882
|
+
actionApproved,
|
|
883
|
+
});
|
|
884
|
+
const result = remote ? rebaseRemoteRepairPaths(repairResult, target) : repairResult;
|
|
885
|
+
if (flags.json)
|
|
886
|
+
console.log(JSON.stringify(result));
|
|
887
|
+
else
|
|
888
|
+
printRepairResult(result);
|
|
889
|
+
process.exitCode = result.repaired ? 0 : 1;
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
226
892
|
if (cmd === "up") {
|
|
227
893
|
const provider = flags.provider ?? "docker";
|
|
228
894
|
const timeoutMs = Number(flags.timeout ?? 60_000);
|
|
229
895
|
const port = flags.port === undefined ? undefined : Number(flags.port);
|
|
896
|
+
const command = flags.command;
|
|
897
|
+
const healthPath = typeof flags["health-path"] === "string" ? flags["health-path"] : undefined;
|
|
230
898
|
const optionError = provider !== "docker" && provider !== "local"
|
|
231
899
|
? `invalid --provider value: ${String(provider)} (expected docker or local)`
|
|
232
900
|
: !Number.isFinite(timeoutMs) || timeoutMs <= 0
|
|
233
901
|
? `invalid --timeout value: ${String(flags.timeout)} (expected a positive number)`
|
|
234
902
|
: port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
235
903
|
? `invalid --port value: ${String(flags.port)} (expected an integer from 1 to 65535)`
|
|
236
|
-
:
|
|
904
|
+
: command !== undefined && (typeof command !== "string" || command.trim().length === 0)
|
|
905
|
+
? "--command requires a non-empty command string"
|
|
906
|
+
: healthPath !== undefined && (typeof healthPath !== "string" || !healthPath.startsWith("/"))
|
|
907
|
+
? "--health-path requires a path starting with / (e.g. /health)"
|
|
908
|
+
: null;
|
|
237
909
|
if (optionError) {
|
|
238
910
|
if (flags.json)
|
|
239
911
|
console.log(JSON.stringify(machineFailure(optionError)));
|
|
@@ -251,9 +923,18 @@ async function main() {
|
|
|
251
923
|
timeoutMs,
|
|
252
924
|
install: Boolean(flags.install),
|
|
253
925
|
port,
|
|
926
|
+
command: typeof command === "string" ? command : undefined,
|
|
927
|
+
healthPath,
|
|
928
|
+
ciOidc: Boolean(flags["ci-oidc"]),
|
|
254
929
|
};
|
|
255
930
|
const outcome = await up(target, opts);
|
|
256
931
|
const verified = outcome.attestation?.result.booted === true && outcome.attestation.result.healthVerified === true;
|
|
932
|
+
if (flags.receipt && outcome.attestation) {
|
|
933
|
+
const receiptPath = path.join(target, ".bootproof", "living-receipt.html");
|
|
934
|
+
emitLivingReceipt(outcome.attestation, receiptPath);
|
|
935
|
+
if (!flags.json)
|
|
936
|
+
console.log(`${GREEN}\u2713${RESET} Living Receipt: ${portableRelative(process.cwd(), receiptPath)}`);
|
|
937
|
+
}
|
|
257
938
|
if (flags.json) {
|
|
258
939
|
console.log(JSON.stringify(machineResult(outcome, evidencePath)));
|
|
259
940
|
if (flags.ci || !opts.dryRun)
|
|
@@ -296,16 +977,43 @@ async function main() {
|
|
|
296
977
|
return;
|
|
297
978
|
}
|
|
298
979
|
if (cmd === "verify") {
|
|
299
|
-
const
|
|
980
|
+
const directoryTarget = fs.existsSync(target) && fs.statSync(target).isDirectory();
|
|
981
|
+
const p = directoryTarget ? attestationPath(target) : target;
|
|
300
982
|
if (!fs.existsSync(p)) {
|
|
301
|
-
bad(`no
|
|
983
|
+
bad(`no proof at ${p} — run bootproof up or bootproof fix first`);
|
|
302
984
|
process.exitCode = 1;
|
|
303
985
|
return;
|
|
304
986
|
}
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
987
|
+
const proof = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
988
|
+
const requireKnownSigner = Boolean(flags["require-known-signer"] || flags.strict);
|
|
989
|
+
const trustRequested = Boolean(flags["trust-signer"]);
|
|
990
|
+
if (isRepairReceipt(proof)) {
|
|
991
|
+
let trust = evaluateRepairReceiptSignature(proof);
|
|
992
|
+
trust = maybeTrustSigner(trust, proof.signer?.publicKey, trustRequested, () => evaluateRepairReceiptSignature(proof));
|
|
993
|
+
printSignatureTrust(trust);
|
|
994
|
+
const summary = proof.verification && proof.repair
|
|
995
|
+
? `failure=${proof.verification.before.failureClass} repair=${proof.repair.id} after=${proof.verification.after.healthObservation}`
|
|
996
|
+
: `failure=${proof.beforeFailureClass} repair=${proof.repairId} applied=${proof.applyResult.status} progressed=${proof.progressed} verified=${proof.verified}`;
|
|
997
|
+
console.log(`${DIM}${summary}${RESET}`);
|
|
998
|
+
if (signerTrustFails(trust, requireKnownSigner))
|
|
999
|
+
process.exitCode = 1;
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const att = proof;
|
|
1003
|
+
let trust = evaluateAttestationSignature(att);
|
|
1004
|
+
trust = maybeTrustSigner(trust, att.signer?.publicKey, trustRequested, () => evaluateAttestationSignature(att));
|
|
1005
|
+
printSignatureTrust(trust);
|
|
1006
|
+
console.log(`Attested trust claim: ${att.trust?.level ?? "legacy_unspecified"} (signed content; signer tier shown above)`);
|
|
1007
|
+
const commitMismatch = directoryTarget ? commitMismatchMessage(att, target) : null;
|
|
1008
|
+
if (commitMismatch)
|
|
1009
|
+
warn(commitMismatch);
|
|
1010
|
+
if (att.verificationMode === "external-health") {
|
|
1011
|
+
console.log(`${DIM}attested: classification=${att.classification} bootproofOrchestrated=false at ${att.observedAt ?? att.finishedAt}${RESET}`);
|
|
1012
|
+
console.log("This attestation observes an externally managed service; it does not claim BootProof started the application.");
|
|
1013
|
+
if (signerTrustFails(trust, requireKnownSigner) || (commitMismatch && requireKnownSigner))
|
|
1014
|
+
process.exitCode = 1;
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
309
1017
|
console.log(`${DIM}attested: booted=${att.result.booted} at commit ${att.repo.commit ?? "unknown"} on ${att.environment.os} node ${att.environment.node}${RESET}`);
|
|
310
1018
|
const retainedRemote = managedRemoteSource(att.repo.path);
|
|
311
1019
|
if (retainedRemote) {
|
|
@@ -315,29 +1023,85 @@ async function main() {
|
|
|
315
1023
|
else {
|
|
316
1024
|
console.log(`Replaying attested plan with bootproof up --provider ${att.plan.provider} would re-verify it on this machine.`);
|
|
317
1025
|
}
|
|
318
|
-
if (att.result.booted) {
|
|
1026
|
+
if (att.result.booted && !commitMismatch && (trust.tier === "self" || trust.tier === "known")) {
|
|
319
1027
|
const live = await pollHealth(att.plan.healthUrl, 3000);
|
|
320
1028
|
if (live.responded)
|
|
321
1029
|
ok(`bonus observation: ${att.plan.healthUrl} is responding right now (HTTP ${live.status})`);
|
|
322
1030
|
else
|
|
323
1031
|
console.log(`${DIM}(app not currently running — attestation describes a past verified run)${RESET}`);
|
|
324
1032
|
}
|
|
325
|
-
if (
|
|
1033
|
+
else if (att.result.booted && commitMismatch) {
|
|
1034
|
+
console.log(`${DIM}(live health bonus skipped because the attestation does not match the current commit)${RESET}`);
|
|
1035
|
+
}
|
|
1036
|
+
else if (att.result.booted && trust.tier === "unknown-foreign") {
|
|
1037
|
+
console.log(`${DIM}(live health bonus skipped because the signer is unknown)${RESET}`);
|
|
1038
|
+
}
|
|
1039
|
+
if (signerTrustFails(trust, requireKnownSigner) || (commitMismatch && requireKnownSigner))
|
|
326
1040
|
process.exitCode = 1;
|
|
327
1041
|
return;
|
|
328
1042
|
}
|
|
1043
|
+
if (cmd === "registry") {
|
|
1044
|
+
const sub = positional[0];
|
|
1045
|
+
const repo = path.resolve(String(positional[1] ?? "."));
|
|
1046
|
+
if (sub !== "export") {
|
|
1047
|
+
bad(`unknown registry subcommand: ${sub ?? "(none)"} — use export`);
|
|
1048
|
+
process.exitCode = 1;
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const requestedMode = String(flags.mode ?? (flags.federated ? "federated_public_candidate" : "local_export"));
|
|
1052
|
+
const registryModes = ["local_export", "federated_public_candidate", "cloud_upload_candidate"];
|
|
1053
|
+
if (!registryModes.includes(requestedMode)) {
|
|
1054
|
+
bad(`invalid --mode value: ${requestedMode}`);
|
|
1055
|
+
process.exitCode = 1;
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
if (flags.federated && requestedMode !== "federated_public_candidate") {
|
|
1059
|
+
bad("--federated requires --mode federated_public_candidate when --mode is provided");
|
|
1060
|
+
process.exitCode = 1;
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const entry = registryEntryFor(repo, requestedMode);
|
|
1064
|
+
if (!entry) {
|
|
1065
|
+
bad(`no attestation at ${attestationPath(repo)} — run bootproof up first`);
|
|
1066
|
+
process.exitCode = 1;
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (flags.federated) {
|
|
1070
|
+
const receipt = buildFederatedReceipt(entry, { sign: true });
|
|
1071
|
+
const output = writeFederatedReceipt(repo, receipt);
|
|
1072
|
+
ok(`wrote redacted federated public candidate: ${output}`);
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
const output = writeRegistryEntry(repo, entry);
|
|
1076
|
+
ok(`wrote redacted registry entry: ${output}`);
|
|
1077
|
+
}
|
|
1078
|
+
console.log(`${DIM}redactions applied: ${entry.redactionsApplied.join(", ")}${RESET}`);
|
|
1079
|
+
console.log("Nothing has been uploaded. This export is local and opt-in.");
|
|
1080
|
+
if (flags.federated) {
|
|
1081
|
+
console.log("Review the receipt before deliberately committing it to the public repository.");
|
|
1082
|
+
}
|
|
1083
|
+
else if (requestedMode === "cloud_upload_candidate") {
|
|
1084
|
+
console.log("This is only a Cloud upload candidate. BootProof Cloud upload is not implemented here.");
|
|
1085
|
+
}
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
329
1088
|
if (cmd === "attest") {
|
|
330
1089
|
const sub = positional[0];
|
|
331
1090
|
const repo = path.resolve(String(positional[1] ?? "."));
|
|
332
1091
|
if (sub === "export") {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
1092
|
+
const verificationFlag = ["trust-signer", "require-known-signer", "strict"]
|
|
1093
|
+
.find(flag => flags[flag] !== undefined);
|
|
1094
|
+
if (verificationFlag) {
|
|
1095
|
+
bad(`--${verificationFlag} is supported only by attest check`);
|
|
1096
|
+
process.exitCode = 1;
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
const entry = registryEntryFor(repo, "local_export");
|
|
1100
|
+
if (!entry) {
|
|
1101
|
+
bad(`no attestation at ${attestationPath(repo)} — run bootproof up first`);
|
|
336
1102
|
process.exitCode = 1;
|
|
337
1103
|
return;
|
|
338
1104
|
}
|
|
339
|
-
const att = JSON.parse(fs.readFileSync(ap, "utf8"));
|
|
340
|
-
const entry = buildRegistryEntry(att);
|
|
341
1105
|
const out = writeRegistryEntry(repo, entry);
|
|
342
1106
|
ok(`wrote redacted registry entry: ${out}`);
|
|
343
1107
|
console.log(`${DIM}redactions applied: ${entry.redactionsApplied.length ? entry.redactionsApplied.join(", ") : "none needed"}${RESET}`);
|
|
@@ -354,10 +1118,12 @@ async function main() {
|
|
|
354
1118
|
return;
|
|
355
1119
|
}
|
|
356
1120
|
const entry = JSON.parse(fs.readFileSync(ep, "utf8"));
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
1121
|
+
const requireKnownSigner = Boolean(flags["require-known-signer"] || flags.strict);
|
|
1122
|
+
let trust = evaluateRegistryEntrySignature(entry);
|
|
1123
|
+
trust = maybeTrustSigner(trust, entry.signature?.publicKey, Boolean(flags["trust-signer"]), () => evaluateRegistryEntrySignature(entry));
|
|
1124
|
+
printSignatureTrust(trust);
|
|
1125
|
+
console.log(`${DIM}verified=${entry.verified} class=${entry.failureClass ?? "none"} commit=${entry.commitHash?.slice(0, 8) ?? "?"}${RESET}`);
|
|
1126
|
+
if (signerTrustFails(trust, requireKnownSigner))
|
|
361
1127
|
process.exitCode = 1;
|
|
362
1128
|
return;
|
|
363
1129
|
}
|
|
@@ -365,12 +1131,121 @@ async function main() {
|
|
|
365
1131
|
process.exitCode = 1;
|
|
366
1132
|
return;
|
|
367
1133
|
}
|
|
1134
|
+
if (cmd === "export-sbom") {
|
|
1135
|
+
const format = flags.format ?? "cyclonedx-json";
|
|
1136
|
+
if (format !== "cyclonedx-json") {
|
|
1137
|
+
if (flags.json)
|
|
1138
|
+
console.log(JSON.stringify(machineFailure(`Unsupported SBOM format: ${format}. Currently supported: cyclonedx-json.`)));
|
|
1139
|
+
else
|
|
1140
|
+
bad(`Unsupported SBOM format: ${format}. Currently supported: cyclonedx-json.`);
|
|
1141
|
+
process.exitCode = 1;
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
const result = exportSbom(target, format);
|
|
1146
|
+
if (flags.json) {
|
|
1147
|
+
console.log(JSON.stringify(result));
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
ok(`SBOM exported: ${portableRelative(process.cwd(), result.path)}`);
|
|
1151
|
+
console.log(` format: ${result.format}`);
|
|
1152
|
+
console.log(` components: ${result.componentCount}`);
|
|
1153
|
+
console.log(`${DIM}CycloneDX 1.5 JSON. Read from package-lock.json. No transitive resolution beyond the lockfile.${RESET}`);
|
|
1154
|
+
}
|
|
1155
|
+
catch (e) {
|
|
1156
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1157
|
+
if (flags.json)
|
|
1158
|
+
console.log(JSON.stringify(machineFailure(msg)));
|
|
1159
|
+
else
|
|
1160
|
+
bad(msg);
|
|
1161
|
+
process.exitCode = 1;
|
|
1162
|
+
}
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (cmd === "rotate-keys") {
|
|
1166
|
+
const repo = flags.repo ? path.resolve(String(flags.repo)) : undefined;
|
|
1167
|
+
const resign = Boolean(flags.resign);
|
|
1168
|
+
const backup = !flags["no-backup"];
|
|
1169
|
+
try {
|
|
1170
|
+
const result = rotateSigner({ repo, resignAttestation: resign, backup });
|
|
1171
|
+
if (flags.json) {
|
|
1172
|
+
console.log(JSON.stringify(result));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
ok("Signing key rotated.");
|
|
1176
|
+
console.log(` old public key: ${result.oldPublicKey.split("\n")[0]}...`);
|
|
1177
|
+
console.log(` new public key: ${result.newPublicKey.split("\n")[0]}...`);
|
|
1178
|
+
if (result.backedUpTo)
|
|
1179
|
+
console.log(` old key backed up to: ${result.backedUpTo}`);
|
|
1180
|
+
if (result.reSignedAttestation)
|
|
1181
|
+
console.log(` re-signed latest attestation at ${repo}`);
|
|
1182
|
+
console.log(`${DIM}Existing attestations still verify with their embedded public key. Rotation only affects future signatures.${RESET}`);
|
|
1183
|
+
}
|
|
1184
|
+
catch (e) {
|
|
1185
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1186
|
+
if (flags.json)
|
|
1187
|
+
console.log(JSON.stringify(machineFailure(msg)));
|
|
1188
|
+
else
|
|
1189
|
+
bad(msg);
|
|
1190
|
+
process.exitCode = 1;
|
|
1191
|
+
}
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
368
1194
|
if (cmd === "explain") {
|
|
369
1195
|
const p = positional[0] ? path.resolve(positional[0]) : attestationPath(target);
|
|
370
|
-
const
|
|
1196
|
+
const proof = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1197
|
+
if (isRepairReceipt(proof)) {
|
|
1198
|
+
const trust = evaluateRepairReceiptSignature(proof);
|
|
1199
|
+
console.log(`${BOLD}Repair receipt explained${RESET}`);
|
|
1200
|
+
console.log(`Signature: ${signatureTrustText(trust).replace(/^signature /, "")}`);
|
|
1201
|
+
if (!trust.integrityValid) {
|
|
1202
|
+
console.log("The receipt has been tampered with or is malformed. Its repair claims are not trusted.");
|
|
1203
|
+
process.exitCode = 1;
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
console.log(`Before: NOT VERIFIED — ${proof.beforeFailureClass}.`);
|
|
1207
|
+
console.log(`Repair: ${proof.repairId} (${proof.actionType}; scope=${proof.mutationScope}; risk=${proof.riskLevel}).`);
|
|
1208
|
+
if (proof.proposedAction.command)
|
|
1209
|
+
console.log(`Command: ${proof.proposedAction.command.display}`);
|
|
1210
|
+
if (proof.proposedAction.instruction)
|
|
1211
|
+
console.log(`Instruction: ${proof.proposedAction.instruction}`);
|
|
1212
|
+
console.log(`Applied: ${proof.applyResult.status}. Progressed: ${proof.progressed}. Verified: ${proof.verified}.`);
|
|
1213
|
+
if (proof.afterFailureClass)
|
|
1214
|
+
console.log(`After failure: ${proof.afterFailureClass}.`);
|
|
1215
|
+
console.log(`Description: ${proof.explanation}`);
|
|
1216
|
+
if (proof.repair) {
|
|
1217
|
+
if (proof.repair.filesChanged.length)
|
|
1218
|
+
console.log(`Files changed in sandbox: ${proof.repair.filesChanged.join(", ")}`);
|
|
1219
|
+
if (proof.repair.planDelta)
|
|
1220
|
+
console.log(`Plan delta: ${proof.repair.planDelta}`);
|
|
1221
|
+
if (proof.repair.envDelta)
|
|
1222
|
+
console.log(`Environment delta: ${proof.repair.envDelta}`);
|
|
1223
|
+
}
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const att = proof;
|
|
1227
|
+
const trust = evaluateAttestationSignature(att);
|
|
371
1228
|
console.log(`${BOLD}Attestation explained${RESET}`);
|
|
1229
|
+
console.log(`Signature: ${signatureTrustText(trust).replace(/^signature /, "")}`);
|
|
1230
|
+
if (!trust.integrityValid) {
|
|
1231
|
+
console.log("The attestation has been tampered with or is malformed. Its boot claims are not trusted.");
|
|
1232
|
+
process.exitCode = 1;
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (att.verificationMode === "external-health") {
|
|
1236
|
+
console.log(att.result.healthVerified
|
|
1237
|
+
? `This externally managed service was VERIFIED: ${att.result.healthObservation}.`
|
|
1238
|
+
: `This external health check did NOT verify. Classification: ${att.classification}.`);
|
|
1239
|
+
console.log("BootProof did not start or orchestrate this service.");
|
|
1240
|
+
console.log(att.result.explanation);
|
|
1241
|
+
if (att.plan.healthCandidates?.length)
|
|
1242
|
+
console.log(`Health candidates: ${att.plan.healthCandidates.join(", ")}`);
|
|
1243
|
+
for (const o of att.observed)
|
|
1244
|
+
console.log(` ${o.ok ? "\u2713" : "\u2717"} ${o.id}: ${o.observation}`);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
372
1247
|
console.log(att.result.booted ? `This run BOOTED: ${att.result.healthObservation}.` : `This run did NOT verify. Failure class: ${att.result.failureClass}.`);
|
|
373
|
-
console.log(`
|
|
1248
|
+
console.log(`Attested trust claim: ${att.trust?.level ?? "legacy_unspecified"} (signed content; signer tier shown above)`);
|
|
374
1249
|
if (!att.result.booted && att.result.failureClass) {
|
|
375
1250
|
const diagnosis = diagnoseFailure(att.result.failureClass, att.result.failureEvidence, att.result.explanation);
|
|
376
1251
|
console.log(`What happened: ${diagnosis.whatHappened}`);
|
|
@@ -392,9 +1267,32 @@ async function main() {
|
|
|
392
1267
|
}
|
|
393
1268
|
main().catch(err => {
|
|
394
1269
|
const argv = process.argv.slice(2);
|
|
395
|
-
if (argv[0] === "up"
|
|
1270
|
+
if (argv.includes("--json") && (argv[0] === "up" || argv[0] === "verify-url")) {
|
|
396
1271
|
console.log(JSON.stringify(machineFailure(String(err?.message ?? err))));
|
|
397
1272
|
}
|
|
1273
|
+
else if (argv.includes("--json") && argv[0] === "fix") {
|
|
1274
|
+
const result = {
|
|
1275
|
+
schema: "bootproof/repair-result/v1",
|
|
1276
|
+
repaired: false,
|
|
1277
|
+
failureClass: null,
|
|
1278
|
+
repairId: null,
|
|
1279
|
+
receiptPath: null,
|
|
1280
|
+
patchPath: null,
|
|
1281
|
+
afterAttestationPath: null,
|
|
1282
|
+
explanation: String(err?.message ?? err),
|
|
1283
|
+
};
|
|
1284
|
+
console.log(JSON.stringify(result));
|
|
1285
|
+
}
|
|
1286
|
+
else if (argv.includes("--json") && argv[0] === "apply-repair") {
|
|
1287
|
+
const result = {
|
|
1288
|
+
schema: "bootproof/repair-apply-result/v1",
|
|
1289
|
+
applied: false,
|
|
1290
|
+
receiptPath: ".bootproof/repair-receipt.json",
|
|
1291
|
+
filesChanged: [],
|
|
1292
|
+
explanation: String(err?.message ?? err),
|
|
1293
|
+
};
|
|
1294
|
+
console.log(JSON.stringify(result));
|
|
1295
|
+
}
|
|
398
1296
|
else {
|
|
399
1297
|
bad(String(err?.message ?? err));
|
|
400
1298
|
}
|