bootproof 0.3.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 +840 -152
- 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 +730 -46
- package/dist/diagnosis.js +101 -16
- package/dist/diff.d.ts +29 -0
- package/dist/diff.js +569 -0
- package/dist/exec.d.ts +30 -2
- package/dist/exec.js +329 -51
- package/dist/external-health.d.ts +16 -0
- package/dist/external-health.js +214 -0
- package/dist/infer.js +238 -39
- package/dist/plan.js +2 -0
- package/dist/proof.d.ts +78 -2
- package/dist/proof.js +265 -12
- 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.js +3 -3
- 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 +43 -11
- package/dist/repair.js +716 -7
- package/dist/run.d.ts +3 -0
- package/dist/run.js +218 -41
- package/dist/sbom.d.ts +22 -0
- package/dist/sbom.js +99 -0
- package/dist/taxonomy.d.ts +8 -3
- package/dist/taxonomy.js +404 -8
- package/dist/types.d.ts +40 -1
- package/docs/AGENT_IN_THE_LOOP.md +171 -0
- package/docs/AGENT_RUN_RECEIPTS.md +38 -0
- package/docs/CI_ACTION.md +67 -2
- package/docs/DETERMINISTIC_REPAIR_SAFETY_MODEL.md +705 -0
- package/docs/FAILURE_TAXONOMY.md +28 -1
- package/docs/HONESTY_CONTRACT.md +34 -12
- package/docs/LAUNCH_PLAYBOOK.md +232 -0
- package/docs/REAL_WORLD_FIXTURES.md +105 -0
- package/docs/REGISTRY.md +48 -28
- package/docs/REPAIR_RECEIPT.md +54 -8
- 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 +10 -6
package/dist/infer.js
CHANGED
|
@@ -42,6 +42,12 @@ const REPO_COMPOSE_FILES = [
|
|
|
42
42
|
"docker/docker-compose.yml",
|
|
43
43
|
"docker/docker-compose.yaml",
|
|
44
44
|
];
|
|
45
|
+
const VITE_CONFIG_FILES = [
|
|
46
|
+
"vite.config.js",
|
|
47
|
+
"vite.config.ts",
|
|
48
|
+
"vite.config.mjs",
|
|
49
|
+
"vite.config.cjs",
|
|
50
|
+
];
|
|
45
51
|
function detectRepoComposeFile(repo) {
|
|
46
52
|
return REPO_COMPOSE_FILES.find(file => exists(repo, file)) ?? null;
|
|
47
53
|
}
|
|
@@ -192,6 +198,45 @@ function pickAppCommand(pkg, pm) {
|
|
|
192
198
|
}
|
|
193
199
|
return { command: null, source: "no dev/start/serve/preview script found", script: null };
|
|
194
200
|
}
|
|
201
|
+
function shellSegmentExecutable(segment) {
|
|
202
|
+
let remaining = segment.trim();
|
|
203
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+/.test(remaining)) {
|
|
204
|
+
remaining = remaining.replace(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+/, "");
|
|
205
|
+
}
|
|
206
|
+
return remaining.match(/^([A-Za-z0-9_.-]+)/)?.[1] ?? null;
|
|
207
|
+
}
|
|
208
|
+
function projectCliFromScript(scriptCommand, packageManager) {
|
|
209
|
+
const standardCommands = new Set([
|
|
210
|
+
"npm", "pnpm", "yarn", "bun", "node", "npx", "corepack",
|
|
211
|
+
"go", "ruby", "bundle", "make", "python", "python3", "php", "composer",
|
|
212
|
+
"cd", "echo", "env", "export", "sh", "bash", "true", "false", "test",
|
|
213
|
+
]);
|
|
214
|
+
if (packageManager !== "unknown")
|
|
215
|
+
standardCommands.add(packageManager);
|
|
216
|
+
for (const segment of scriptCommand.split(/\s*(?:&&|\|\||;)\s*/)) {
|
|
217
|
+
const executable = shellSegmentExecutable(segment);
|
|
218
|
+
if (executable && !standardCommands.has(executable))
|
|
219
|
+
return executable;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
function executableAvailable(repo, executable) {
|
|
224
|
+
const localBin = path.join(repo, "node_modules", ".bin", executable);
|
|
225
|
+
if (exists(repo, path.relative(repo, localBin)))
|
|
226
|
+
return true;
|
|
227
|
+
const extensions = process.platform === "win32"
|
|
228
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
|
|
229
|
+
: [""];
|
|
230
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
|
|
231
|
+
for (const extension of extensions) {
|
|
232
|
+
if (fs.existsSync(path.join(directory, `${executable}${extension.toLowerCase()}`))
|
|
233
|
+
|| fs.existsSync(path.join(directory, `${executable}${extension.toUpperCase()}`))) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
195
240
|
function detectNestedFrontend(repo) {
|
|
196
241
|
const preferred = ["superset-frontend", "frontend", "web", "ui", "client"];
|
|
197
242
|
for (const dir of preferred) {
|
|
@@ -233,6 +278,52 @@ function detectGoEntrypoint(repo) {
|
|
|
233
278
|
portFlag: /(?:Int|IntVar)\(\s*["']port["']/m.test(sourceText),
|
|
234
279
|
};
|
|
235
280
|
}
|
|
281
|
+
function detectGoService(repo) {
|
|
282
|
+
if (!exists(repo, "go.mod")) {
|
|
283
|
+
return {
|
|
284
|
+
ollamaLike: false,
|
|
285
|
+
command: null,
|
|
286
|
+
commandSource: null,
|
|
287
|
+
port: null,
|
|
288
|
+
healthCandidates: [],
|
|
289
|
+
markers: [],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const moduleFile = readText(repo, "go.mod");
|
|
293
|
+
const mainFile = readText(repo, "main.go");
|
|
294
|
+
const commandFile = readText(repo, "cmd/cmd.go");
|
|
295
|
+
const routesFile = readText(repo, "server/routes.go");
|
|
296
|
+
const envConfigFile = readText(repo, "envconfig/config.go");
|
|
297
|
+
const evidence = [moduleFile, mainFile, commandFile, routesFile, envConfigFile].join("\n");
|
|
298
|
+
const exactModule = /^module\s+github\.com\/ollama\/ollama\s*$/m.test(moduleFile);
|
|
299
|
+
const ollamaModule = /^module\s+\S*ollama(?:\/\S*)?\s*$/mi.test(moduleFile);
|
|
300
|
+
const markers = [
|
|
301
|
+
exactModule || ollamaModule ? "ollama module" : null,
|
|
302
|
+
/\bOLLAMA_HOST\b/.test(evidence) ? "OLLAMA_HOST" : null,
|
|
303
|
+
/\b11434\b/.test(evidence) ? "port 11434" : null,
|
|
304
|
+
/["'`]\/api\/tags["'`]/.test(evidence) ? "/api/tags" : null,
|
|
305
|
+
/\bUse:\s*["']serve["']|\bollama serve\b/i.test(evidence) ? "serve command" : null,
|
|
306
|
+
].filter((marker) => marker !== null);
|
|
307
|
+
const hasRootEntrypoint = exists(repo, "main.go");
|
|
308
|
+
const hasServeEvidence = markers.includes("serve command");
|
|
309
|
+
const strongOllamaEvidence = exactModule || markers.length >= 3;
|
|
310
|
+
const ollamaLike = hasRootEntrypoint && hasServeEvidence && strongOllamaEvidence;
|
|
311
|
+
return {
|
|
312
|
+
ollamaLike,
|
|
313
|
+
command: ollamaLike ? "go run . serve" : null,
|
|
314
|
+
commandSource: ollamaLike ? "Ollama Go service entrypoint: main.go + serve command" : null,
|
|
315
|
+
port: ollamaLike ? 11434 : null,
|
|
316
|
+
healthCandidates: ollamaLike
|
|
317
|
+
? [
|
|
318
|
+
"http://127.0.0.1:11434/",
|
|
319
|
+
"http://localhost:11434/",
|
|
320
|
+
"http://127.0.0.1:11434/api/tags",
|
|
321
|
+
"http://localhost:11434/api/tags",
|
|
322
|
+
]
|
|
323
|
+
: [],
|
|
324
|
+
markers,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
236
327
|
function detectRubyCommand(repo) {
|
|
237
328
|
if (!exists(repo, "Gemfile") || !exists(repo, "bin/rails"))
|
|
238
329
|
return null;
|
|
@@ -244,12 +335,13 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
|
|
|
244
335
|
const setupPy = readText(repo, "setup.py");
|
|
245
336
|
const requirements = readText(repo, "requirements.txt");
|
|
246
337
|
const compose = repoComposeFile ? readText(repo, repoComposeFile) : "";
|
|
338
|
+
const composer = readJson(path.join(repo, "composer.json"));
|
|
247
339
|
const rootDeps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
248
340
|
const nestedDeps = { ...(nestedFrontend?.pkg?.dependencies ?? {}), ...(nestedFrontend?.pkg?.devDependencies ?? {}) };
|
|
249
|
-
const backendMarkers = present(repo, ["pyproject.toml", "setup.py", "requirements.txt", "manage.py", "go.mod", "go.work", "Gemfile", "config/database.yml", "Makefile", "superset/app.py", "superset/config.py"]);
|
|
341
|
+
const backendMarkers = present(repo, ["pyproject.toml", "setup.py", "requirements.txt", "manage.py", "go.mod", "go.work", "Gemfile", "config/database.yml", "Makefile", "superset/app.py", "superset/config.py", "artisan", "composer.json"]);
|
|
250
342
|
if (isDirectory(repo, "pkg"))
|
|
251
343
|
backendMarkers.push("pkg/");
|
|
252
|
-
const frontendMarkers = present(repo, ["package.json", "yarn.lock", "pnpm-lock.yaml", "nx.json"]);
|
|
344
|
+
const frontendMarkers = present(repo, ["package.json", "yarn.lock", "pnpm-lock.yaml", "nx.json", ...VITE_CONFIG_FILES]);
|
|
253
345
|
if (isDirectory(repo, "public"))
|
|
254
346
|
frontendMarkers.push("public/");
|
|
255
347
|
if (isDirectory(repo, "packages"))
|
|
@@ -261,18 +353,50 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
|
|
|
261
353
|
(exists(repo, "superset/app.py") || exists(repo, "superset/config.py") || /\bflask\b/i.test(pyproject + setupPy + makefile));
|
|
262
354
|
const hasFlask = hasFlaskBackend && (/\bflask\b/i.test(pyproject + setupPy + makefile) || exists(repo, "superset/app.py"));
|
|
263
355
|
const hasDjango = exists(repo, "manage.py") && /\bdjango\b/i.test(pyproject + requirements);
|
|
264
|
-
const
|
|
356
|
+
const hasLargePythonNodeHybrid = exists(repo, "pyproject.toml")
|
|
357
|
+
&& exists(repo, "Makefile")
|
|
358
|
+
&& exists(repo, "package.json")
|
|
359
|
+
&& exists(repo, "pnpm-lock.yaml");
|
|
360
|
+
const hasPythonBackend = hasFlaskBackend || hasDjango || hasLargePythonNodeHybrid;
|
|
265
361
|
const hasGoBackend = exists(repo, "go.mod") || exists(repo, "go.work");
|
|
266
362
|
const hasRubyBackend = exists(repo, "Gemfile");
|
|
363
|
+
const hasLaravel = exists(repo, "artisan") && Boolean(composer);
|
|
364
|
+
const hasPhpBackend = hasLaravel;
|
|
365
|
+
const viteConfig = VITE_CONFIG_FILES.find(file => exists(repo, file)) ?? null;
|
|
366
|
+
const hasLaravelViteFrontend = hasLaravel && Boolean(pkg) && Boolean(viteConfig);
|
|
267
367
|
const makeCommand = detectMakeCommand(repo, makefile);
|
|
268
368
|
const goEntrypoint = detectGoEntrypoint(repo);
|
|
369
|
+
const goService = detectGoService(repo);
|
|
269
370
|
const rubyCommand = detectRubyCommand(repo);
|
|
270
371
|
const hasMakeDrivenBackend = Boolean(makefile && /^[A-Za-z0-9_.-]+:\s*(?:[^=]|$)/m.test(makefile));
|
|
271
|
-
const hasNodeFrontend = Boolean(pkg) && (isDirectory(repo, "public")
|
|
372
|
+
const hasNodeFrontend = Boolean(pkg) && (isDirectory(repo, "public")
|
|
373
|
+
|| isDirectory(repo, "packages")
|
|
374
|
+
|| exists(repo, "nx.json")
|
|
375
|
+
|| hasGoBackend
|
|
376
|
+
|| hasRubyBackend
|
|
377
|
+
|| hasLaravelViteFrontend
|
|
378
|
+
|| hasLargePythonNodeHybrid);
|
|
272
379
|
const hasReact = Boolean(rootDeps.react || nestedDeps.react);
|
|
273
380
|
const hasReactFrontend = Boolean(nestedFrontend && hasReact);
|
|
274
381
|
const hasCelery = /\bcelery\b/i.test(pyproject + setupPy + makefile + compose);
|
|
275
382
|
const hasCompose = serviceMarkers.length > 0;
|
|
383
|
+
if (hasLargePythonNodeHybrid) {
|
|
384
|
+
if (isDirectory(repo, "src"))
|
|
385
|
+
backendMarkers.push("src/");
|
|
386
|
+
if (isDirectory(repo, "static"))
|
|
387
|
+
frontendMarkers.push("static/");
|
|
388
|
+
if (isDirectory(repo, "devservices"))
|
|
389
|
+
serviceMarkers.push("devservices/");
|
|
390
|
+
}
|
|
391
|
+
if (hasGoBackend) {
|
|
392
|
+
if (exists(repo, "main.go"))
|
|
393
|
+
backendMarkers.push("main.go");
|
|
394
|
+
if (isDirectory(repo, "cmd"))
|
|
395
|
+
backendMarkers.push("cmd/");
|
|
396
|
+
if (isDirectory(repo, "server"))
|
|
397
|
+
backendMarkers.push("server/");
|
|
398
|
+
backendMarkers.push(...goService.markers);
|
|
399
|
+
}
|
|
276
400
|
const stack = [];
|
|
277
401
|
if (hasPythonBackend)
|
|
278
402
|
stack.push("python-backend");
|
|
@@ -284,8 +408,14 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
|
|
|
284
408
|
stack.push("go-backend");
|
|
285
409
|
if (hasRubyBackend)
|
|
286
410
|
stack.push("ruby-backend");
|
|
287
|
-
if (
|
|
411
|
+
if (hasPhpBackend)
|
|
412
|
+
stack.push("php-backend");
|
|
413
|
+
if (hasLaravel)
|
|
414
|
+
stack.push("laravel");
|
|
415
|
+
if (hasLargePythonNodeHybrid
|
|
416
|
+
|| (hasMakeDrivenBackend && !hasPythonBackend && !hasGoBackend && !hasRubyBackend && !hasPhpBackend)) {
|
|
288
417
|
stack.push("make-driven");
|
|
418
|
+
}
|
|
289
419
|
if (hasNodeFrontend)
|
|
290
420
|
stack.push("node-frontend");
|
|
291
421
|
if (hasReactFrontend)
|
|
@@ -294,7 +424,7 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
|
|
|
294
424
|
stack.push("react");
|
|
295
425
|
if (rootDeps.next)
|
|
296
426
|
stack.push("nextjs");
|
|
297
|
-
if (rootDeps.vite)
|
|
427
|
+
if (rootDeps.vite || viteConfig)
|
|
298
428
|
stack.push("vite");
|
|
299
429
|
if (rootDeps.express)
|
|
300
430
|
stack.push("express");
|
|
@@ -308,6 +438,10 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
|
|
|
308
438
|
stack.push("docker-compose");
|
|
309
439
|
if (hasCelery)
|
|
310
440
|
stack.push("celery");
|
|
441
|
+
if (hasLargePythonNodeHybrid)
|
|
442
|
+
stack.push("large-hybrid-app");
|
|
443
|
+
if (hasLargePythonNodeHybrid && isDirectory(repo, "devservices"))
|
|
444
|
+
stack.push("devservices-backed");
|
|
311
445
|
const setupSteps = [];
|
|
312
446
|
if (/^\s*superset db upgrade\s*$/m.test(makefile))
|
|
313
447
|
setupSteps.push("superset db upgrade");
|
|
@@ -327,18 +461,28 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
|
|
|
327
461
|
workerCommand,
|
|
328
462
|
makeCommand,
|
|
329
463
|
goEntrypoint,
|
|
464
|
+
goService,
|
|
330
465
|
rubyCommand,
|
|
331
466
|
hasPythonBackend,
|
|
467
|
+
hasLargePythonNodeHybrid,
|
|
332
468
|
hasFlask,
|
|
333
469
|
hasDjango,
|
|
334
470
|
hasGoBackend,
|
|
335
471
|
hasRubyBackend,
|
|
472
|
+
hasPhpBackend,
|
|
473
|
+
hasLaravel,
|
|
474
|
+
hasLaravelViteFrontend,
|
|
475
|
+
sailCommand: hasLaravel && exists(repo, "vendor/bin/sail") ? "./vendor/bin/sail up" : null,
|
|
336
476
|
hasMakeDrivenBackend,
|
|
337
477
|
hasNodeFrontend,
|
|
338
478
|
};
|
|
339
479
|
}
|
|
340
|
-
function detectPort(pkg, repo, commands) {
|
|
341
|
-
const sources = [
|
|
480
|
+
function detectPort(pkg, repo, commands, options = {}) {
|
|
481
|
+
const sources = [
|
|
482
|
+
options.ignorePackageScripts ? "" : JSON.stringify(pkg?.scripts ?? {}),
|
|
483
|
+
readText(repo, "Makefile"),
|
|
484
|
+
...commands.filter((v) => Boolean(v)),
|
|
485
|
+
].join("\n");
|
|
342
486
|
const m = sources.match(/(?:-p|--port)(?:=|\s+|[\\"]+)(\d{2,5})/);
|
|
343
487
|
if (m)
|
|
344
488
|
return { port: Number(m[1]), evidence: `port flag in command evidence: ${m[0].replace(/\\"/g, "").trim()}` };
|
|
@@ -349,10 +493,11 @@ function detectPort(pkg, repo, commands) {
|
|
|
349
493
|
if (listen)
|
|
350
494
|
return { port: Number(listen[1]), evidence: "HTTP listen address in source" };
|
|
351
495
|
const envEx = readText(repo, ".env.example");
|
|
352
|
-
const pm = envEx.match(/^PORT=(\d{2,5})/m);
|
|
496
|
+
const pm = envEx.match(/^(?:APP_)?PORT=(\d{2,5})/m);
|
|
353
497
|
if (pm)
|
|
354
|
-
return { port: Number(pm[1]), evidence: "
|
|
355
|
-
|
|
498
|
+
return { port: Number(pm[1]), evidence: `${pm[0].split("=")[0]} in .env.example` };
|
|
499
|
+
const defaultPort = options.defaultPort ?? 3000;
|
|
500
|
+
return { port: defaultPort, evidence: `default assumption (${defaultPort}); not evidence-based` };
|
|
356
501
|
}
|
|
357
502
|
function detectServices(pkg, repo, repoComposeFile) {
|
|
358
503
|
const out = [];
|
|
@@ -534,45 +679,74 @@ export function inferRepo(repoPath, opts = {}) {
|
|
|
534
679
|
const architecture = detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile);
|
|
535
680
|
const pm = detectPackageManager(repo, pkg, nestedFrontend?.dir ?? null);
|
|
536
681
|
const rootApp = pickAppCommand(pkg, pm.pm);
|
|
682
|
+
const rootScriptText = rootApp.script ? String(pkg?.scripts?.[rootApp.script] ?? "") : "";
|
|
683
|
+
const projectCliCommand = architecture.hasLargePythonNodeHybrid && rootScriptText
|
|
684
|
+
? projectCliFromScript(rootScriptText, pm.pm)
|
|
685
|
+
: null;
|
|
686
|
+
const projectCliReady = projectCliCommand === null
|
|
687
|
+
? null
|
|
688
|
+
: executableAvailable(repo, projectCliCommand);
|
|
689
|
+
const assetDevServerCommand = architecture.hasLaravelViteFrontend && /\bvite\b/i.test(rootScriptText)
|
|
690
|
+
? rootApp.command
|
|
691
|
+
: null;
|
|
537
692
|
const commandEvidence = [
|
|
538
693
|
architecture.flaskCommand,
|
|
539
694
|
architecture.makeCommand?.command ?? null,
|
|
540
695
|
architecture.goEntrypoint?.sourceText ?? null,
|
|
696
|
+
architecture.goService.command,
|
|
541
697
|
architecture.rubyCommand?.commandBase ?? null,
|
|
698
|
+
architecture.sailCommand,
|
|
542
699
|
];
|
|
543
|
-
const { port, evidence: portEvidence } = detectPort(pkg, repo, commandEvidence
|
|
544
|
-
|
|
700
|
+
const { port, evidence: portEvidence } = detectPort(pkg, repo, commandEvidence, {
|
|
701
|
+
ignorePackageScripts: architecture.hasLaravel,
|
|
702
|
+
defaultPort: architecture.goService.port
|
|
703
|
+
?? (architecture.sailCommand ? 80 : architecture.hasLaravel ? 8000 : 3000),
|
|
704
|
+
});
|
|
705
|
+
const goCommand = architecture.goService.command ?? (architecture.goEntrypoint
|
|
545
706
|
? [
|
|
546
707
|
architecture.goEntrypoint.commandBase,
|
|
547
708
|
architecture.goEntrypoint.portFlag ? `--port ${port}` : "",
|
|
548
709
|
architecture.goEntrypoint.dataDirFlag ? "--data .bootproof/runtime/go-app" : "",
|
|
549
710
|
].filter(Boolean).join(" ")
|
|
550
|
-
: null;
|
|
711
|
+
: null);
|
|
551
712
|
const rubyCommand = architecture.rubyCommand ? `${architecture.rubyCommand.commandBase} -p ${port}` : null;
|
|
552
713
|
const djangoCommand = architecture.hasDjango ? `python manage.py runserver 127.0.0.1:${port}` : null;
|
|
714
|
+
const laravelCommand = architecture.hasLaravel
|
|
715
|
+
? architecture.sailCommand ?? `php artisan serve --host=127.0.0.1 --port=${port}`
|
|
716
|
+
: null;
|
|
553
717
|
const repositoryBackendCommand = architecture.makeCommand?.command ?? goCommand ?? rubyCommand;
|
|
554
|
-
const backendCommand = architecture.flaskCommand ?? djangoCommand ?? repositoryBackendCommand;
|
|
718
|
+
const backendCommand = architecture.flaskCommand ?? djangoCommand ?? laravelCommand ?? repositoryBackendCommand;
|
|
555
719
|
const nestedFrontendCommand = architecture.frontendMakeCommand;
|
|
556
720
|
const frontendCommand = nestedFrontendCommand ?? rootApp.command;
|
|
557
721
|
const appCommand = architecture.flaskCommand
|
|
558
722
|
?? djangoCommand
|
|
723
|
+
?? laravelCommand
|
|
559
724
|
?? (architecture.hasGoBackend && architecture.hasNodeFrontend && rootApp.command ? rootApp.command : null)
|
|
560
725
|
?? repositoryBackendCommand
|
|
561
726
|
?? rootApp.command;
|
|
727
|
+
const selectedProjectCliCommand = rootApp.command === appCommand ? projectCliCommand : null;
|
|
728
|
+
const selectedProjectCliReady = selectedProjectCliCommand === null ? null : projectCliReady;
|
|
562
729
|
const appCommandSource = architecture.flaskCommand
|
|
563
730
|
? `Makefile Flask command: ${architecture.flaskCommand}`
|
|
564
731
|
: djangoCommand && appCommand === djangoCommand
|
|
565
732
|
? "Django entrypoint: manage.py"
|
|
566
|
-
:
|
|
567
|
-
? architecture.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
733
|
+
: laravelCommand && appCommand === laravelCommand
|
|
734
|
+
? architecture.sailCommand
|
|
735
|
+
? "Laravel Sail entrypoint: vendor/bin/sail"
|
|
736
|
+
: "Laravel entrypoint: artisan"
|
|
737
|
+
: architecture.makeCommand && appCommand === architecture.makeCommand.command
|
|
738
|
+
? architecture.makeCommand.source
|
|
739
|
+
: goCommand && appCommand === goCommand
|
|
740
|
+
? architecture.goService.commandSource ?? architecture.goEntrypoint?.source ?? rootApp.source
|
|
741
|
+
: rubyCommand && appCommand === rubyCommand
|
|
742
|
+
? architecture.rubyCommand?.source ?? rootApp.source
|
|
743
|
+
: selectedProjectCliCommand && selectedProjectCliReady === false
|
|
744
|
+
? `${rootApp.source}; project CLI ${selectedProjectCliCommand} readiness not established`
|
|
745
|
+
: rootApp.source;
|
|
573
746
|
const recognizedApplication = architecture.hasPythonBackend ||
|
|
574
747
|
architecture.hasGoBackend ||
|
|
575
748
|
architecture.hasRubyBackend ||
|
|
749
|
+
architecture.hasPhpBackend ||
|
|
576
750
|
architecture.hasMakeDrivenBackend ||
|
|
577
751
|
composeApplicationServices.some(service => service.source === "build");
|
|
578
752
|
const workspaces = opts.workspace ? [] : rankWorkspaces(rootRepo, rootPkg);
|
|
@@ -582,6 +756,7 @@ export function inferRepo(repoPath, opts = {}) {
|
|
|
582
756
|
const rootDeps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
583
757
|
const preparationCommands = [];
|
|
584
758
|
const nodePreparationRequired = Boolean(rootApp.command &&
|
|
759
|
+
!architecture.hasLaravel &&
|
|
585
760
|
(Object.keys(rootDeps).length > 0 || exists(repo, "yarn.lock") || exists(repo, "pnpm-lock.yaml") || exists(repo, "package-lock.json") || exists(repo, "nx.json")));
|
|
586
761
|
if (nodePreparationRequired) {
|
|
587
762
|
const command = installCommand(pm.pm, pm.packageDir);
|
|
@@ -594,6 +769,9 @@ export function inferRepo(repoPath, opts = {}) {
|
|
|
594
769
|
if (rubyCommand && appCommand === rubyCommand) {
|
|
595
770
|
preparationCommands.push({ id: "bundle-install", kind: "install", command: "bundle install", description: "install declared Ruby gems", source: "Gemfile and bin/rails present" });
|
|
596
771
|
}
|
|
772
|
+
if (laravelCommand && appCommand === laravelCommand) {
|
|
773
|
+
preparationCommands.push({ id: "composer-install", kind: "install", command: "composer install", description: "install declared PHP dependencies", source: "composer.json and artisan present" });
|
|
774
|
+
}
|
|
597
775
|
const dependencyInstallRequired = preparationCommands.length > 0;
|
|
598
776
|
const incompleteAppCommand = Boolean(architecture.hasGoBackend && architecture.hasNodeFrontend && rootApp.command);
|
|
599
777
|
const multiAppCommand = Boolean(rootApp.command && /\b(?:turbo|nx)\s+run\s+dev\b[^\n]*--parallel\b/i.test(rootApp.source));
|
|
@@ -601,28 +779,38 @@ export function inferRepo(repoPath, opts = {}) {
|
|
|
601
779
|
? "multi-workspace development pipeline; no single application health target selected"
|
|
602
780
|
: incompleteAppCommand
|
|
603
781
|
? "frontend/dev pipeline only; Go backend markers also detected"
|
|
604
|
-
: architecture.
|
|
605
|
-
?
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
782
|
+
: architecture.hasLargePythonNodeHybrid && rootApp.command === appCommand
|
|
783
|
+
? selectedProjectCliCommand && selectedProjectCliReady === false
|
|
784
|
+
? `large Python/Node hybrid package script; project CLI ${selectedProjectCliCommand} readiness not established`
|
|
785
|
+
: "large Python/Node hybrid package script"
|
|
786
|
+
: architecture.hasFlask && nestedFrontend
|
|
787
|
+
? "Python/Flask backend command; React frontend and worker require separate orchestration"
|
|
788
|
+
: djangoCommand && appCommand === djangoCommand
|
|
789
|
+
? "Django application command"
|
|
790
|
+
: laravelCommand && appCommand === laravelCommand
|
|
791
|
+
? architecture.sailCommand
|
|
792
|
+
? "Laravel application through Sail"
|
|
793
|
+
: "Laravel application command; Vite is an asset development server only"
|
|
794
|
+
: goCommand && appCommand === goCommand && nestedFrontend
|
|
795
|
+
? "Go application command serving repository-embedded frontend assets"
|
|
796
|
+
: rubyCommand && appCommand === rubyCommand
|
|
797
|
+
? "Rails application command"
|
|
798
|
+
: architecture.makeCommand && appCommand === architecture.makeCommand.command
|
|
799
|
+
? "repository-defined Make target"
|
|
800
|
+
: appCommand
|
|
801
|
+
? "application command"
|
|
802
|
+
: "no runnable command selected";
|
|
617
803
|
const healthCandidates = notApp
|
|
618
804
|
? []
|
|
619
805
|
: !appCommand && composeHealthCandidates.length
|
|
620
806
|
? composeHealthCandidates
|
|
621
807
|
: !appCommand
|
|
622
808
|
? []
|
|
623
|
-
: architecture.
|
|
624
|
-
?
|
|
625
|
-
:
|
|
809
|
+
: architecture.goService.ollamaLike
|
|
810
|
+
? architecture.goService.healthCandidates
|
|
811
|
+
: architecture.hasGoBackend && architecture.hasNodeFrontend
|
|
812
|
+
? [`http://localhost:${port}/api/health`, `http://localhost:${port}/`]
|
|
813
|
+
: [`http://localhost:${port}/`];
|
|
626
814
|
let confidence = 0;
|
|
627
815
|
if (appCommand)
|
|
628
816
|
confidence += 35;
|
|
@@ -636,6 +824,8 @@ export function inferRepo(repoPath, opts = {}) {
|
|
|
636
824
|
confidence += 10;
|
|
637
825
|
if (services.length || env.required.length)
|
|
638
826
|
confidence += 5;
|
|
827
|
+
if (selectedProjectCliCommand && selectedProjectCliReady === false)
|
|
828
|
+
confidence = Math.min(confidence, 60);
|
|
639
829
|
return {
|
|
640
830
|
repoPath: repo,
|
|
641
831
|
isApplication: !notApp,
|
|
@@ -656,14 +846,23 @@ export function inferRepo(repoPath, opts = {}) {
|
|
|
656
846
|
dependencyInstallRequired,
|
|
657
847
|
appCommand,
|
|
658
848
|
appCommandSource,
|
|
849
|
+
selectedPackageScriptName: rootApp.command === appCommand ? rootApp.script : null,
|
|
850
|
+
selectedPackageScriptCommand: rootApp.command === appCommand ? rootScriptText || null : null,
|
|
851
|
+
projectCliCommand: selectedProjectCliCommand,
|
|
852
|
+
projectCliReady: selectedProjectCliReady,
|
|
659
853
|
backendCommand,
|
|
660
854
|
frontendCommand,
|
|
855
|
+
asset_dev_server_command: assetDevServerCommand,
|
|
661
856
|
workerCommand: architecture.workerCommand,
|
|
662
857
|
commandScope,
|
|
663
858
|
incompleteAppCommand,
|
|
664
859
|
multiAppCommand,
|
|
665
860
|
port,
|
|
666
|
-
portEvidence
|
|
861
|
+
portEvidence: architecture.goService.port
|
|
862
|
+
? "known Ollama service port from repository evidence"
|
|
863
|
+
: portEvidence,
|
|
864
|
+
observedPort: null,
|
|
865
|
+
healthCandidateSource: architecture.goService.ollamaLike ? "known_service" : "inferred",
|
|
667
866
|
healthCandidates,
|
|
668
867
|
services,
|
|
669
868
|
requiredEnv: env.required,
|
package/dist/plan.js
CHANGED
|
@@ -106,6 +106,8 @@ export function buildPlan(inf, provider) {
|
|
|
106
106
|
steps,
|
|
107
107
|
healthUrl,
|
|
108
108
|
healthCandidates,
|
|
109
|
+
observedPort: inf.observedPort,
|
|
110
|
+
healthCandidateSource: inf.healthCandidateSource,
|
|
109
111
|
generatedFiles: [
|
|
110
112
|
...(composeFileFor(inf) ? [{ path: "docker-compose.bootproof.yml", purpose: "service containers" }] : []),
|
|
111
113
|
...(envExampleFor(inf) ? [{ path: ".env.bootproof.example", purpose: "suggested local env values (never auto-applied)" }] : []),
|
package/dist/proof.d.ts
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
|
-
import type { Attestation, ObservedStep, RunPlan, FailureClass } from "./types.js";
|
|
2
|
-
export declare const TOOL_ID = "bootproof@0.
|
|
1
|
+
import type { Attestation, AttestationTrust, ObservedStep, RunPlan, FailureClass, HealthEvidence, VerificationMode, ExternalVerificationClassification } from "./types.js";
|
|
2
|
+
export declare const TOOL_ID = "bootproof@0.4.0";
|
|
3
|
+
export type { AttestationTrust } from "./types.js";
|
|
4
|
+
export type SignerTrustTier = "invalid" | "self" | "known" | "unknown-foreign";
|
|
5
|
+
export interface SignatureTrustResult {
|
|
6
|
+
integrityValid: boolean;
|
|
7
|
+
tier: SignerTrustTier;
|
|
8
|
+
fingerprint: string | null;
|
|
9
|
+
label: string | null;
|
|
10
|
+
}
|
|
3
11
|
export declare function gitInfo(repo: string): Attestation["repo"];
|
|
12
|
+
export declare function knownSignersPath(): string;
|
|
13
|
+
export interface RotationResult {
|
|
14
|
+
schema: "bootproof/key-rotation/v1";
|
|
15
|
+
rotatedAt: string;
|
|
16
|
+
oldPublicKey: string;
|
|
17
|
+
newPublicKey: string;
|
|
18
|
+
backedUpTo: string | null;
|
|
19
|
+
reSignedAttestation: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Rotate the local ed25519 signing key. The old key's public key is archived
|
|
23
|
+
* (so existing attestations remain independently verifiable), a new keypair is
|
|
24
|
+
* generated, and the latest attestation in the given repo is optionally
|
|
25
|
+
* re-signed with the new key.
|
|
26
|
+
*
|
|
27
|
+
* Rotation does NOT invalidate existing attestations — they still carry the
|
|
28
|
+
* old public key inline and verify with it. Rotation only affects what key
|
|
29
|
+
* future attestations will be signed with.
|
|
30
|
+
*/
|
|
31
|
+
export declare function rotateSigner(opts?: {
|
|
32
|
+
repo?: string;
|
|
33
|
+
resignAttestation?: boolean;
|
|
34
|
+
backup?: boolean;
|
|
35
|
+
}): RotationResult;
|
|
36
|
+
export declare function signerFingerprint(publicKeyPem: string): string;
|
|
37
|
+
export declare function trustSigner(publicKeyPem: string, label?: string): {
|
|
38
|
+
fingerprint: string;
|
|
39
|
+
firstSeenAt: string;
|
|
40
|
+
label: string | null;
|
|
41
|
+
};
|
|
42
|
+
export declare function evaluateDetachedSignature(body: Buffer, signature: string | null | undefined, publicKeyPem: string | null | undefined): SignatureTrustResult;
|
|
4
43
|
export declare function buildAttestation(input: {
|
|
5
44
|
repo: string;
|
|
6
45
|
plan: RunPlan;
|
|
@@ -9,16 +48,53 @@ export declare function buildAttestation(input: {
|
|
|
9
48
|
booted: boolean;
|
|
10
49
|
healthVerified: boolean;
|
|
11
50
|
healthObservation: string | null;
|
|
51
|
+
healthEvidence?: HealthEvidence | null;
|
|
12
52
|
observedHealthCandidates?: string[];
|
|
13
53
|
failureClass: FailureClass | null;
|
|
14
54
|
failureEvidence: string | null;
|
|
15
55
|
explanation: string;
|
|
56
|
+
verificationMode?: VerificationMode;
|
|
57
|
+
bootproofOrchestrated?: boolean;
|
|
58
|
+
externalHealthUrl?: string | null;
|
|
59
|
+
observedStatus?: number | null;
|
|
60
|
+
observedFinalUrl?: string | null;
|
|
61
|
+
observedAt?: string | null;
|
|
62
|
+
responseSnippet?: string;
|
|
63
|
+
classification?: ExternalVerificationClassification | null;
|
|
64
|
+
trust?: AttestationTrust;
|
|
16
65
|
}): Attestation;
|
|
66
|
+
/**
|
|
67
|
+
* Detect GitHub Actions OIDC environment. Present only when the workflow has
|
|
68
|
+
* `permissions: id-token: write`. The presence of these env vars IS the consent —
|
|
69
|
+
* the workflow author explicitly granted the OIDC scope.
|
|
70
|
+
*/
|
|
71
|
+
export declare function detectOidcEnv(env?: NodeJS.ProcessEnv): {
|
|
72
|
+
requestUrl: string;
|
|
73
|
+
requestToken: string;
|
|
74
|
+
} | null;
|
|
75
|
+
/**
|
|
76
|
+
* Fetch the OIDC JWT from GitHub Actions and decode its claims (without verification —
|
|
77
|
+
* verification is the receiver's job, not the signer's). Returns a compact record of
|
|
78
|
+
* claims suitable for embedding in the attestation trust block.
|
|
79
|
+
*/
|
|
80
|
+
export declare function fetchOidcClaims(requestUrl: string, requestToken: string, fetchImpl?: typeof fetch): Promise<Record<string, string>>;
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the trust block for an attestation. When `--ci-oidc` is requested and
|
|
83
|
+
* the GitHub Actions OIDC environment is present, fetch the OIDC token and return
|
|
84
|
+
* a ci_oidc_signed trust block. Otherwise return local_developer_signed.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveTrust(opts?: {
|
|
87
|
+
ciOidc?: boolean;
|
|
88
|
+
env?: NodeJS.ProcessEnv;
|
|
89
|
+
fetchImpl?: typeof fetch;
|
|
90
|
+
}): Promise<AttestationTrust>;
|
|
17
91
|
export declare function signDetached(body: Buffer): {
|
|
18
92
|
signature: string;
|
|
19
93
|
publicKeyPem: string;
|
|
20
94
|
};
|
|
21
95
|
export declare function verifyDetached(body: Buffer, signature: string, publicKeyPem: string): boolean;
|
|
22
96
|
export declare function verifySignature(att: Attestation): boolean;
|
|
97
|
+
export declare function evaluateAttestationSignature(att: Attestation): SignatureTrustResult;
|
|
98
|
+
export declare function currentGitHead(repo: string): string | null;
|
|
23
99
|
export declare function attestationPath(repo: string): string;
|
|
24
100
|
export declare function writeAttestation(repo: string, att: Attestation): string;
|