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
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { redactJsonValue, redactText } from "./redact.js";
|
|
2
|
+
import { ACTION_RISK_LEVELS, buildAiSuggestedRepairAction, } from "./repair-safety.js";
|
|
3
|
+
export const AI_KEY_REQUIRED_MESSAGE = "AI-assisted repair is optional and requires your own OPENAI_API_KEY or ANTHROPIC_API_KEY. BootProof up, explain, plan-agent, verify-url, and deterministic fix work without AI.";
|
|
4
|
+
const SUGGESTION_KEYS = new Set([
|
|
5
|
+
"schema",
|
|
6
|
+
"confidence",
|
|
7
|
+
"failure_class",
|
|
8
|
+
"suggested_action_type",
|
|
9
|
+
"suggested_command",
|
|
10
|
+
"suggested_patch",
|
|
11
|
+
"explanation_for_user",
|
|
12
|
+
"risk_level",
|
|
13
|
+
"requires_human_approval",
|
|
14
|
+
"why_this_is_safe",
|
|
15
|
+
"what_to_check_after",
|
|
16
|
+
]);
|
|
17
|
+
const COMMAND_KEYS = new Set(["executable", "args", "display"]);
|
|
18
|
+
const PATCH_KEYS = new Set(["format", "content", "files"]);
|
|
19
|
+
const ACTION_TYPES = new Set(["command", "patch", "instruction"]);
|
|
20
|
+
const FAILURE_EVIDENCE_LIMIT = 8000;
|
|
21
|
+
const STEP_EVIDENCE_LIMIT = 2000;
|
|
22
|
+
const RESPONSE_LIMIT = 256_000;
|
|
23
|
+
const AI_REPAIR_JSON_SCHEMA = {
|
|
24
|
+
type: "object",
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
required: [...SUGGESTION_KEYS],
|
|
27
|
+
properties: {
|
|
28
|
+
schema: { const: "bootproof/ai-repair-suggestion/v1" },
|
|
29
|
+
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
30
|
+
failure_class: { type: "string", minLength: 1 },
|
|
31
|
+
suggested_action_type: { enum: ["command", "patch", "instruction"] },
|
|
32
|
+
suggested_command: {
|
|
33
|
+
anyOf: [
|
|
34
|
+
{
|
|
35
|
+
type: "object",
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
required: ["executable", "args", "display"],
|
|
38
|
+
properties: {
|
|
39
|
+
executable: { type: "string", minLength: 1 },
|
|
40
|
+
args: { type: "array", items: { type: "string" } },
|
|
41
|
+
display: { type: "string", minLength: 1 },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{ type: "null" },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
suggested_patch: {
|
|
48
|
+
anyOf: [
|
|
49
|
+
{
|
|
50
|
+
type: "object",
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
required: ["format", "content", "files"],
|
|
53
|
+
properties: {
|
|
54
|
+
format: { const: "unified-diff" },
|
|
55
|
+
content: { type: "string", minLength: 1 },
|
|
56
|
+
files: {
|
|
57
|
+
type: "array",
|
|
58
|
+
minItems: 1,
|
|
59
|
+
uniqueItems: true,
|
|
60
|
+
items: { type: "string", minLength: 1 },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{ type: "null" },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
explanation_for_user: { type: "string", minLength: 1 },
|
|
68
|
+
risk_level: { enum: [...ACTION_RISK_LEVELS] },
|
|
69
|
+
requires_human_approval: { const: true },
|
|
70
|
+
why_this_is_safe: { type: "string", minLength: 1 },
|
|
71
|
+
what_to_check_after: { type: "string", minLength: 1 },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
function isRecord(value) {
|
|
75
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
76
|
+
}
|
|
77
|
+
function exactKeys(value, allowed) {
|
|
78
|
+
return Object.keys(value).filter(key => !allowed.has(key));
|
|
79
|
+
}
|
|
80
|
+
function nonEmptyString(value) {
|
|
81
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
82
|
+
}
|
|
83
|
+
function stringArray(value) {
|
|
84
|
+
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
85
|
+
}
|
|
86
|
+
function truncate(value, limit) {
|
|
87
|
+
return value.length <= limit ? value : `${value.slice(0, limit)}\n[truncated]`;
|
|
88
|
+
}
|
|
89
|
+
function patchHeaderFiles(content) {
|
|
90
|
+
const files = new Set();
|
|
91
|
+
for (const match of content.matchAll(/^(?:---|\+\+\+)\s+([^\t\n]+)$/gm)) {
|
|
92
|
+
const raw = match[1].trim();
|
|
93
|
+
if (raw === "/dev/null")
|
|
94
|
+
continue;
|
|
95
|
+
files.add(raw.replace(/^[ab]\//, ""));
|
|
96
|
+
}
|
|
97
|
+
return [...files].sort();
|
|
98
|
+
}
|
|
99
|
+
export function resolveAiProvider(env = process.env) {
|
|
100
|
+
const openaiKey = env.OPENAI_API_KEY?.trim();
|
|
101
|
+
const anthropicKey = env.ANTHROPIC_API_KEY?.trim();
|
|
102
|
+
const requested = env.BOOTPROOF_AI_PROVIDER?.trim().toLowerCase();
|
|
103
|
+
if (!openaiKey && !anthropicKey)
|
|
104
|
+
throw new Error(AI_KEY_REQUIRED_MESSAGE);
|
|
105
|
+
if (requested && requested !== "openai" && requested !== "anthropic") {
|
|
106
|
+
throw new Error(`Unsupported BOOTPROOF_AI_PROVIDER: ${requested}. Expected openai or anthropic.`);
|
|
107
|
+
}
|
|
108
|
+
if (requested === "openai" && !openaiKey) {
|
|
109
|
+
throw new Error("BOOTPROOF_AI_PROVIDER=openai requires OPENAI_API_KEY.");
|
|
110
|
+
}
|
|
111
|
+
if (requested === "anthropic" && !anthropicKey) {
|
|
112
|
+
throw new Error("BOOTPROOF_AI_PROVIDER=anthropic requires ANTHROPIC_API_KEY.");
|
|
113
|
+
}
|
|
114
|
+
if ((requested === "openai" || (!requested && openaiKey)) && openaiKey) {
|
|
115
|
+
return {
|
|
116
|
+
provider: "openai",
|
|
117
|
+
apiKey: openaiKey,
|
|
118
|
+
model: env.BOOTPROOF_OPENAI_MODEL?.trim() || "gpt-4.1",
|
|
119
|
+
endpoint: "https://api.openai.com/v1/responses",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
provider: "anthropic",
|
|
124
|
+
apiKey: anthropicKey,
|
|
125
|
+
model: env.BOOTPROOF_ANTHROPIC_MODEL?.trim() || "claude-sonnet-4-6",
|
|
126
|
+
endpoint: "https://api.anthropic.com/v1/messages",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export function buildAiRepairContext(attestation) {
|
|
130
|
+
if (attestation.result.booted ||
|
|
131
|
+
attestation.result.healthVerified ||
|
|
132
|
+
!attestation.result.failureClass) {
|
|
133
|
+
throw new Error("AI repair requires a classified failed attestation.");
|
|
134
|
+
}
|
|
135
|
+
const raw = {
|
|
136
|
+
schema: "bootproof/ai-repair-context/v1",
|
|
137
|
+
failureClass: attestation.result.failureClass,
|
|
138
|
+
verificationMode: attestation.verificationMode,
|
|
139
|
+
bootproofOrchestrated: attestation.bootproofOrchestrated,
|
|
140
|
+
failureEvidence: truncate(attestation.result.failureEvidence ?? "", FAILURE_EVIDENCE_LIMIT),
|
|
141
|
+
explanation: truncate(attestation.result.explanation, STEP_EVIDENCE_LIMIT),
|
|
142
|
+
observedEvidence: attestation.observed.map(observation => ({
|
|
143
|
+
id: observation.id,
|
|
144
|
+
ok: observation.ok,
|
|
145
|
+
observation: truncate(observation.observation, STEP_EVIDENCE_LIMIT),
|
|
146
|
+
...(observation.evidenceHead
|
|
147
|
+
? { evidenceHead: truncate(observation.evidenceHead, STEP_EVIDENCE_LIMIT) }
|
|
148
|
+
: {}),
|
|
149
|
+
...(observation.evidenceTail
|
|
150
|
+
? { evidenceTail: truncate(observation.evidenceTail, STEP_EVIDENCE_LIMIT) }
|
|
151
|
+
: {}),
|
|
152
|
+
...(observation.firstErrorLine ? { firstErrorLine: observation.firstErrorLine } : {}),
|
|
153
|
+
...(observation.firstExceptionLine ? { firstExceptionLine: observation.firstExceptionLine } : {}),
|
|
154
|
+
...(observation.detectedCause ? { detectedCause: observation.detectedCause } : {}),
|
|
155
|
+
})),
|
|
156
|
+
healthEvidence: attestation.result.healthEvidence,
|
|
157
|
+
};
|
|
158
|
+
const redacted = redactJsonValue(raw);
|
|
159
|
+
return {
|
|
160
|
+
...redacted.value,
|
|
161
|
+
redactionsApplied: redacted.applied,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export function validateAiRepairSuggestion(value, expectedFailureClass) {
|
|
165
|
+
if (!isRecord(value))
|
|
166
|
+
throw new Error("AI repair suggestion must be a JSON object.");
|
|
167
|
+
const errors = exactKeys(value, SUGGESTION_KEYS).map(key => `unsupported field: ${key}`);
|
|
168
|
+
for (const key of SUGGESTION_KEYS) {
|
|
169
|
+
if (!(key in value))
|
|
170
|
+
errors.push(`missing field: ${key}`);
|
|
171
|
+
}
|
|
172
|
+
if (value.schema !== "bootproof/ai-repair-suggestion/v1")
|
|
173
|
+
errors.push("invalid AI repair suggestion schema");
|
|
174
|
+
if (typeof value.confidence !== "number" || value.confidence < 0 || value.confidence > 1) {
|
|
175
|
+
errors.push("confidence must be a number from 0 to 1");
|
|
176
|
+
}
|
|
177
|
+
if (!nonEmptyString(value.failure_class))
|
|
178
|
+
errors.push("failure_class must be a non-empty string");
|
|
179
|
+
if (expectedFailureClass && value.failure_class !== expectedFailureClass) {
|
|
180
|
+
errors.push(`failure_class must match ${expectedFailureClass}`);
|
|
181
|
+
}
|
|
182
|
+
if (!ACTION_TYPES.has(value.suggested_action_type))
|
|
183
|
+
errors.push("invalid suggested_action_type");
|
|
184
|
+
if (!ACTION_RISK_LEVELS.includes(value.risk_level))
|
|
185
|
+
errors.push("invalid risk_level");
|
|
186
|
+
if (value.requires_human_approval !== true)
|
|
187
|
+
errors.push("requires_human_approval must be true");
|
|
188
|
+
for (const key of ["explanation_for_user", "why_this_is_safe", "what_to_check_after"]) {
|
|
189
|
+
if (!nonEmptyString(value[key]))
|
|
190
|
+
errors.push(`${key} must be a non-empty string`);
|
|
191
|
+
}
|
|
192
|
+
const command = value.suggested_command;
|
|
193
|
+
if (command !== null) {
|
|
194
|
+
if (!isRecord(command)) {
|
|
195
|
+
errors.push("suggested_command must be an object or null");
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
errors.push(...exactKeys(command, COMMAND_KEYS).map(key => `unsupported command field: ${key}`));
|
|
199
|
+
if (!nonEmptyString(command.executable))
|
|
200
|
+
errors.push("suggested_command.executable must be a non-empty string");
|
|
201
|
+
if (!stringArray(command.args))
|
|
202
|
+
errors.push("suggested_command.args must be a string array");
|
|
203
|
+
if (!nonEmptyString(command.display))
|
|
204
|
+
errors.push("suggested_command.display must be a non-empty string");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const patch = value.suggested_patch;
|
|
208
|
+
if (patch !== null) {
|
|
209
|
+
if (!isRecord(patch)) {
|
|
210
|
+
errors.push("suggested_patch must be an object or null");
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
errors.push(...exactKeys(patch, PATCH_KEYS).map(key => `unsupported patch field: ${key}`));
|
|
214
|
+
if (patch.format !== "unified-diff")
|
|
215
|
+
errors.push("suggested_patch.format must be unified-diff");
|
|
216
|
+
if (!nonEmptyString(patch.content))
|
|
217
|
+
errors.push("suggested_patch.content must be a non-empty string");
|
|
218
|
+
if (!stringArray(patch.files) || patch.files.length === 0) {
|
|
219
|
+
errors.push("suggested_patch.files must be a non-empty string array");
|
|
220
|
+
}
|
|
221
|
+
else if (typeof patch.content === "string") {
|
|
222
|
+
const headers = patchHeaderFiles(patch.content);
|
|
223
|
+
const declared = [...new Set(patch.files)].sort();
|
|
224
|
+
if (headers.join("\n") !== declared.join("\n")) {
|
|
225
|
+
errors.push("suggested_patch.files must exactly match unified diff headers");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (value.suggested_action_type === "command" && (command === null || patch !== null)) {
|
|
231
|
+
errors.push("command suggestions require only suggested_command");
|
|
232
|
+
}
|
|
233
|
+
if (value.suggested_action_type === "patch" && (patch === null || command !== null)) {
|
|
234
|
+
errors.push("patch suggestions require only suggested_patch");
|
|
235
|
+
}
|
|
236
|
+
if (value.suggested_action_type === "instruction" && (command !== null || patch !== null)) {
|
|
237
|
+
errors.push("instruction suggestions cannot contain a command or patch");
|
|
238
|
+
}
|
|
239
|
+
if (errors.length)
|
|
240
|
+
throw new Error(`Invalid AI repair JSON: ${[...new Set(errors)].join("; ")}`);
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
function promptFor(context) {
|
|
244
|
+
return [
|
|
245
|
+
"Return exactly one JSON object matching bootproof/ai-repair-suggestion/v1.",
|
|
246
|
+
"Suggest one smallest safe local action only. Do not claim success.",
|
|
247
|
+
"Never use sudo, shell chaining, redirects, pipe-to-shell, protected .env writes, secret reads, uploads, destructive commands, or invented secrets.",
|
|
248
|
+
"Commands must be structured as executable plus args, and display must exactly render them.",
|
|
249
|
+
"Set requires_human_approval to true. BootProof will independently risk-classify, approve, execute, and verify.",
|
|
250
|
+
"Structured redacted failure evidence:",
|
|
251
|
+
JSON.stringify(context),
|
|
252
|
+
].join("\n");
|
|
253
|
+
}
|
|
254
|
+
function openAiText(payload) {
|
|
255
|
+
if (!isRecord(payload))
|
|
256
|
+
throw new Error("OpenAI returned an invalid response envelope.");
|
|
257
|
+
if (nonEmptyString(payload.output_text))
|
|
258
|
+
return payload.output_text;
|
|
259
|
+
if (Array.isArray(payload.output)) {
|
|
260
|
+
for (const item of payload.output) {
|
|
261
|
+
if (!isRecord(item) || !Array.isArray(item.content))
|
|
262
|
+
continue;
|
|
263
|
+
for (const content of item.content) {
|
|
264
|
+
if (isRecord(content) && content.type === "output_text" && nonEmptyString(content.text)) {
|
|
265
|
+
return content.text;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
throw new Error("OpenAI response did not contain JSON output text.");
|
|
271
|
+
}
|
|
272
|
+
function anthropicText(payload) {
|
|
273
|
+
if (!isRecord(payload) || !Array.isArray(payload.content)) {
|
|
274
|
+
throw new Error("Anthropic returned an invalid response envelope.");
|
|
275
|
+
}
|
|
276
|
+
const text = payload.content
|
|
277
|
+
.filter(item => isRecord(item) && item.type === "text" && typeof item.text === "string")
|
|
278
|
+
.map(item => item.text)
|
|
279
|
+
.join("");
|
|
280
|
+
if (!text.trim())
|
|
281
|
+
throw new Error("Anthropic response did not contain JSON output text.");
|
|
282
|
+
return text;
|
|
283
|
+
}
|
|
284
|
+
async function responseJson(response) {
|
|
285
|
+
const text = await response.text();
|
|
286
|
+
if (text.length > RESPONSE_LIMIT)
|
|
287
|
+
throw new Error("AI provider response exceeded BootProof's size limit.");
|
|
288
|
+
if (!response.ok) {
|
|
289
|
+
const safe = redactText(text).text.slice(0, 1000);
|
|
290
|
+
throw new Error(`AI provider request failed with HTTP ${response.status}${safe ? `: ${safe}` : ""}`);
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
return JSON.parse(text);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
throw new Error("AI provider returned a non-JSON response envelope.");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
export async function requestAiRepairSuggestion(attestation, options = {}) {
|
|
300
|
+
const config = resolveAiProvider(options.env);
|
|
301
|
+
const context = buildAiRepairContext(attestation);
|
|
302
|
+
const prompt = promptFor(context);
|
|
303
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
304
|
+
const controller = new AbortController();
|
|
305
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 30_000);
|
|
306
|
+
try {
|
|
307
|
+
const response = config.provider === "openai"
|
|
308
|
+
? await fetchImpl(config.endpoint, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: {
|
|
311
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
312
|
+
"content-type": "application/json",
|
|
313
|
+
},
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
model: config.model,
|
|
316
|
+
store: false,
|
|
317
|
+
instructions: "You are a repair suggestion generator. Output only the requested strict JSON object.",
|
|
318
|
+
input: prompt,
|
|
319
|
+
text: {
|
|
320
|
+
format: {
|
|
321
|
+
type: "json_schema",
|
|
322
|
+
name: "bootproof_ai_repair_suggestion",
|
|
323
|
+
strict: true,
|
|
324
|
+
schema: AI_REPAIR_JSON_SCHEMA,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
signal: controller.signal,
|
|
329
|
+
})
|
|
330
|
+
: await fetchImpl(config.endpoint, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: {
|
|
333
|
+
"anthropic-version": "2023-06-01",
|
|
334
|
+
"content-type": "application/json",
|
|
335
|
+
"x-api-key": config.apiKey,
|
|
336
|
+
},
|
|
337
|
+
body: JSON.stringify({
|
|
338
|
+
model: config.model,
|
|
339
|
+
max_tokens: 2048,
|
|
340
|
+
system: "Output only one strict JSON object matching the requested schema.",
|
|
341
|
+
messages: [{ role: "user", content: prompt }],
|
|
342
|
+
}),
|
|
343
|
+
signal: controller.signal,
|
|
344
|
+
});
|
|
345
|
+
const envelope = await responseJson(response);
|
|
346
|
+
const output = config.provider === "openai" ? openAiText(envelope) : anthropicText(envelope);
|
|
347
|
+
let parsed;
|
|
348
|
+
try {
|
|
349
|
+
parsed = JSON.parse(output);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
throw new Error("Invalid AI repair JSON: provider output was not exactly one JSON object.");
|
|
353
|
+
}
|
|
354
|
+
const suggestion = validateAiRepairSuggestion(parsed, context.failureClass);
|
|
355
|
+
const action = buildAiSuggestedRepairAction({
|
|
356
|
+
actionType: suggestion.suggested_action_type,
|
|
357
|
+
mutationScope: suggestion.suggested_action_type === "patch" ? "repo_only" : "none",
|
|
358
|
+
riskLevel: suggestion.risk_level,
|
|
359
|
+
requiresApproval: true,
|
|
360
|
+
command: suggestion.suggested_command,
|
|
361
|
+
patch: suggestion.suggested_patch,
|
|
362
|
+
instruction: suggestion.suggested_action_type === "instruction"
|
|
363
|
+
? suggestion.explanation_for_user
|
|
364
|
+
: null,
|
|
365
|
+
explanation: suggestion.explanation_for_user,
|
|
366
|
+
evidenceRefs: [".bootproof/attestation.json"],
|
|
367
|
+
verificationStep: suggestion.what_to_check_after,
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
provider: config.provider,
|
|
371
|
+
model: config.model,
|
|
372
|
+
context,
|
|
373
|
+
suggestion,
|
|
374
|
+
action,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
clearTimeout(timer);
|
|
379
|
+
}
|
|
380
|
+
}
|