bootproof 0.3.0 → 0.4.1

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.
Files changed (71) hide show
  1. package/README.md +844 -152
  2. package/dist/agent-plan.d.ts +44 -0
  3. package/dist/agent-plan.js +826 -0
  4. package/dist/agent-run.d.ts +117 -0
  5. package/dist/agent-run.js +459 -0
  6. package/dist/ai-repair.d.ts +58 -0
  7. package/dist/ai-repair.js +380 -0
  8. package/dist/cli.js +730 -46
  9. package/dist/diagnosis.js +101 -16
  10. package/dist/diff.d.ts +29 -0
  11. package/dist/diff.js +569 -0
  12. package/dist/exec.d.ts +30 -2
  13. package/dist/exec.js +329 -51
  14. package/dist/external-health.d.ts +16 -0
  15. package/dist/external-health.js +214 -0
  16. package/dist/infer.js +238 -39
  17. package/dist/plan.js +2 -0
  18. package/dist/proof.d.ts +78 -2
  19. package/dist/proof.js +265 -12
  20. package/dist/receipt.d.ts +52 -0
  21. package/dist/receipt.js +356 -0
  22. package/dist/redact.d.ts +4 -0
  23. package/dist/redact.js +86 -2
  24. package/dist/registry.d.ts +82 -30
  25. package/dist/registry.js +355 -53
  26. package/dist/remote.js +3 -3
  27. package/dist/repair-playbooks.d.ts +24 -0
  28. package/dist/repair-playbooks.js +593 -0
  29. package/dist/repair-safety.d.ts +130 -0
  30. package/dist/repair-safety.js +766 -0
  31. package/dist/repair.d.ts +43 -11
  32. package/dist/repair.js +716 -7
  33. package/dist/run.d.ts +3 -0
  34. package/dist/run.js +218 -41
  35. package/dist/sbom.d.ts +22 -0
  36. package/dist/sbom.js +99 -0
  37. package/dist/taxonomy.d.ts +8 -3
  38. package/dist/taxonomy.js +404 -8
  39. package/dist/types.d.ts +40 -1
  40. package/docs/AGENT_IN_THE_LOOP.md +171 -0
  41. package/docs/AGENT_RUN_RECEIPTS.md +38 -0
  42. package/docs/CI_ACTION.md +67 -2
  43. package/docs/DETERMINISTIC_REPAIR_SAFETY_MODEL.md +705 -0
  44. package/docs/DISTRIBUTION.md +83 -0
  45. package/docs/FAILURE_TAXONOMY.md +28 -1
  46. package/docs/HONESTY_CONTRACT.md +34 -12
  47. package/docs/LAUNCH_PLAYBOOK.md +232 -0
  48. package/docs/REAL_WORLD_FIXTURES.md +105 -0
  49. package/docs/REGISTRY.md +48 -28
  50. package/docs/REPAIR_RECEIPT.md +54 -8
  51. package/docs/agent-loop-gap-analysis.md +188 -0
  52. package/docs/examples/registry-seeds/advertised-port-mismatch.json +28 -0
  53. package/docs/examples/registry-seeds/airbyte-abctl-external-orchestrator.json +36 -0
  54. package/docs/examples/registry-seeds/go-ollama-service.json +36 -0
  55. package/docs/examples/registry-seeds/laravel-vite-sqlite.json +36 -0
  56. package/docs/examples/registry-seeds/monorepo-ambiguous-health.json +29 -0
  57. package/docs/examples/registry-seeds/php-composer.json +33 -0
  58. package/docs/examples/registry-seeds/rails-bundler.json +32 -0
  59. package/docs/examples/registry-seeds/sentry-devenv-direnv.json +41 -0
  60. package/docs/schemas/action-verdict-v1.schema.json +64 -0
  61. package/docs/schemas/agent-plan-v1.schema.json +148 -0
  62. package/docs/schemas/agent-run-receipts-v1.schema.json +192 -0
  63. package/docs/schemas/ai-repair-suggestion-v1.schema.json +70 -0
  64. package/docs/schemas/ci-context-v1.schema.json +63 -0
  65. package/docs/schemas/diff-result-v1.schema.json +66 -0
  66. package/docs/schemas/federated-receipt-v1.schema.json +51 -0
  67. package/docs/schemas/registry-entry-v1.schema.json +95 -0
  68. package/docs/schemas/registry-seed-example-v1.schema.json +102 -0
  69. package/docs/schemas/repair-action-v1.schema.json +136 -0
  70. package/docs/schemas/repair-receipt-v1.schema.json +221 -0
  71. package/package.json +21 -11
package/dist/repair.js CHANGED
@@ -1,14 +1,21 @@
1
1
  import crypto from "node:crypto";
2
+ import { spawn } from "node:child_process";
2
3
  import fs from "node:fs";
3
4
  import net from "node:net";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
7
  import { parse, stringify } from "yaml";
7
- import { minimalEnv, runToCompletion } from "./exec.js";
8
+ import { buildExecutionEnv, runToCompletion } from "./exec.js";
8
9
  import { REPAIRED_GENERATED_COMPOSE_MARKER, repoComposeRepairFile } from "./plan.js";
9
- import { attestationPath, buildAttestation, gitInfo, signDetached, TOOL_ID, verifyDetached, verifySignature, writeAttestation, } from "./proof.js";
10
+ import { redactText } from "./redact.js";
11
+ import { attestationPath, buildAttestation, evaluateDetachedSignature, gitInfo, signDetached, TOOL_ID, verifyDetached, verifySignature, writeAttestation, } from "./proof.js";
10
12
  import { up } from "./run.js";
11
13
  import { inferRepo } from "./infer.js";
14
+ import { buildExternalHealthAttestation } from "./external-health.js";
15
+ import { buildRepairAction, buildRepairReceiptBase, createRepairCommand, validateRepairAction, } from "./repair-safety.js";
16
+ import { deterministicRepairCandidateFor, repairProgressed, } from "./repair-playbooks.js";
17
+ export * from "./repair-safety.js";
18
+ export * from "./repair-playbooks.js";
12
19
  const LOCKFILE = /^(?:package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|Gemfile\.lock|go\.sum)$/;
13
20
  const ENV_EXAMPLE = /^\.env(?:\.[^/]+)?\.example$/;
14
21
  const BOOTPROOF_FILE = /(^|\/)[^/]*\.bootproof\.[^/]+$/;
@@ -45,6 +52,8 @@ export function assertRepairScope(changes) {
45
52
  const file = normalizedRelative(change.path);
46
53
  const base = path.posix.basename(file);
47
54
  const allowed = file === "package.json" ||
55
+ file === "config/database.yml" ||
56
+ file === "config/gitlab.yml" ||
48
57
  LOCKFILE.test(base) ||
49
58
  BOOTPROOF_FILE.test(file) ||
50
59
  ENV_EXAMPLE.test(base) ||
@@ -74,6 +83,9 @@ export function verifyRepairReceipt(receipt) {
74
83
  return false;
75
84
  return verifyDetached(canonicalReceipt(receipt), receipt.signature, receipt.signer.publicKey);
76
85
  }
86
+ export function evaluateRepairReceiptSignature(receipt) {
87
+ return evaluateDetachedSignature(canonicalReceipt(receipt), receipt.signature, receipt.signer?.publicKey);
88
+ }
77
89
  export function sha256Attestation(attestation) {
78
90
  return crypto.createHash("sha256").update(JSON.stringify(attestation)).digest("hex");
79
91
  }
@@ -237,6 +249,8 @@ async function remapConflictingServicePort(context) {
237
249
  preconditions: [{ path: repoCompose, content: source }],
238
250
  planDelta: `Create ${repairFile} as a complete repaired copy of ${repoCompose}. Use service step: ${command}`,
239
251
  envDelta: null,
252
+ mutationScope: "repo_only",
253
+ riskLevel: "low",
240
254
  };
241
255
  }
242
256
  const generatedPath = path.join(context.sandbox, composeFile);
@@ -260,6 +274,8 @@ async function remapConflictingServicePort(context) {
260
274
  preconditions: [],
261
275
  planDelta: null,
262
276
  envDelta: null,
277
+ mutationScope: "repo_only",
278
+ riskLevel: "low",
263
279
  };
264
280
  }
265
281
  export function packageManagerActivationCommand(packageManager, version) {
@@ -274,7 +290,7 @@ async function activatePackageManager(context) {
274
290
  return null;
275
291
  const corepackHome = path.join(context.sandbox, ".bootproof", "corepack");
276
292
  const environment = { COREPACK_HOME: corepackHome };
277
- const result = await runToCompletion(command, context.sandbox, 120_000, minimalEnv(environment));
293
+ const result = await runToCompletion(command, context.sandbox, 120_000, buildExecutionEnv(environment));
278
294
  if (result.exitCode !== 0 || result.timedOut) {
279
295
  throw new Error(`environment remediation failed: ${command}\n${result.stderr || result.stdout}`);
280
296
  }
@@ -290,6 +306,9 @@ async function activatePackageManager(context) {
290
306
  planDelta: null,
291
307
  envDelta: command,
292
308
  environment,
309
+ mutationScope: "project_cache",
310
+ riskLevel: "medium",
311
+ command: createRepairCommand("corepack", ["prepare", `${packageManager}@${packageManagerVersion}`, "--activate"]),
293
312
  };
294
313
  }
295
314
  export function prismaRepairCommand(repo) {
@@ -308,6 +327,10 @@ function readPackageJson(repo) {
308
327
  function hasAnyFile(repo, files) {
309
328
  return files.find(file => fs.existsSync(path.join(repo, file))) ?? null;
310
329
  }
330
+ function structuredPlaybookCommand(command) {
331
+ const [executable, ...args] = command.split(" ");
332
+ return createRepairCommand(executable, args);
333
+ }
311
334
  export function migrationRepairFor(repo, evidence) {
312
335
  const pkg = readPackageJson(repo);
313
336
  const dependencies = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
@@ -400,6 +423,9 @@ async function deployFrameworkMigrations(context) {
400
423
  planDelta: `Insert after dependency installation and before application start: ${migration.command}`,
401
424
  envDelta: null,
402
425
  additionalPreparationCommands: [preparation],
426
+ mutationScope: "database",
427
+ riskLevel: "high",
428
+ command: structuredPlaybookCommand(migration.command),
403
429
  };
404
430
  }
405
431
  const REGISTRY = {
@@ -462,6 +488,43 @@ function persistFailureAttestation(repo, source, extraEvidence = null) {
462
488
  writeAttestation(repo, attestation);
463
489
  return attestation;
464
490
  }
491
+ function proposedActionFor(applied) {
492
+ if (applied.fileChanges.length) {
493
+ const patch = applied.patch ?? applied.diff;
494
+ if (!patch)
495
+ throw new Error("repair file changes require an exact patch");
496
+ return buildRepairAction({
497
+ actionType: "patch",
498
+ mutationScope: "repo_only",
499
+ riskLevel: applied.riskLevel,
500
+ patch: {
501
+ format: "unified-diff",
502
+ content: patch,
503
+ files: applied.fileChanges.map(change => normalizedRelative(change.path)),
504
+ },
505
+ explanation: applied.description,
506
+ evidenceRefs: [".bootproof/attestation.json"],
507
+ });
508
+ }
509
+ if (applied.command) {
510
+ return buildRepairAction({
511
+ actionType: "command",
512
+ mutationScope: applied.mutationScope,
513
+ riskLevel: applied.riskLevel,
514
+ command: applied.command,
515
+ explanation: applied.description,
516
+ evidenceRefs: [".bootproof/attestation.json"],
517
+ });
518
+ }
519
+ return buildRepairAction({
520
+ actionType: "instruction",
521
+ mutationScope: "none",
522
+ riskLevel: applied.riskLevel,
523
+ instruction: applied.description,
524
+ explanation: applied.description,
525
+ evidenceRefs: [".bootproof/attestation.json"],
526
+ });
527
+ }
465
528
  function buildRepairReceipt(repo, before, after, applied, startedAt) {
466
529
  if (before.result.booted ||
467
530
  before.result.healthVerified ||
@@ -478,8 +541,29 @@ function buildRepairReceipt(repo, before, after, applied, startedAt) {
478
541
  const beforeHash = sha256Attestation(before);
479
542
  const afterHash = sha256Attestation(after);
480
543
  const info = gitInfo(repo);
544
+ const finishedAt = new Date().toISOString();
545
+ const proposedAction = proposedActionFor(applied);
546
+ const base = buildRepairReceiptBase({
547
+ repairId: applied.id,
548
+ createdAt: finishedAt,
549
+ bootproofVersion: TOOL_ID.replace(/^bootproof@/, ""),
550
+ beforeFailureClass: before.result.failureClass,
551
+ beforeEvidenceHash: sha256Text(before.result.failureEvidence ?? ""),
552
+ proposedAction,
553
+ appliedAt: finishedAt,
554
+ applyResult: {
555
+ status: "applied",
556
+ exitCode: 0,
557
+ filesChanged: applied.filesChanged.map(normalizedRelative),
558
+ evidence: null,
559
+ },
560
+ progressed: true,
561
+ verified: true,
562
+ explanation: applied.description,
563
+ redactionsApplied: [],
564
+ });
481
565
  const receipt = {
482
- schema: "bootproof/repair-receipt/v1",
566
+ ...base,
483
567
  tool: TOOL_ID,
484
568
  repo: { remote: info.remote, commit: info.commit, dirty: info.dirty },
485
569
  environment: { os: `${os.platform()} ${os.release()}`, arch: os.arch(), node: process.version },
@@ -512,12 +596,13 @@ function buildRepairReceipt(repo, before, after, applied, startedAt) {
512
596
  },
513
597
  after: {
514
598
  booted: true,
599
+ bootproofOrchestrated: true,
515
600
  healthObservation: after.result.healthObservation,
516
601
  attestationSha256: afterHash,
517
602
  },
518
603
  },
519
604
  startedAt,
520
- finishedAt: new Date().toISOString(),
605
+ finishedAt,
521
606
  signer: null,
522
607
  signature: null,
523
608
  };
@@ -572,6 +657,605 @@ function signedFailedAttestation(repo, requestedProvider) {
572
657
  return null;
573
658
  }
574
659
  }
660
+ export function latestDeterministicRepairCandidate(repoPath, requestedProvider) {
661
+ const repo = path.resolve(repoPath);
662
+ const attestation = signedFailedAttestation(repo, requestedProvider);
663
+ if (!attestation)
664
+ return null;
665
+ const candidate = deterministicRepairCandidateFor(attestation, { repoPath: repo });
666
+ return candidate ? { attestation, candidate } : null;
667
+ }
668
+ export function latestFailedAttestation(repoPath, requestedProvider) {
669
+ return signedFailedAttestation(path.resolve(repoPath), requestedProvider);
670
+ }
671
+ async function runStructuredRepairCommand(command, cwd, timeoutMs = 600_000) {
672
+ return await new Promise(resolve => {
673
+ const child = spawn(command.executable, command.args, {
674
+ cwd,
675
+ shell: false,
676
+ env: buildExecutionEnv(),
677
+ stdio: ["ignore", "pipe", "pipe"],
678
+ });
679
+ let output = "";
680
+ let settled = false;
681
+ const finish = (exitCode, extra = "") => {
682
+ if (settled)
683
+ return;
684
+ settled = true;
685
+ clearTimeout(timer);
686
+ const redacted = redactText(`${output}${extra}`.slice(-4000));
687
+ resolve({
688
+ exitCode,
689
+ evidence: redacted.text,
690
+ redactionsApplied: redacted.applied,
691
+ });
692
+ };
693
+ const capture = (chunk) => {
694
+ output = `${output}${String(chunk)}`.slice(-4000);
695
+ };
696
+ child.stdout?.on("data", capture);
697
+ child.stderr?.on("data", capture);
698
+ child.on("close", code => finish(code));
699
+ child.on("error", error => finish(null, `\n${error.message}`));
700
+ const timer = setTimeout(() => {
701
+ child.kill("SIGTERM");
702
+ finish(null, "\nrepair command timed out");
703
+ }, timeoutMs);
704
+ });
705
+ }
706
+ function buildLifecycleRepairReceipt(input) {
707
+ const after = input.after ?? null;
708
+ const verified = Boolean(after?.result.healthVerified &&
709
+ (after.result.booted || after.verificationMode === "external-health"));
710
+ const progressed = repairProgressed(input.candidate.failureClass, after);
711
+ const info = gitInfo(input.repo);
712
+ const redactedRemote = redactText(info.remote ?? "");
713
+ const redactionsApplied = [...new Set([
714
+ ...(input.redactionsApplied ?? []),
715
+ ...redactedRemote.applied,
716
+ ])];
717
+ const base = buildRepairReceiptBase({
718
+ repairId: input.candidate.id,
719
+ createdAt: input.createdAt,
720
+ bootproofVersion: TOOL_ID.replace(/^bootproof@/, ""),
721
+ beforeFailureClass: input.candidate.failureClass,
722
+ beforeEvidenceHash: sha256Text(input.before.result.failureEvidence ?? ""),
723
+ proposedAction: input.candidate.action,
724
+ ...(input.approvedAt ? { approvedAt: input.approvedAt } : {}),
725
+ ...(input.appliedAt ? { appliedAt: input.appliedAt } : {}),
726
+ applyResult: input.applyResult,
727
+ ...(after?.result.failureClass ? { afterFailureClass: after.result.failureClass } : {}),
728
+ progressed,
729
+ verified,
730
+ explanation: input.explanation,
731
+ redactionsApplied,
732
+ });
733
+ const receipt = {
734
+ ...base,
735
+ tool: TOOL_ID,
736
+ repo: {
737
+ remote: info.remote === null ? null : redactedRemote.text,
738
+ commit: info.commit,
739
+ dirty: info.dirty,
740
+ },
741
+ environment: { os: `${os.platform()} ${os.release()}`, arch: os.arch(), node: process.version },
742
+ failure: {
743
+ class: input.candidate.failureClass,
744
+ beforeAttestationSha256: sha256Attestation(input.before),
745
+ },
746
+ startedAt: input.createdAt,
747
+ finishedAt: new Date().toISOString(),
748
+ ...(verified && after?.result.healthObservation
749
+ ? {
750
+ verification: {
751
+ before: {
752
+ booted: false,
753
+ failureClass: input.candidate.failureClass,
754
+ attestationSha256: sha256Attestation(input.before),
755
+ },
756
+ after: {
757
+ booted: after.result.booted,
758
+ bootproofOrchestrated: after.bootproofOrchestrated,
759
+ healthObservation: after.result.healthObservation,
760
+ attestationSha256: sha256Attestation(after),
761
+ },
762
+ },
763
+ }
764
+ : {}),
765
+ ...(input.aiRepair
766
+ ? {
767
+ aiEvidence: {
768
+ provider: input.aiRepair.provider,
769
+ model: input.aiRepair.model,
770
+ context: input.aiRepair.context,
771
+ suggestion: input.aiRepair.suggestion,
772
+ },
773
+ }
774
+ : {}),
775
+ signer: null,
776
+ signature: null,
777
+ };
778
+ const signed = signDetached(canonicalReceipt(receipt));
779
+ receipt.signature = signed.signature;
780
+ receipt.signer = { publicKey: signed.publicKeyPem, algorithm: "ed25519" };
781
+ return receipt;
782
+ }
783
+ function writeLifecycleRepairReceipt(repo, receipt) {
784
+ const file = receiptPath(repo);
785
+ fs.mkdirSync(path.dirname(file), { recursive: true });
786
+ fs.writeFileSync(file, JSON.stringify(receipt, null, 2) + "\n");
787
+ return file;
788
+ }
789
+ async function executeDeterministicRepairCandidate(repo, before, candidate, options) {
790
+ const startedAt = new Date().toISOString();
791
+ cleanPreviousRepairOutputs(repo);
792
+ const result = (receipt, explanation, afterFile = null, patchFile = null) => {
793
+ const receiptFile = writeLifecycleRepairReceipt(repo, receipt);
794
+ return {
795
+ schema: "bootproof/repair-result/v1",
796
+ repaired: receipt.verified,
797
+ failureClass: candidate.failureClass,
798
+ repairId: candidate.id,
799
+ receiptPath: relativeOutput(repo, receiptFile),
800
+ patchPath: relativeOutput(repo, patchFile),
801
+ afterAttestationPath: relativeOutput(repo, afterFile),
802
+ explanation,
803
+ };
804
+ };
805
+ const safetyErrors = [candidate.action, ...(candidate.followUpActions ?? [])]
806
+ .flatMap(action => validateRepairAction(action).errors);
807
+ if (safetyErrors.length) {
808
+ const explanation = `Deterministic repair candidate was blocked by the safety validator: ${[...new Set(safetyErrors)].join("; ")}`;
809
+ return result(buildLifecycleRepairReceipt({
810
+ repo,
811
+ before,
812
+ candidate,
813
+ createdAt: startedAt,
814
+ applyResult: { status: "failed", exitCode: null, filesChanged: [], evidence: explanation },
815
+ explanation,
816
+ }), explanation);
817
+ }
818
+ const provider = options.provider ?? before.plan.provider;
819
+ if (candidate.action.actionType === "instruction") {
820
+ const explanation = `Deterministic instruction recorded: ${candidate.action.instruction}`;
821
+ return result(buildLifecycleRepairReceipt({
822
+ repo,
823
+ before,
824
+ candidate,
825
+ createdAt: startedAt,
826
+ applyResult: { status: "not_applied", exitCode: null, filesChanged: [], evidence: null },
827
+ explanation,
828
+ }), explanation);
829
+ }
830
+ if (candidate.action.actionType === "patch") {
831
+ const patch = candidate.action.patch;
832
+ if (!patch || !candidate.fileChanges?.length) {
833
+ throw new Error("validated patch repair is missing its exact file changes");
834
+ }
835
+ const patchFile = path.join(repo, ".bootproof", `repair-${candidate.id}.patch`);
836
+ fs.mkdirSync(path.dirname(patchFile), { recursive: true });
837
+ fs.writeFileSync(patchFile, patch.content);
838
+ if (provider === "local" && !options.unsafeLocal) {
839
+ const explanation = "The repair patch was not tested because the required BootProof rerun would execute repository code locally; rerun with --unsafe-local after review.";
840
+ return result(buildLifecycleRepairReceipt({
841
+ repo,
842
+ before,
843
+ candidate,
844
+ createdAt: startedAt,
845
+ applyResult: { status: "not_applied", exitCode: null, filesChanged: [], evidence: null },
846
+ explanation,
847
+ }), explanation, null, patchFile);
848
+ }
849
+ if (options.actionApproved !== true) {
850
+ const explanation = "Repair patch was not approved. The working tree was not modified.";
851
+ return result(buildLifecycleRepairReceipt({
852
+ repo,
853
+ before,
854
+ candidate,
855
+ createdAt: startedAt,
856
+ applyResult: { status: "not_applied", exitCode: null, filesChanged: [], evidence: null },
857
+ explanation,
858
+ }), explanation, null, patchFile);
859
+ }
860
+ const approvedAt = new Date().toISOString();
861
+ const { root, sandbox, composeProjectName } = copyToSandbox(repo);
862
+ let patchApplied = false;
863
+ let afterOutcome = null;
864
+ let rerunError = "";
865
+ try {
866
+ for (const change of candidate.fileChanges) {
867
+ const target = path.join(sandbox, normalizedRelative(change.path));
868
+ const current = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : null;
869
+ if (current !== change.before) {
870
+ throw new Error(`repair patch preimage changed for ${change.path}`);
871
+ }
872
+ }
873
+ writeChanges(sandbox, candidate.fileChanges);
874
+ patchApplied = true;
875
+ afterOutcome = await up(sandbox, {
876
+ provider,
877
+ unsafeLocal: options.unsafeLocal,
878
+ dryRun: false,
879
+ timeoutMs: options.timeoutMs,
880
+ install: true,
881
+ port: options.port,
882
+ remoteSource: options.remoteSource,
883
+ environment: { COMPOSE_PROJECT_NAME: composeProjectName },
884
+ });
885
+ }
886
+ catch (error) {
887
+ rerunError = error instanceof Error ? error.message : String(error);
888
+ }
889
+ finally {
890
+ await cleanupServices(afterOutcome, buildExecutionEnv({ COMPOSE_PROJECT_NAME: composeProjectName }));
891
+ fs.rmSync(root, { recursive: true, force: true });
892
+ }
893
+ const after = afterOutcome?.attestation ?? null;
894
+ let afterFile = null;
895
+ if (after) {
896
+ afterFile = afterAttestationPath(repo);
897
+ fs.mkdirSync(path.dirname(afterFile), { recursive: true });
898
+ fs.writeFileSync(afterFile, JSON.stringify(after, null, 2) + "\n");
899
+ }
900
+ const verified = Boolean(after?.result.booted && after.result.healthVerified);
901
+ const progressed = repairProgressed(candidate.failureClass, after);
902
+ const redactedError = redactText(rerunError);
903
+ const explanation = !patchApplied
904
+ ? `Repair patch could not be applied in the sandbox: ${redactedError.text || "unknown failure"}.`
905
+ : verified
906
+ ? `Repair patch was tested in the sandbox and BootProof observed verified health: ${after.result.healthObservation}.`
907
+ : progressed
908
+ ? `Repair patch was tested in the sandbox; BootProof progressed from ${candidate.failureClass} to ${after.result.failureClass}.`
909
+ : "Repair patch was tested in the sandbox, but the BootProof rerun did not verify boot or reach a different failure class.";
910
+ return result(buildLifecycleRepairReceipt({
911
+ repo,
912
+ before,
913
+ candidate,
914
+ createdAt: startedAt,
915
+ approvedAt,
916
+ ...(patchApplied ? { appliedAt: new Date().toISOString() } : {}),
917
+ applyResult: {
918
+ status: patchApplied ? "applied" : "failed",
919
+ exitCode: patchApplied ? 0 : null,
920
+ filesChanged: patchApplied ? candidate.fileChanges.map(change => change.path) : [],
921
+ evidence: redactedError.text || null,
922
+ },
923
+ after,
924
+ explanation,
925
+ redactionsApplied: redactedError.applied,
926
+ }), explanation, afterFile, patchFile);
927
+ }
928
+ const command = candidate.action.command;
929
+ if (!command)
930
+ throw new Error("validated command repair is missing its exact command");
931
+ if (provider === "local" && !options.unsafeLocal) {
932
+ const explanation = "The repair command was not run because the required BootProof rerun would execute repository code locally; rerun with --unsafe-local after review.";
933
+ return result(buildLifecycleRepairReceipt({
934
+ repo,
935
+ before,
936
+ candidate,
937
+ createdAt: startedAt,
938
+ applyResult: { status: "not_applied", exitCode: null, filesChanged: [], evidence: null },
939
+ explanation,
940
+ }), explanation);
941
+ }
942
+ if (options.actionApproved !== true && options.commandApproved !== true) {
943
+ const explanation = "Repair command was not approved. No command was executed.";
944
+ return result(buildLifecycleRepairReceipt({
945
+ repo,
946
+ before,
947
+ candidate,
948
+ createdAt: startedAt,
949
+ applyResult: { status: "not_applied", exitCode: null, filesChanged: [], evidence: null },
950
+ explanation,
951
+ }), explanation);
952
+ }
953
+ const approvedAt = new Date().toISOString();
954
+ const commandResult = await runStructuredRepairCommand(command, repo);
955
+ const { root, sandbox, composeProjectName } = copyToSandbox(repo);
956
+ let afterOutcome = null;
957
+ let rerunError = "";
958
+ try {
959
+ afterOutcome = await up(sandbox, {
960
+ provider,
961
+ unsafeLocal: options.unsafeLocal,
962
+ dryRun: false,
963
+ timeoutMs: options.timeoutMs,
964
+ install: true,
965
+ port: options.port,
966
+ remoteSource: options.remoteSource,
967
+ environment: { COMPOSE_PROJECT_NAME: composeProjectName },
968
+ });
969
+ }
970
+ catch (error) {
971
+ rerunError = error instanceof Error ? error.message : String(error);
972
+ }
973
+ finally {
974
+ await cleanupServices(afterOutcome, buildExecutionEnv({ COMPOSE_PROJECT_NAME: composeProjectName }));
975
+ fs.rmSync(root, { recursive: true, force: true });
976
+ }
977
+ const after = afterOutcome?.attestation ?? null;
978
+ let afterFile = null;
979
+ if (after) {
980
+ afterFile = afterAttestationPath(repo);
981
+ fs.mkdirSync(path.dirname(afterFile), { recursive: true });
982
+ fs.writeFileSync(afterFile, JSON.stringify(after, null, 2) + "\n");
983
+ }
984
+ const commandApplied = commandResult.exitCode === 0;
985
+ const verified = Boolean(after?.result.booted && after.result.healthVerified);
986
+ const progressed = repairProgressed(candidate.failureClass, after);
987
+ const redactedEvidence = redactText([commandResult.evidence, rerunError].filter(Boolean).join("\n"));
988
+ const explanation = verified
989
+ ? commandApplied
990
+ ? `Repair command applied and BootProof observed verified health: ${after.result.healthObservation}.`
991
+ : `Repair command failed with exit ${commandResult.exitCode ?? "unknown"}, but the BootProof rerun observed verified health: ${after.result.healthObservation}.`
992
+ : progressed
993
+ ? commandApplied
994
+ ? `Repair command applied; BootProof progressed from ${candidate.failureClass} to ${after.result.failureClass}.`
995
+ : `Repair command failed with exit ${commandResult.exitCode ?? "unknown"}, but BootProof progressed from ${candidate.failureClass} to ${after.result.failureClass}.`
996
+ : commandApplied
997
+ ? "Repair command completed, but the BootProof rerun did not verify boot or reach a different failure class."
998
+ : `Repair command failed with exit ${commandResult.exitCode ?? "unknown"}; the BootProof rerun did not verify boot or reach a different failure class.`;
999
+ const receipt = buildLifecycleRepairReceipt({
1000
+ repo,
1001
+ before,
1002
+ candidate,
1003
+ createdAt: startedAt,
1004
+ approvedAt,
1005
+ ...(commandApplied ? { appliedAt: new Date().toISOString() } : {}),
1006
+ applyResult: {
1007
+ status: commandApplied ? "applied" : "failed",
1008
+ exitCode: commandResult.exitCode,
1009
+ filesChanged: [],
1010
+ evidence: redactedEvidence.text || null,
1011
+ },
1012
+ after,
1013
+ explanation,
1014
+ redactionsApplied: [...new Set([
1015
+ ...commandResult.redactionsApplied,
1016
+ ...redactedEvidence.applied,
1017
+ ])],
1018
+ });
1019
+ return result(receipt, explanation, afterFile);
1020
+ }
1021
+ function verifiedAfterRepair(attestation) {
1022
+ return Boolean(attestation?.result.healthVerified &&
1023
+ (attestation.result.booted || attestation.verificationMode === "external-health"));
1024
+ }
1025
+ function writeRepairAfterAttestation(repo, attestation) {
1026
+ if (!attestation)
1027
+ return null;
1028
+ const file = afterAttestationPath(repo);
1029
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1030
+ fs.writeFileSync(file, JSON.stringify(attestation, null, 2) + "\n");
1031
+ return file;
1032
+ }
1033
+ async function rerunAfterAiRepair(sandbox, before, provider, options, composeProjectName) {
1034
+ try {
1035
+ if (before.verificationMode === "external-health" && before.externalHealthUrl) {
1036
+ const after = await buildExternalHealthAttestation(sandbox, before.externalHealthUrl, options.timeoutMs);
1037
+ return { after, outcome: null, error: "" };
1038
+ }
1039
+ const outcome = await up(sandbox, {
1040
+ provider,
1041
+ unsafeLocal: options.unsafeLocal,
1042
+ dryRun: false,
1043
+ timeoutMs: options.timeoutMs,
1044
+ install: true,
1045
+ port: options.port,
1046
+ remoteSource: options.remoteSource,
1047
+ environment: { COMPOSE_PROJECT_NAME: composeProjectName },
1048
+ });
1049
+ return { after: outcome.attestation, outcome, error: "" };
1050
+ }
1051
+ catch (error) {
1052
+ return {
1053
+ after: null,
1054
+ outcome: null,
1055
+ error: error instanceof Error ? error.message : String(error),
1056
+ };
1057
+ }
1058
+ }
1059
+ export async function executeAiSuggestedRepair(repoPath, before, action, options) {
1060
+ const repo = path.resolve(repoPath);
1061
+ const startedAt = new Date().toISOString();
1062
+ const failureClass = before.result.failureClass;
1063
+ if (!failureClass || before.result.healthVerified || before.result.booted || !verifySignature(before)) {
1064
+ throw new Error("AI repair requires a signature-valid classified failed attestation.");
1065
+ }
1066
+ if (action.source !== "ai_suggested" || action.deterministic) {
1067
+ throw new Error("AI repair execution requires an ai_suggested action.");
1068
+ }
1069
+ const validation = validateRepairAction(action);
1070
+ if (!validation.valid) {
1071
+ throw new Error(`AI suggestion was blocked by BootProof safety policy: ${validation.errors.join("; ")}`);
1072
+ }
1073
+ cleanPreviousRepairOutputs(repo);
1074
+ const candidate = {
1075
+ id: `ai-suggested-${failureClass}-${sha256Text(JSON.stringify(action)).slice(0, 12)}`,
1076
+ failureClass,
1077
+ action,
1078
+ };
1079
+ const result = (receipt, explanation, afterFile = null, patchFile = null) => {
1080
+ const receiptFile = writeLifecycleRepairReceipt(repo, receipt);
1081
+ return {
1082
+ schema: "bootproof/repair-result/v1",
1083
+ repaired: receipt.verified,
1084
+ failureClass,
1085
+ repairId: candidate.id,
1086
+ receiptPath: relativeOutput(repo, receiptFile),
1087
+ patchPath: relativeOutput(repo, patchFile),
1088
+ afterAttestationPath: relativeOutput(repo, afterFile),
1089
+ explanation,
1090
+ };
1091
+ };
1092
+ const receiptFor = (input) => buildLifecycleRepairReceipt({
1093
+ repo,
1094
+ before,
1095
+ candidate,
1096
+ createdAt: startedAt,
1097
+ aiRepair: options.aiRepair ?? null,
1098
+ ...input,
1099
+ });
1100
+ const provider = options.provider ?? before.plan.provider;
1101
+ const localRerunRequiresAcknowledgement = before.verificationMode !== "external-health" &&
1102
+ provider === "local" &&
1103
+ !options.unsafeLocal;
1104
+ if (action.actionType === "instruction") {
1105
+ const approved = options.actionApproved === true;
1106
+ const explanation = approved
1107
+ ? `AI-suggested instruction was approved for manual use but was not executed: ${action.instruction}`
1108
+ : "AI-suggested instruction was not approved. No command or patch was executed.";
1109
+ return result(receiptFor({
1110
+ ...(approved ? { approvedAt: new Date().toISOString() } : {}),
1111
+ applyResult: { status: "not_applied", exitCode: null, filesChanged: [], evidence: null },
1112
+ explanation,
1113
+ redactionsApplied: [],
1114
+ }), explanation);
1115
+ }
1116
+ if (action.actionType === "patch") {
1117
+ const patch = action.patch;
1118
+ if (!patch)
1119
+ throw new Error("validated AI patch suggestion is missing its exact patch");
1120
+ const patchFile = path.join(repo, ".bootproof", `repair-${candidate.id}.patch`);
1121
+ if (localRerunRequiresAcknowledgement) {
1122
+ const explanation = "The AI-suggested patch was not tested because the required BootProof rerun would execute repository code locally; rerun with --unsafe-local after review.";
1123
+ return result(receiptFor({
1124
+ applyResult: { status: "not_applied", exitCode: null, filesChanged: [], evidence: null },
1125
+ explanation,
1126
+ redactionsApplied: [],
1127
+ }), explanation);
1128
+ }
1129
+ if (options.actionApproved !== true) {
1130
+ const explanation = "AI-suggested patch was not approved. The working tree was not modified.";
1131
+ return result(receiptFor({
1132
+ applyResult: { status: "not_applied", exitCode: null, filesChanged: [], evidence: null },
1133
+ explanation,
1134
+ redactionsApplied: [],
1135
+ }), explanation);
1136
+ }
1137
+ const approvedAt = new Date().toISOString();
1138
+ fs.mkdirSync(path.dirname(patchFile), { recursive: true });
1139
+ fs.writeFileSync(patchFile, patch.content);
1140
+ const { root, sandbox, composeProjectName } = copyToSandbox(repo);
1141
+ const sandboxPatch = path.join(root, "ai-suggested.patch");
1142
+ fs.writeFileSync(sandboxPatch, patch.content);
1143
+ let patchResult = {
1144
+ exitCode: null,
1145
+ evidence: "",
1146
+ redactionsApplied: [],
1147
+ };
1148
+ let after = null;
1149
+ let outcome = null;
1150
+ let rerunError = "";
1151
+ try {
1152
+ for (const file of patch.files)
1153
+ assertRepairTargetPath(sandbox, file);
1154
+ const check = await runStructuredRepairCommand(createRepairCommand("git", ["apply", "--check", sandboxPatch]), sandbox);
1155
+ patchResult = check.exitCode === 0
1156
+ ? await runStructuredRepairCommand(createRepairCommand("git", ["apply", sandboxPatch]), sandbox)
1157
+ : check;
1158
+ if (patchResult.exitCode === 0) {
1159
+ const rerun = await rerunAfterAiRepair(sandbox, before, provider, options, composeProjectName);
1160
+ after = rerun.after;
1161
+ outcome = rerun.outcome;
1162
+ rerunError = rerun.error;
1163
+ }
1164
+ }
1165
+ catch (error) {
1166
+ rerunError = error instanceof Error ? error.message : String(error);
1167
+ }
1168
+ finally {
1169
+ await cleanupServices(outcome, buildExecutionEnv({ COMPOSE_PROJECT_NAME: composeProjectName }));
1170
+ fs.rmSync(root, { recursive: true, force: true });
1171
+ }
1172
+ const afterFile = writeRepairAfterAttestation(repo, after);
1173
+ const patchApplied = patchResult.exitCode === 0;
1174
+ const progressed = repairProgressed(failureClass, after);
1175
+ const redacted = redactText([patchResult.evidence, rerunError].filter(Boolean).join("\n"));
1176
+ const explanation = verifiedAfterRepair(after)
1177
+ ? `AI-suggested patch was tested in a sandbox and BootProof independently verified health: ${after.result.healthObservation}.`
1178
+ : progressed
1179
+ ? `AI-suggested patch was tested in a sandbox; BootProof progressed from ${failureClass} to ${after.result.failureClass}.`
1180
+ : patchApplied
1181
+ ? "AI-suggested patch was tested in a sandbox, but BootProof did not verify boot or reach a different failure class."
1182
+ : `AI-suggested patch was blocked or failed in the sandbox: ${redacted.text || "git apply failed"}.`;
1183
+ return result(receiptFor({
1184
+ approvedAt,
1185
+ ...(patchApplied ? { appliedAt: new Date().toISOString() } : {}),
1186
+ applyResult: {
1187
+ status: patchApplied ? "applied" : "failed",
1188
+ exitCode: patchResult.exitCode,
1189
+ filesChanged: patchApplied ? patch.files : [],
1190
+ evidence: redacted.text || null,
1191
+ },
1192
+ after,
1193
+ explanation,
1194
+ redactionsApplied: [...new Set([...patchResult.redactionsApplied, ...redacted.applied])],
1195
+ }), explanation, afterFile, patchFile);
1196
+ }
1197
+ const command = action.command;
1198
+ if (!command)
1199
+ throw new Error("validated AI command suggestion is missing its exact command");
1200
+ if (localRerunRequiresAcknowledgement) {
1201
+ const explanation = "The AI-suggested command was not run because the required BootProof rerun would execute repository code locally; rerun with --unsafe-local after review.";
1202
+ return result(receiptFor({
1203
+ applyResult: { status: "not_applied", exitCode: null, filesChanged: [], evidence: null },
1204
+ explanation,
1205
+ redactionsApplied: [],
1206
+ }), explanation);
1207
+ }
1208
+ if (options.actionApproved !== true) {
1209
+ const explanation = "AI-suggested command was not approved. No command was executed.";
1210
+ return result(receiptFor({
1211
+ applyResult: { status: "not_applied", exitCode: null, filesChanged: [], evidence: null },
1212
+ explanation,
1213
+ redactionsApplied: [],
1214
+ }), explanation);
1215
+ }
1216
+ const approvedAt = new Date().toISOString();
1217
+ const commandResult = await runStructuredRepairCommand(command, repo);
1218
+ const { root, sandbox, composeProjectName } = copyToSandbox(repo);
1219
+ let after = null;
1220
+ let outcome = null;
1221
+ let rerunError = "";
1222
+ try {
1223
+ const rerun = await rerunAfterAiRepair(sandbox, before, provider, options, composeProjectName);
1224
+ after = rerun.after;
1225
+ outcome = rerun.outcome;
1226
+ rerunError = rerun.error;
1227
+ }
1228
+ finally {
1229
+ await cleanupServices(outcome, buildExecutionEnv({ COMPOSE_PROJECT_NAME: composeProjectName }));
1230
+ fs.rmSync(root, { recursive: true, force: true });
1231
+ }
1232
+ const afterFile = writeRepairAfterAttestation(repo, after);
1233
+ const commandApplied = commandResult.exitCode === 0;
1234
+ const progressed = repairProgressed(failureClass, after);
1235
+ const redacted = redactText([commandResult.evidence, rerunError].filter(Boolean).join("\n"));
1236
+ const explanation = verifiedAfterRepair(after)
1237
+ ? commandApplied
1238
+ ? `AI-suggested command completed and BootProof independently verified health: ${after.result.healthObservation}.`
1239
+ : `AI-suggested command failed with exit ${commandResult.exitCode ?? "unknown"}, but BootProof independently verified health: ${after.result.healthObservation}.`
1240
+ : progressed
1241
+ ? `AI-suggested command ${commandApplied ? "completed" : "failed"}; BootProof progressed from ${failureClass} to ${after.result.failureClass}.`
1242
+ : commandApplied
1243
+ ? "AI-suggested command completed, but BootProof did not verify boot or reach a different failure class."
1244
+ : `AI-suggested command failed with exit ${commandResult.exitCode ?? "unknown"}; BootProof did not verify boot or reach a different failure class.`;
1245
+ return result(receiptFor({
1246
+ approvedAt,
1247
+ ...(commandApplied ? { appliedAt: new Date().toISOString() } : {}),
1248
+ applyResult: {
1249
+ status: commandApplied ? "applied" : "failed",
1250
+ exitCode: commandResult.exitCode,
1251
+ filesChanged: [],
1252
+ evidence: redacted.text || null,
1253
+ },
1254
+ after,
1255
+ explanation,
1256
+ redactionsApplied: [...new Set([...commandResult.redactionsApplied, ...redacted.applied])],
1257
+ }), explanation, afterFile);
1258
+ }
575
1259
  async function cleanupServices(outcome, env) {
576
1260
  if (!outcome)
577
1261
  return;
@@ -609,6 +1293,12 @@ export function applyVerifiedRepair(repoPath, receiptFile = path.join(repoPath,
609
1293
  if (receipt.schema !== "bootproof/repair-receipt/v1" || !verifyRepairReceipt(receipt)) {
610
1294
  return fail("repair receipt signature is invalid; no files were written");
611
1295
  }
1296
+ if (!receipt.verified) {
1297
+ return fail("repair receipt is not verified; no repository files were written");
1298
+ }
1299
+ if (!receipt.repair) {
1300
+ return fail("repair receipt contains no verified repository file changes to apply");
1301
+ }
612
1302
  const changes = receipt.repair.fileChanges;
613
1303
  if (!Array.isArray(changes) || changes.length === 0) {
614
1304
  return fail("verified repair has no repository file changes to apply");
@@ -708,6 +1398,25 @@ export async function repairRepo(repoPath, options) {
708
1398
  const existingBefore = signedFailedAttestation(repo, options.provider);
709
1399
  const freshBefore = freshFailedAttestation(repo, options.provider);
710
1400
  const provider = options.provider ?? existingBefore?.plan.provider ?? "docker";
1401
+ if (existingBefore) {
1402
+ const candidate = deterministicRepairCandidateFor(existingBefore, { repoPath: repo });
1403
+ if (candidate) {
1404
+ return await executeDeterministicRepairCandidate(repo, existingBefore, candidate, options);
1405
+ }
1406
+ if (!(REGISTRY[existingBefore.result.failureClass] ?? []).length) {
1407
+ const failureClass = existingBefore.result.failureClass;
1408
+ return {
1409
+ schema: "bootproof/repair-result/v1",
1410
+ repaired: false,
1411
+ failureClass,
1412
+ repairId: null,
1413
+ receiptPath: null,
1414
+ patchPath: null,
1415
+ afterAttestationPath: null,
1416
+ explanation: `No verified deterministic remediation is known for ${failureClass} yet.`,
1417
+ };
1418
+ }
1419
+ }
711
1420
  if (provider === "local" && !options.unsafeLocal) {
712
1421
  return {
713
1422
  schema: "bootproof/repair-result/v1",
@@ -773,7 +1482,7 @@ export async function repairRepo(repoPath, options) {
773
1482
  receiptPath: null,
774
1483
  patchPath: null,
775
1484
  afterAttestationPath: null,
776
- explanation: `no verified remediation is known for ${failureClass} yet`,
1485
+ explanation: `No verified deterministic remediation is known for ${failureClass} yet.`,
777
1486
  };
778
1487
  }
779
1488
  let attemptedId = null;
@@ -851,7 +1560,7 @@ export async function repairRepo(repoPath, options) {
851
1560
  };
852
1561
  }
853
1562
  finally {
854
- await cleanupServices(lastOutcome, minimalEnv({ COMPOSE_PROJECT_NAME: composeProjectName }));
1563
+ await cleanupServices(lastOutcome, buildExecutionEnv({ COMPOSE_PROJECT_NAME: composeProjectName }));
855
1564
  fs.rmSync(root, { recursive: true, force: true });
856
1565
  }
857
1566
  }