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/proof.js
CHANGED
|
@@ -3,11 +3,13 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
|
-
|
|
6
|
+
import { buildExecutionEnv } from "./exec.js";
|
|
7
|
+
import { redactJsonValue } from "./redact.js";
|
|
8
|
+
export const TOOL_ID = "bootproof@0.4.0";
|
|
7
9
|
export function gitInfo(repo) {
|
|
8
10
|
const git = (...args) => {
|
|
9
11
|
try {
|
|
10
|
-
return execFileSync("git", ["-C", repo, ...args], { encoding: "utf8" }).trim();
|
|
12
|
+
return execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", env: buildExecutionEnv() }).trim();
|
|
11
13
|
}
|
|
12
14
|
catch {
|
|
13
15
|
return null;
|
|
@@ -26,6 +28,9 @@ export function gitInfo(repo) {
|
|
|
26
28
|
function signerKeyPath() {
|
|
27
29
|
return path.join(os.homedir(), ".bootproof", "signer.json");
|
|
28
30
|
}
|
|
31
|
+
export function knownSignersPath() {
|
|
32
|
+
return path.join(os.homedir(), ".bootproof", "known_signers.json");
|
|
33
|
+
}
|
|
29
34
|
function loadOrCreateSigner() {
|
|
30
35
|
const p = signerKeyPath();
|
|
31
36
|
if (fs.existsSync(p)) {
|
|
@@ -39,27 +44,204 @@ function loadOrCreateSigner() {
|
|
|
39
44
|
fs.writeFileSync(p, JSON.stringify({ privateKeyPem, publicKeyPem }), { mode: 0o600 });
|
|
40
45
|
return { privateKey: crypto.createPrivateKey(privateKeyPem), publicKeyPem };
|
|
41
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Rotate the local ed25519 signing key. The old key's public key is archived
|
|
49
|
+
* (so existing attestations remain independently verifiable), a new keypair is
|
|
50
|
+
* generated, and the latest attestation in the given repo is optionally
|
|
51
|
+
* re-signed with the new key.
|
|
52
|
+
*
|
|
53
|
+
* Rotation does NOT invalidate existing attestations — they still carry the
|
|
54
|
+
* old public key inline and verify with it. Rotation only affects what key
|
|
55
|
+
* future attestations will be signed with.
|
|
56
|
+
*/
|
|
57
|
+
export function rotateSigner(opts = {}) {
|
|
58
|
+
const p = signerKeyPath();
|
|
59
|
+
const oldKey = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf8")) : null;
|
|
60
|
+
const oldPublicKeyPem = oldKey?.publicKeyPem ?? null;
|
|
61
|
+
// Generate the new keypair.
|
|
62
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
|
|
63
|
+
const newPrivateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
64
|
+
const newPublicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
65
|
+
// Back up the old key before overwriting (unless explicitly skipped).
|
|
66
|
+
let backedUpTo = null;
|
|
67
|
+
if (opts.backup !== false && oldKey) {
|
|
68
|
+
const backupDir = path.join(os.homedir(), ".bootproof", "archived-keys");
|
|
69
|
+
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
|
|
70
|
+
const backupName = `signer-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
|
|
71
|
+
backedUpTo = path.join(backupDir, backupName);
|
|
72
|
+
fs.writeFileSync(backedUpTo, JSON.stringify(oldKey, null, 2), { mode: 0o600 });
|
|
73
|
+
}
|
|
74
|
+
// Write the new key.
|
|
75
|
+
fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
|
|
76
|
+
fs.writeFileSync(p, JSON.stringify({ privateKeyPem: newPrivateKeyPem, publicKeyPem: newPublicKeyPem }), { mode: 0o600 });
|
|
77
|
+
// Optionally re-sign the latest attestation with the new key.
|
|
78
|
+
let reSigned = false;
|
|
79
|
+
if (opts.resignAttestation && opts.repo) {
|
|
80
|
+
const attPath = attestationPath(opts.repo);
|
|
81
|
+
if (fs.existsSync(attPath)) {
|
|
82
|
+
const att = JSON.parse(fs.readFileSync(attPath, "utf8"));
|
|
83
|
+
// Re-sign: the canonical body excludes signature and signer fields.
|
|
84
|
+
const body = canonicalBody(att);
|
|
85
|
+
const newPrivateKey = crypto.createPrivateKey(newPrivateKeyPem);
|
|
86
|
+
att.signature = crypto.sign(null, body, newPrivateKey).toString("base64");
|
|
87
|
+
att.signer = { publicKey: newPublicKeyPem, algorithm: "ed25519" };
|
|
88
|
+
fs.writeFileSync(attPath, JSON.stringify(att, null, 2) + "\n");
|
|
89
|
+
reSigned = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
schema: "bootproof/key-rotation/v1",
|
|
94
|
+
rotatedAt: new Date().toISOString(),
|
|
95
|
+
oldPublicKey: oldPublicKeyPem ?? "(no prior key existed)",
|
|
96
|
+
newPublicKey: newPublicKeyPem,
|
|
97
|
+
backedUpTo,
|
|
98
|
+
reSignedAttestation: reSigned,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function localSignerPublicKey() {
|
|
102
|
+
const p = signerKeyPath();
|
|
103
|
+
if (!fs.existsSync(p))
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
const saved = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
107
|
+
return typeof saved.publicKeyPem === "string" ? saved.publicKeyPem : null;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function emptyKnownSignerStore() {
|
|
114
|
+
return { schema: "bootproof/known-signers/v1", signers: {} };
|
|
115
|
+
}
|
|
116
|
+
function readKnownSignerStore() {
|
|
117
|
+
const p = knownSignersPath();
|
|
118
|
+
if (!fs.existsSync(p))
|
|
119
|
+
return emptyKnownSignerStore();
|
|
120
|
+
try {
|
|
121
|
+
const value = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
122
|
+
if (value.schema !== "bootproof/known-signers/v1" || !value.signers || typeof value.signers !== "object") {
|
|
123
|
+
return emptyKnownSignerStore();
|
|
124
|
+
}
|
|
125
|
+
return { schema: value.schema, signers: value.signers };
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return emptyKnownSignerStore();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function signerFingerprint(publicKeyPem) {
|
|
132
|
+
const publicKey = crypto.createPublicKey(publicKeyPem);
|
|
133
|
+
const spki = publicKey.export({ type: "spki", format: "der" });
|
|
134
|
+
return `sha256:${crypto.createHash("sha256").update(spki).digest("hex")}`;
|
|
135
|
+
}
|
|
136
|
+
export function trustSigner(publicKeyPem, label) {
|
|
137
|
+
const fingerprint = signerFingerprint(publicKeyPem);
|
|
138
|
+
const store = readKnownSignerStore();
|
|
139
|
+
const existing = store.signers[fingerprint];
|
|
140
|
+
const record = existing ?? {
|
|
141
|
+
firstSeenAt: new Date().toISOString(),
|
|
142
|
+
...(label ? { label } : {}),
|
|
143
|
+
};
|
|
144
|
+
if (label)
|
|
145
|
+
record.label = label;
|
|
146
|
+
store.signers[fingerprint] = record;
|
|
147
|
+
const p = knownSignersPath();
|
|
148
|
+
fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
|
|
149
|
+
fs.writeFileSync(p, JSON.stringify(store, null, 2) + "\n", { mode: 0o600 });
|
|
150
|
+
return {
|
|
151
|
+
fingerprint,
|
|
152
|
+
firstSeenAt: record.firstSeenAt,
|
|
153
|
+
label: record.label ?? null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function signerTrust(publicKeyPem) {
|
|
157
|
+
let fingerprint;
|
|
158
|
+
try {
|
|
159
|
+
fingerprint = signerFingerprint(publicKeyPem);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return { tier: "invalid", fingerprint: null, label: null };
|
|
163
|
+
}
|
|
164
|
+
const localPublicKey = localSignerPublicKey();
|
|
165
|
+
if (localPublicKey) {
|
|
166
|
+
try {
|
|
167
|
+
if (signerFingerprint(localPublicKey) === fingerprint) {
|
|
168
|
+
return { tier: "self", fingerprint, label: null };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// A malformed local signer cannot establish trust in a foreign artifact.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const known = readKnownSignerStore().signers[fingerprint];
|
|
176
|
+
if (known)
|
|
177
|
+
return { tier: "known", fingerprint, label: known.label ?? null };
|
|
178
|
+
return { tier: "unknown-foreign", fingerprint, label: null };
|
|
179
|
+
}
|
|
180
|
+
export function evaluateDetachedSignature(body, signature, publicKeyPem) {
|
|
181
|
+
if (!signature || !publicKeyPem || !verifyDetached(body, signature, publicKeyPem)) {
|
|
182
|
+
return { integrityValid: false, tier: "invalid", fingerprint: null, label: null };
|
|
183
|
+
}
|
|
184
|
+
return { integrityValid: true, ...signerTrust(publicKeyPem) };
|
|
185
|
+
}
|
|
42
186
|
function canonicalBody(att) {
|
|
43
187
|
const { signature: _s, signer: _k, ...body } = att;
|
|
44
188
|
return Buffer.from(JSON.stringify(body));
|
|
45
189
|
}
|
|
46
190
|
export function buildAttestation(input) {
|
|
191
|
+
const verificationMode = input.verificationMode ?? "bootproof-orchestrated";
|
|
192
|
+
const bootproofOrchestrated = verificationMode === "external-health"
|
|
193
|
+
? false
|
|
194
|
+
: input.bootproofOrchestrated ?? true;
|
|
195
|
+
const redactionsApplied = new Set();
|
|
196
|
+
const redact = (value) => {
|
|
197
|
+
const redacted = redactJsonValue(value);
|
|
198
|
+
for (const rule of redacted.applied)
|
|
199
|
+
redactionsApplied.add(rule);
|
|
200
|
+
return redacted.value;
|
|
201
|
+
};
|
|
202
|
+
const repo = gitInfo(input.repo);
|
|
203
|
+
const persistedRepo = {
|
|
204
|
+
...repo,
|
|
205
|
+
remote: redact(repo.remote),
|
|
206
|
+
};
|
|
207
|
+
const persistedPlan = redact(input.plan);
|
|
208
|
+
const persistedObserved = redact(input.observed);
|
|
209
|
+
const persistedHealthObservation = redact(input.healthObservation);
|
|
210
|
+
const persistedHealthEvidence = redact(input.healthEvidence ?? null);
|
|
211
|
+
const persistedObservedHealthCandidates = redact(input.observedHealthCandidates ?? []);
|
|
212
|
+
const persistedFailureEvidence = redact(input.failureEvidence);
|
|
213
|
+
const persistedExplanation = redact(input.explanation);
|
|
214
|
+
const persistedExternalHealthUrl = redact(input.externalHealthUrl ?? null);
|
|
215
|
+
const persistedObservedFinalUrl = redact(input.observedFinalUrl ?? null);
|
|
216
|
+
const persistedResponseSnippet = redact(input.responseSnippet ?? "");
|
|
47
217
|
const att = {
|
|
48
218
|
schema: "bootproof/attestation/v1",
|
|
49
219
|
tool: TOOL_ID,
|
|
50
|
-
|
|
220
|
+
verificationMode,
|
|
221
|
+
bootproofOrchestrated,
|
|
222
|
+
externalHealthUrl: persistedExternalHealthUrl,
|
|
223
|
+
observedStatus: input.observedStatus ?? null,
|
|
224
|
+
observedFinalUrl: persistedObservedFinalUrl,
|
|
225
|
+
observedAt: input.observedAt ?? null,
|
|
226
|
+
responseSnippet: persistedResponseSnippet,
|
|
227
|
+
classification: input.classification ?? null,
|
|
228
|
+
redactionsApplied: [...redactionsApplied].sort(),
|
|
229
|
+
repo: persistedRepo,
|
|
51
230
|
environment: { os: `${os.platform()} ${os.release()}`, arch: os.arch(), node: process.version },
|
|
52
|
-
trust: { level: "local_developer_signed", signer: "local_ed25519", oidc: null },
|
|
53
|
-
plan:
|
|
54
|
-
observed:
|
|
231
|
+
trust: input.trust ?? { level: "local_developer_signed", signer: "local_ed25519", oidc: null },
|
|
232
|
+
plan: persistedPlan,
|
|
233
|
+
observed: persistedObserved,
|
|
55
234
|
result: {
|
|
56
235
|
booted: input.booted,
|
|
57
236
|
healthVerified: input.healthVerified,
|
|
58
|
-
healthObservation:
|
|
59
|
-
|
|
237
|
+
healthObservation: persistedHealthObservation,
|
|
238
|
+
healthEvidence: persistedHealthEvidence,
|
|
239
|
+
observedHealthCandidates: persistedObservedHealthCandidates,
|
|
240
|
+
observedPort: persistedPlan.observedPort ?? null,
|
|
241
|
+
healthCandidateSource: persistedPlan.healthCandidateSource ?? "inferred",
|
|
60
242
|
failureClass: input.failureClass,
|
|
61
|
-
failureEvidence:
|
|
62
|
-
explanation:
|
|
243
|
+
failureEvidence: persistedFailureEvidence,
|
|
244
|
+
explanation: persistedExplanation,
|
|
63
245
|
},
|
|
64
246
|
startedAt: input.startedAt,
|
|
65
247
|
finishedAt: new Date().toISOString(),
|
|
@@ -71,6 +253,67 @@ export function buildAttestation(input) {
|
|
|
71
253
|
att.signer = { publicKey: publicKeyPem, algorithm: "ed25519" };
|
|
72
254
|
return att;
|
|
73
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Detect GitHub Actions OIDC environment. Present only when the workflow has
|
|
258
|
+
* `permissions: id-token: write`. The presence of these env vars IS the consent —
|
|
259
|
+
* the workflow author explicitly granted the OIDC scope.
|
|
260
|
+
*/
|
|
261
|
+
export function detectOidcEnv(env = process.env) {
|
|
262
|
+
const url = env.ACTIONS_ID_TOKEN_REQUEST_URL?.trim();
|
|
263
|
+
const token = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN?.trim();
|
|
264
|
+
if (!url || !token)
|
|
265
|
+
return null;
|
|
266
|
+
return { requestUrl: url, requestToken: token };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Fetch the OIDC JWT from GitHub Actions and decode its claims (without verification —
|
|
270
|
+
* verification is the receiver's job, not the signer's). Returns a compact record of
|
|
271
|
+
* claims suitable for embedding in the attestation trust block.
|
|
272
|
+
*/
|
|
273
|
+
export async function fetchOidcClaims(requestUrl, requestToken, fetchImpl = fetch) {
|
|
274
|
+
const url = new URL(requestUrl);
|
|
275
|
+
url.searchParams.set("audience", "bootproof.dev");
|
|
276
|
+
const response = await fetchImpl(url.toString(), {
|
|
277
|
+
headers: { authorization: `bearer ${requestToken}` },
|
|
278
|
+
});
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
throw new Error(`OIDC token request failed with HTTP ${response.status}`);
|
|
281
|
+
}
|
|
282
|
+
const body = await response.json();
|
|
283
|
+
if (!body.value || typeof body.value !== "string") {
|
|
284
|
+
throw new Error("OIDC token response did not contain a 'value' field");
|
|
285
|
+
}
|
|
286
|
+
// Decode the JWT payload (middle segment). No verification — the receiver verifies.
|
|
287
|
+
const parts = body.value.split(".");
|
|
288
|
+
if (parts.length !== 3)
|
|
289
|
+
throw new Error("OIDC token is not a valid JWT");
|
|
290
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
291
|
+
// Extract the claims that matter for provenance. Stringify everything for the schema.
|
|
292
|
+
const claims = {};
|
|
293
|
+
for (const key of ["iss", "sub", "aud", "ref", "repository", "repository_owner", "run_id", "run_attempt", "event_name", "workflow", "job_workflow_ref"]) {
|
|
294
|
+
if (payload[key] !== undefined && payload[key] !== null) {
|
|
295
|
+
claims[key] = String(payload[key]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return claims;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Resolve the trust block for an attestation. When `--ci-oidc` is requested and
|
|
302
|
+
* the GitHub Actions OIDC environment is present, fetch the OIDC token and return
|
|
303
|
+
* a ci_oidc_signed trust block. Otherwise return local_developer_signed.
|
|
304
|
+
*/
|
|
305
|
+
export async function resolveTrust(opts = {}) {
|
|
306
|
+
if (!opts.ciOidc) {
|
|
307
|
+
return { level: "local_developer_signed", signer: "local_ed25519", oidc: null };
|
|
308
|
+
}
|
|
309
|
+
const oidcEnv = detectOidcEnv(opts.env);
|
|
310
|
+
if (!oidcEnv) {
|
|
311
|
+
throw new Error("--ci-oidc was requested but ACTIONS_ID_TOKEN_REQUEST_URL/ACTIONS_ID_TOKEN_REQUEST_TOKEN are not set. "
|
|
312
|
+
+ "Ensure the workflow has `permissions: id-token: write`.");
|
|
313
|
+
}
|
|
314
|
+
const claims = await fetchOidcClaims(oidcEnv.requestUrl, oidcEnv.requestToken, opts.fetchImpl);
|
|
315
|
+
return { level: "ci_oidc_signed", signer: "local_ed25519", oidc: claims };
|
|
316
|
+
}
|
|
74
317
|
export function signDetached(body) {
|
|
75
318
|
const { privateKey, publicKeyPem } = loadOrCreateSigner();
|
|
76
319
|
return { signature: crypto.sign(null, body, privateKey).toString("base64"), publicKeyPem };
|
|
@@ -86,11 +329,21 @@ export function verifyDetached(body, signature, publicKeyPem) {
|
|
|
86
329
|
export function verifySignature(att) {
|
|
87
330
|
if (!att.signature || !att.signer)
|
|
88
331
|
return false;
|
|
332
|
+
return verifyDetached(canonicalBody(att), att.signature, att.signer.publicKey);
|
|
333
|
+
}
|
|
334
|
+
export function evaluateAttestationSignature(att) {
|
|
335
|
+
return evaluateDetachedSignature(canonicalBody(att), att.signature, att.signer?.publicKey);
|
|
336
|
+
}
|
|
337
|
+
export function currentGitHead(repo) {
|
|
89
338
|
try {
|
|
90
|
-
return
|
|
339
|
+
return execFileSync("git", ["-C", repo, "rev-parse", "HEAD"], {
|
|
340
|
+
encoding: "utf8",
|
|
341
|
+
env: buildExecutionEnv(),
|
|
342
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
343
|
+
}).trim();
|
|
91
344
|
}
|
|
92
345
|
catch {
|
|
93
|
-
return
|
|
346
|
+
return null;
|
|
94
347
|
}
|
|
95
348
|
}
|
|
96
349
|
export function attestationPath(repo) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Attestation } from "./types.js";
|
|
2
|
+
interface ReceiptRecord {
|
|
3
|
+
schema: string;
|
|
4
|
+
id: string;
|
|
5
|
+
capturedAt: string;
|
|
6
|
+
capturedBy: string;
|
|
7
|
+
trust: {
|
|
8
|
+
level: string;
|
|
9
|
+
signer: string;
|
|
10
|
+
oidc: string | null;
|
|
11
|
+
upgradePath: string[];
|
|
12
|
+
};
|
|
13
|
+
repo: {
|
|
14
|
+
url: string;
|
|
15
|
+
commit: string;
|
|
16
|
+
branch: string;
|
|
17
|
+
label: string;
|
|
18
|
+
};
|
|
19
|
+
inference: {
|
|
20
|
+
language: string;
|
|
21
|
+
packageManager: string;
|
|
22
|
+
startCommand: string;
|
|
23
|
+
} | null;
|
|
24
|
+
plan: Array<{
|
|
25
|
+
step: string;
|
|
26
|
+
command: string;
|
|
27
|
+
exitCode: number;
|
|
28
|
+
durationMs: number;
|
|
29
|
+
}>;
|
|
30
|
+
log: Array<{
|
|
31
|
+
t: number;
|
|
32
|
+
level: string;
|
|
33
|
+
line: string;
|
|
34
|
+
}>;
|
|
35
|
+
observed: {
|
|
36
|
+
kind: string;
|
|
37
|
+
url?: string;
|
|
38
|
+
status?: number;
|
|
39
|
+
latencyMs?: number;
|
|
40
|
+
body?: string;
|
|
41
|
+
signal?: string | null;
|
|
42
|
+
code?: number;
|
|
43
|
+
windowMs?: number;
|
|
44
|
+
exitCode?: number;
|
|
45
|
+
};
|
|
46
|
+
booted: boolean;
|
|
47
|
+
healthVerified: boolean;
|
|
48
|
+
failureClass: string | null;
|
|
49
|
+
}
|
|
50
|
+
declare function attestationToRecord(att: Attestation): ReceiptRecord;
|
|
51
|
+
export declare function emitLivingReceipt(att: Attestation, outPath: string): string;
|
|
52
|
+
export { attestationToRecord };
|