loopgen 0.6.2 → 0.7.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.
- package/README.md +38 -24
- package/dist/cli.js +46 -7
- package/dist/core/attest.js +184 -0
- package/dist/core/audit.js +1 -1
- package/dist/core/governance-report.js +2 -0
- package/dist/core/governance.js +12 -0
- package/dist/core/report.js +25 -5
- package/dist/core/runner.js +29 -2
- package/examples/github-attested-merge-gate.yml +37 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -4,24 +4,28 @@
|
|
|
4
4
|
[](https://github.com/Nagiici/Loopgen/actions/workflows/ci.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**The vendor-neutral verification & provenance gate for AI-generated code — bring your own agent.**
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
forbidden-path and iteration limits, and
|
|
12
|
-
|
|
9
|
+
Whatever agent writes the code — Claude Code, Codex, Cursor, or a local model — loopgen does the part a
|
|
10
|
+
stateless model can't: it **runs** your real `test` / `lint` / `build`, **gates on the exit codes**,
|
|
11
|
+
**enforces** forbidden-path and iteration limits, and records the run in a hash-chained ledger. Locally
|
|
12
|
+
that's **tamper-evident evidence**; in CI it signs the entry against the **Sigstore/Rekor** public
|
|
13
|
+
transparency log — a **verifiable, signed attestation** you can gate merges on.
|
|
13
14
|
|
|
14
15
|
```bash
|
|
15
16
|
npx loopgen init # scan your repo + pick bounded-loop templates (local wizard, nothing written)
|
|
16
|
-
npx loopgen run test-repair # run the loop
|
|
17
|
+
npx loopgen run test-repair # run + verify the loop; leave tamper-evident evidence (signed in CI) — exits 0/1
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
- ✅ **
|
|
20
|
-
writes
|
|
21
|
-
drives a **local model** itself and **blocks forbidden writes before they land**.
|
|
20
|
+
- ✅ **Verify the work** — `loopgen run` executes a loop, gates on your *real* verification commands, checks
|
|
21
|
+
forbidden paths, and writes a hash-chained audit a chatbot cannot. *Referee* mode verifies any agent's
|
|
22
|
+
change; *driven* mode drives a **local model** itself and **blocks forbidden writes before they land**.
|
|
23
|
+
- 🔏 **Attest the work** — run it in CI and loopgen signs the audit entry against the **Sigstore/Rekor**
|
|
24
|
+
public transparency log (keyless — no keys to manage): a **verifiable, signed attestation** bound to the
|
|
25
|
+
commit, not just a local self-attested log. [Evidence vs. proof →](docs/THREAT-MODEL.md)
|
|
22
26
|
- 🧾 **Govern the agents** — `loopgen audit` rolls up every dev's hash-chained ledger into a report + a
|
|
23
27
|
self-contained HTML dashboard, and a CI **merge gate** (`audit check`, also a GitHub Action) blocks PRs
|
|
24
|
-
that lack passing, untampered proof.
|
|
28
|
+
that lack passing, untampered — and optionally **attested** — proof.
|
|
25
29
|
- 🏠 **Local-first & open source (MIT)** — no telemetry, no cloud; drives only your local models; API keys
|
|
26
30
|
are referenced by env-var name only.
|
|
27
31
|
|
|
@@ -35,20 +39,23 @@ npx loopgen run test-repair # run the loop, verify it, and leave proof it passe
|
|
|
35
39
|
|
|
36
40
|
### 项目简介
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
**面向 AI 生成代码的厂商中立「验证 + 溯源」闸门 —— 自带 agent。** 无论代码由谁写(Claude Code、Codex、
|
|
43
|
+
Cursor 或本地模型),loopgen 做的是无状态模型做不到的那部分:**执行**你真实的 `test` / `lint` / `build`、
|
|
44
|
+
**按退出码判定**、**强制**禁止路径与迭代上限,并把这次运行记进一条哈希链账本。本地运行得到的是**防篡改证据
|
|
45
|
+
(tamper-evident evidence)**;在 CI 里它会把这条记录对 **Sigstore/Rekor** 公开透明日志签名 —— 一份可被
|
|
46
|
+
第三方校验的**签名证明(attestation)**,可直接用于合并闸门。
|
|
42
47
|
|
|
43
48
|
```bash
|
|
44
49
|
npx loopgen init # 扫描仓库 + 选择有界循环模板(本地向导,不写文件)
|
|
45
50
|
npx loopgen run test-repair # 跑这个循环、验证它,并留下「通过」的证据(退出码 0/1,可接 CI)
|
|
46
51
|
```
|
|
47
52
|
|
|
48
|
-
- ✅
|
|
49
|
-
|
|
53
|
+
- ✅ **验证工作** —— `loopgen run` 执行循环、按你的真实验证命令判定、检查禁止路径,写下 chatbot 做不到的
|
|
54
|
+
哈希链审计。*referee* 模式验证任意 agent 的改动;*driven* 模式自己驱动**本地模型**,并在**落盘前拦截禁止路径写入**。
|
|
55
|
+
- 🔏 **签名证明** —— 在 CI 里运行,loopgen 会把审计条目对 **Sigstore/Rekor** 公开透明日志签名(keyless,
|
|
56
|
+
无需自管密钥):一份绑定 commit、**可被第三方校验的签名证明**,而非仅本地自证日志。[证据 vs 证明 →](docs/THREAT-MODEL.md)
|
|
50
57
|
- 🧾 **治理 agent** —— `loopgen audit` 把每个开发者的哈希链账本聚合成报告 + 自包含 HTML 看板,CI **合并闸门**
|
|
51
|
-
(`audit check`,也有现成 GitHub Action)
|
|
58
|
+
(`audit check`,也有现成 GitHub Action)挡住「缺少通过/未被篡改、以及可选未签名证明」的 PR。
|
|
52
59
|
- 🏠 **local-first & 开源(MIT)** —— 无遥测、无云调用;只驱动你的本地模型;API key 仅按环境变量名引用。
|
|
53
60
|
|
|
54
61
|
> 仍可先用内置 demo 预览:`loopgen init` 不需要真实项目、也不会写入文件。生成的可审查文件包括
|
|
@@ -433,11 +440,11 @@ npm run loopgen -- run [loop] [project]
|
|
|
433
440
|
|
|
434
441
|
`apply` always shows a diff first. Without `--yes`, it asks for confirmation before writing files.
|
|
435
442
|
|
|
436
|
-
###
|
|
443
|
+
### Verify & attest the work (`loopgen run`)
|
|
437
444
|
|
|
438
|
-
Generating config is just instructions. **`loopgen run` actually runs the verification and leaves
|
|
439
|
-
something a stateless model cannot do. After you (or any agent — Claude Code,
|
|
440
|
-
bounded change, run:
|
|
445
|
+
Generating config is just instructions. **`loopgen run` actually runs the verification and leaves a
|
|
446
|
+
tamper-evident record** — something a stateless model cannot do. After you (or any agent — Claude Code,
|
|
447
|
+
Cursor, Codex) complete a bounded change, run:
|
|
441
448
|
|
|
442
449
|
```bash
|
|
443
450
|
npm run loopgen -- run test-repair .
|
|
@@ -450,8 +457,13 @@ in `.loopgen/reports/*.md`. The process exits `0` on pass and `1` on fail, so it
|
|
|
450
457
|
|
|
451
458
|
- `--dry-run` — run the checks, write nothing.
|
|
452
459
|
- `--base <ref>` — git ref to diff against (default `HEAD`).
|
|
453
|
-
- Scope: referee mode is **detection, not a sandbox** — it
|
|
454
|
-
and didn't modify forbidden paths; it does not block reads or out-of-tree writes.
|
|
460
|
+
- Scope: referee mode is **detection, not a sandbox** — it records that the change passed your real
|
|
461
|
+
verification and didn't modify forbidden paths; it does not block reads or out-of-tree writes.
|
|
462
|
+
- Trust: locally this is **tamper-evident evidence**. Run it in CI and loopgen signs the audit entry against
|
|
463
|
+
the **Sigstore/Rekor** public transparency log — a **verifiable, signed attestation** bound to the commit.
|
|
464
|
+
Attestation is automatic in CI with an OIDC identity (`--no-attest` to opt out); verify it with `loopgen
|
|
465
|
+
audit verify --attestation`. See [docs/THREAT-MODEL.md](docs/THREAT-MODEL.md) for what each tier does and
|
|
466
|
+
doesn't prove.
|
|
455
467
|
|
|
456
468
|
#### Driven mode — loopgen runs the loop (`--mode driven`)
|
|
457
469
|
|
|
@@ -482,10 +494,12 @@ Every `loopgen run` appends to a per-repo, hash-chained `.loopgen/audit.jsonl`.
|
|
|
482
494
|
those ledgers into team-level, compliance-ready evidence and a CI gate — local-first, no server:
|
|
483
495
|
|
|
484
496
|
```bash
|
|
485
|
-
npm run loopgen -- audit verify #
|
|
497
|
+
npm run loopgen -- audit verify # verify the hash chain is intact (tamper check)
|
|
498
|
+
npm run loopgen -- audit verify --attestation # also cryptographically verify the CI signatures
|
|
486
499
|
npm run loopgen -- audit summary # one repo: pass rate, by loop, violations
|
|
487
500
|
npm run loopgen -- audit aggregate ../repos --html gov.html --report gov.md # roll up many repos/devs
|
|
488
501
|
npm run loopgen -- audit check --require test-repair --require-no-violations --require-chain # CI gate
|
|
502
|
+
npm run loopgen -- audit check --require-attested # gate on a verifiable CI attestation, not just a local log
|
|
489
503
|
```
|
|
490
504
|
|
|
491
505
|
`aggregate` scans the given files/directories for `audit.jsonl`, merges them into one rollup, and can write
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { startLoopgenServer } from "./server.js";
|
|
|
14
14
|
import { DEFAULT_ADAPTER_IDS, parseAdapterIds } from "./core/adapters.js";
|
|
15
15
|
import { runLoop } from "./core/runner.js";
|
|
16
16
|
import { readAuditLog, verifyAuditChain } from "./core/audit.js";
|
|
17
|
+
import { loadAttestationRef, verifyAttestation } from "./core/attest.js";
|
|
17
18
|
import { buildSummary, collectAuditFiles, evaluatePolicy } from "./core/governance.js";
|
|
18
19
|
import { renderGovernanceHtml, renderGovernanceMarkdown } from "./core/governance-report.js";
|
|
19
20
|
import { promises as fsp } from "node:fs";
|
|
@@ -36,7 +37,7 @@ const { version } = require("../package.json");
|
|
|
36
37
|
const program = new Command();
|
|
37
38
|
program
|
|
38
39
|
.name("loopgen")
|
|
39
|
-
.description("
|
|
40
|
+
.description("Verification & provenance gate for AI-generated code — real verification, tamper-evident local evidence, and a verifiable signed attestation in CI. Bring your own agent.")
|
|
40
41
|
.version(version);
|
|
41
42
|
program
|
|
42
43
|
.command("init")
|
|
@@ -168,7 +169,9 @@ program
|
|
|
168
169
|
.option("--openai-compatible-model <model>", "driven mode: OpenAI-compatible model name")
|
|
169
170
|
.option("--openai-compatible-base-url <url>", "driven mode: OpenAI-compatible base URL")
|
|
170
171
|
.option("--openai-compatible-api-key-env <name>", "driven mode: env var name for the API key")
|
|
171
|
-
.
|
|
172
|
+
.option("--attest", "sign the audit entry against Sigstore/Rekor (default: automatic in CI with an OIDC identity)")
|
|
173
|
+
.option("--no-attest", "skip signing the audit entry, even in CI")
|
|
174
|
+
.description("Run a loop's verification against the working tree; leaves tamper-evident evidence locally, or a verifiable signed attestation in CI.")
|
|
172
175
|
.action(async (loop, project, options) => {
|
|
173
176
|
const result = await runLoop({
|
|
174
177
|
projectRoot: path.resolve(project),
|
|
@@ -185,7 +188,8 @@ program
|
|
|
185
188
|
ollamaBaseUrl: options.ollamaBaseUrl,
|
|
186
189
|
openaiCompatibleModel: options.openaiCompatibleModel,
|
|
187
190
|
openaiCompatibleBaseUrl: options.openaiCompatibleBaseUrl,
|
|
188
|
-
openaiCompatibleApiKeyEnv: options.openaiCompatibleApiKeyEnv
|
|
191
|
+
openaiCompatibleApiKeyEnv: options.openaiCompatibleApiKeyEnv,
|
|
192
|
+
attest: options.attest
|
|
189
193
|
});
|
|
190
194
|
if (options.json) {
|
|
191
195
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -199,9 +203,11 @@ const audit = program.command("audit").description("Inspect, aggregate, and gate
|
|
|
199
203
|
audit
|
|
200
204
|
.command("verify")
|
|
201
205
|
.argument("[project]", "project directory", ".")
|
|
202
|
-
.
|
|
203
|
-
.
|
|
204
|
-
|
|
206
|
+
.option("--attestation", "also cryptographically verify external attestations (cosign/Sigstore), not just the local chain")
|
|
207
|
+
.description("Verify the audit hash chain is intact (tamper check); with --attestation, verify the signed proofs too.")
|
|
208
|
+
.action(async (project, options) => {
|
|
209
|
+
const root = path.resolve(project);
|
|
210
|
+
const entries = await readAuditLog(root);
|
|
205
211
|
const chain = verifyAuditChain(entries);
|
|
206
212
|
if (chain.valid) {
|
|
207
213
|
console.log(`Audit chain valid (${entries.length} entries).`);
|
|
@@ -210,6 +216,28 @@ audit
|
|
|
210
216
|
console.log(`Audit chain BROKEN at entry ${chain.brokenAt}.`);
|
|
211
217
|
process.exitCode = 1;
|
|
212
218
|
}
|
|
219
|
+
if (options.attestation) {
|
|
220
|
+
const attested = entries.filter((entry) => entry.provenance?.tier === "attested");
|
|
221
|
+
if (!attested.length) {
|
|
222
|
+
console.log("Attestations: no CI-attested entries to verify.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
let failures = 0;
|
|
226
|
+
for (const entry of attested) {
|
|
227
|
+
const ref = await loadAttestationRef(root, entry.entryId);
|
|
228
|
+
const result = await verifyAttestation({ projectRoot: root, entryHash: entry.hash, ci: entry.provenance?.ci, ref });
|
|
229
|
+
const id = entry.entryId.slice(0, 8);
|
|
230
|
+
if (result.ok)
|
|
231
|
+
console.log(` attestation ${id}… ok${result.reason ? ` (${result.reason})` : ""}`);
|
|
232
|
+
else {
|
|
233
|
+
console.log(` attestation ${id}… FAILED — ${result.reason ?? "unknown"}`);
|
|
234
|
+
failures += 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
console.log(`Attestations: ${attested.length - failures}/${attested.length} verified.`);
|
|
238
|
+
if (failures)
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
}
|
|
213
241
|
});
|
|
214
242
|
audit
|
|
215
243
|
.command("summary")
|
|
@@ -259,6 +287,7 @@ audit
|
|
|
259
287
|
.option("--since <iso>", "only consider runs at/after this ISO timestamp")
|
|
260
288
|
.option("--require-no-violations", "fail if any run modified forbidden paths")
|
|
261
289
|
.option("--require-chain", "fail if the audit chain is broken")
|
|
290
|
+
.option("--require-attested", "fail unless every run carries a CI attestation claim (verify the signatures with `audit verify --attestation`)")
|
|
262
291
|
.description("Gate CI/merge on the audit log; exits 1 if the policy is not satisfied.")
|
|
263
292
|
.action(async (project, options) => {
|
|
264
293
|
const entries = await readAuditLog(path.resolve(project));
|
|
@@ -266,7 +295,8 @@ audit
|
|
|
266
295
|
requireLoops: options.require ? options.require.split(",").map((item) => item.trim()).filter(Boolean) : undefined,
|
|
267
296
|
since: options.since,
|
|
268
297
|
requireNoViolations: options.requireNoViolations,
|
|
269
|
-
requireChainValid: options.requireChain
|
|
298
|
+
requireChainValid: options.requireChain,
|
|
299
|
+
requireAttested: options.requireAttested
|
|
270
300
|
};
|
|
271
301
|
const result = evaluatePolicy(entries, policy);
|
|
272
302
|
if (result.ok) {
|
|
@@ -352,10 +382,19 @@ function printRunResult(result) {
|
|
|
352
382
|
console.log(` report: ${result.reportPath}`);
|
|
353
383
|
if (!result.dryRun)
|
|
354
384
|
console.log(` audit: .loopgen/audit.jsonl (${result.entry.hash.slice(0, 12)}…)`);
|
|
385
|
+
const tier = result.entry.provenance?.tier;
|
|
386
|
+
if (tier === "attested") {
|
|
387
|
+
const signed = result.attestation && result.attestation.method !== "none";
|
|
388
|
+
console.log(` trust: ${signed ? `CI-attested (signed via ${result.attestation.method}) — verify with \`loopgen audit verify --attestation\`` : "attestation requested but no signer available — local evidence only"}`);
|
|
389
|
+
}
|
|
390
|
+
else if (!result.dryRun) {
|
|
391
|
+
console.log(" trust: local evidence (run in CI for a verifiable signed attestation)");
|
|
392
|
+
}
|
|
355
393
|
}
|
|
356
394
|
function printSummary(summary) {
|
|
357
395
|
console.log(`Runs: ${summary.total} (${summary.passed} pass / ${summary.failed} fail) — ${Math.round(summary.passRate * 100)}%`);
|
|
358
396
|
console.log(`Modes: referee ${summary.byMode.referee}, driven ${summary.byMode.driven}`);
|
|
397
|
+
console.log(`Trust: local ${summary.byTier.local}, CI-attested ${summary.byTier.attested}`);
|
|
359
398
|
console.log(`Chain: ${summary.chain.valid ? "valid" : `BROKEN at ${summary.chain.brokenAt}`}`);
|
|
360
399
|
console.log(`Forbidden violations: ${summary.forbiddenViolationRuns} run(s); blocked attempts (prevented): ${summary.blockedAttempts}`);
|
|
361
400
|
for (const [loop, stat] of Object.entries(summary.byLoop).sort((a, b) => b[1].total - a[1].total)) {
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { canonicalize } from "./audit.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
export const ATTESTATION_DIR = path.join(".loopgen", "attestations");
|
|
9
|
+
export const LOOPGEN_PREDICATE_TYPE = "https://github.com/Nagiici/Loopgen/run-attestation/v1";
|
|
10
|
+
const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
|
|
11
|
+
function sha256Hex(value) {
|
|
12
|
+
return createHash("sha256").update(value).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
function errMsg(error) {
|
|
15
|
+
if (error && typeof error === "object") {
|
|
16
|
+
const stderr = error.stderr;
|
|
17
|
+
if (typeof stderr === "string" && stderr.trim())
|
|
18
|
+
return stderr.trim().split("\n").slice(-1)[0];
|
|
19
|
+
if (error instanceof Error)
|
|
20
|
+
return error.message;
|
|
21
|
+
}
|
|
22
|
+
return String(error);
|
|
23
|
+
}
|
|
24
|
+
function isMissingBinary(error) {
|
|
25
|
+
return Boolean(error && typeof error === "object" && error.code === "ENOENT");
|
|
26
|
+
}
|
|
27
|
+
export function detectCiProvenance(env = process.env) {
|
|
28
|
+
if (env.GITHUB_ACTIONS === "true") {
|
|
29
|
+
const ci = {
|
|
30
|
+
provider: "github-actions",
|
|
31
|
+
repo: env.GITHUB_REPOSITORY,
|
|
32
|
+
ref: env.GITHUB_REF,
|
|
33
|
+
commitSha: env.GITHUB_SHA,
|
|
34
|
+
workflow: env.GITHUB_WORKFLOW,
|
|
35
|
+
workflowRef: env.GITHUB_WORKFLOW_REF,
|
|
36
|
+
runId: env.GITHUB_RUN_ID,
|
|
37
|
+
runAttempt: env.GITHUB_RUN_ATTEMPT,
|
|
38
|
+
runnerEnv: env.RUNNER_ENVIRONMENT,
|
|
39
|
+
actor: env.GITHUB_ACTOR
|
|
40
|
+
};
|
|
41
|
+
const canAttest = Boolean(env.ACTIONS_ID_TOKEN_REQUEST_URL && env.ACTIONS_ID_TOKEN_REQUEST_TOKEN) || Boolean(env.SIGSTORE_ID_TOKEN);
|
|
42
|
+
return { ci, canAttest };
|
|
43
|
+
}
|
|
44
|
+
if (env.CI === "true") {
|
|
45
|
+
const ci = { provider: "other-ci", commitSha: env.GITHUB_SHA ?? env.CI_COMMIT_SHA };
|
|
46
|
+
return { ci, canAttest: Boolean(env.SIGSTORE_ID_TOKEN) };
|
|
47
|
+
}
|
|
48
|
+
return { canAttest: Boolean(env.SIGSTORE_ID_TOKEN) };
|
|
49
|
+
}
|
|
50
|
+
// ---------- attestation subject (the digest set the signature binds to) ----------
|
|
51
|
+
export function buildAttestationSubject(git, loop, verification) {
|
|
52
|
+
const loopSpecHash = sha256Hex(canonicalize(loop));
|
|
53
|
+
const verificationDigest = sha256Hex(canonicalize(verification.results.map((r) => ({ command: r.command, exitCode: r.exitCode, timedOut: r.timedOut }))));
|
|
54
|
+
return { commitSha: git.shaAfter, treeClean: git.clean, loopSpecHash, verificationDigest };
|
|
55
|
+
}
|
|
56
|
+
// ---------- default attestor: keyless Sigstore via cosign (best-effort, degrades to local) ----------
|
|
57
|
+
async function cosignAvailable() {
|
|
58
|
+
try {
|
|
59
|
+
await execFileAsync("cosign", ["version"], { timeout: 15_000 });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (isMissingBinary(error))
|
|
64
|
+
return false;
|
|
65
|
+
return true; // present but `version` exited non-zero for some other reason — still usable
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function predicatePathFor(projectRoot, entryId) {
|
|
69
|
+
return path.join(projectRoot, ATTESTATION_DIR, `${entryId}.predicate.json`);
|
|
70
|
+
}
|
|
71
|
+
function bundlePathFor(projectRoot, entryId) {
|
|
72
|
+
return path.join(projectRoot, ATTESTATION_DIR, `${entryId}.sigstore.json`);
|
|
73
|
+
}
|
|
74
|
+
// cosign verify-blob requires an identity; derive it for GitHub OIDC, else signal "local checks only".
|
|
75
|
+
function githubIdentityArgs(ci) {
|
|
76
|
+
if (ci?.provider !== "github-actions" || !ci.repo)
|
|
77
|
+
return undefined;
|
|
78
|
+
const identity = ci.workflowRef
|
|
79
|
+
? `https://github.com/${ci.workflowRef}`
|
|
80
|
+
: `^https://github.com/${ci.repo}/`;
|
|
81
|
+
const flag = ci.workflowRef ? "--certificate-identity" : "--certificate-identity-regexp";
|
|
82
|
+
return [flag, identity, "--certificate-oidc-issuer", GITHUB_OIDC_ISSUER];
|
|
83
|
+
}
|
|
84
|
+
export function createDefaultAttestor() {
|
|
85
|
+
return {
|
|
86
|
+
async produce(req) {
|
|
87
|
+
if (!(await cosignAvailable()))
|
|
88
|
+
return { method: "none" };
|
|
89
|
+
const predicatePath = predicatePathFor(req.projectRoot, req.entryId);
|
|
90
|
+
const bundlePath = bundlePathFor(req.projectRoot, req.entryId);
|
|
91
|
+
await fs.mkdir(path.dirname(predicatePath), { recursive: true });
|
|
92
|
+
const predicate = {
|
|
93
|
+
predicateType: LOOPGEN_PREDICATE_TYPE,
|
|
94
|
+
entryId: req.entryId,
|
|
95
|
+
entryHash: req.entryHash, // binds the signature to the exact audit entry hash
|
|
96
|
+
subject: req.subject,
|
|
97
|
+
ci: req.ci
|
|
98
|
+
};
|
|
99
|
+
await fs.writeFile(predicatePath, `${JSON.stringify(predicate, null, 2)}\n`, "utf8");
|
|
100
|
+
await execFileAsync("cosign", ["sign-blob", "--yes", "--bundle", bundlePath, predicatePath], {
|
|
101
|
+
timeout: 120_000
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
method: "cosign-keyless",
|
|
105
|
+
bundlePath: path.relative(req.projectRoot, bundlePath),
|
|
106
|
+
predicateType: LOOPGEN_PREDICATE_TYPE
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
async verify(req) {
|
|
110
|
+
const { ref } = req;
|
|
111
|
+
if (ref.method !== "cosign-keyless" || !ref.bundlePath) {
|
|
112
|
+
return { ok: false, reason: "no external attestation bundle recorded for this entry" };
|
|
113
|
+
}
|
|
114
|
+
const bundlePath = path.join(req.projectRoot, ref.bundlePath);
|
|
115
|
+
const predicatePath = bundlePath.replace(/\.sigstore\.json$/, ".predicate.json");
|
|
116
|
+
const predicateRaw = await fs.readFile(predicatePath, "utf8").catch(() => undefined);
|
|
117
|
+
if (!predicateRaw)
|
|
118
|
+
return { ok: false, reason: "attestation predicate file is missing" };
|
|
119
|
+
const predicate = JSON.parse(predicateRaw);
|
|
120
|
+
// Local cross-checks: the signed payload must bind the live entry hash + the recorded CI identity.
|
|
121
|
+
if (predicate.entryHash !== req.entryHash) {
|
|
122
|
+
return { ok: false, reason: `signed entryHash ${predicate.entryHash} != audit entry hash ${req.entryHash}` };
|
|
123
|
+
}
|
|
124
|
+
if (req.ci?.repo && predicate.ci?.repo && req.ci.repo !== predicate.ci.repo) {
|
|
125
|
+
return { ok: false, reason: "signed CI repo does not match the audit entry's CI repo" };
|
|
126
|
+
}
|
|
127
|
+
if (!(await cosignAvailable())) {
|
|
128
|
+
return { ok: true, reason: "predicate cross-check passed; cosign not installed, signature not cryptographically verified" };
|
|
129
|
+
}
|
|
130
|
+
const identityArgs = githubIdentityArgs(predicate.ci ?? req.ci);
|
|
131
|
+
if (!identityArgs) {
|
|
132
|
+
return { ok: true, reason: "predicate cross-check passed; non-GitHub identity, cosign identity not constructed" };
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await execFileAsync("cosign", ["verify-blob", "--bundle", bundlePath, ...identityArgs, predicatePath], {
|
|
136
|
+
timeout: 120_000
|
|
137
|
+
});
|
|
138
|
+
return { ok: true };
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return { ok: false, reason: `cosign verify-blob failed: ${errMsg(error)}` };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Re-derive the attestation reference for an entry from its deterministic sibling-bundle path.
|
|
147
|
+
// (The AttestationRef is out-of-band — not in the hashed entry — so verify reconstructs it by entryId.)
|
|
148
|
+
export async function loadAttestationRef(projectRoot, entryId) {
|
|
149
|
+
const bundlePath = bundlePathFor(projectRoot, entryId);
|
|
150
|
+
const exists = await fs
|
|
151
|
+
.stat(bundlePath)
|
|
152
|
+
.then(() => true)
|
|
153
|
+
.catch(() => false);
|
|
154
|
+
if (!exists)
|
|
155
|
+
return { method: "none" };
|
|
156
|
+
return { method: "cosign-keyless", bundlePath: path.relative(projectRoot, bundlePath), predicateType: LOOPGEN_PREDICATE_TYPE };
|
|
157
|
+
}
|
|
158
|
+
// ---------- top-level wrappers used by runner.ts / cli.ts ----------
|
|
159
|
+
// Produce an attestation. NEVER throws — a failed/unavailable signer must not fail a passing run; it
|
|
160
|
+
// just leaves the run at local-evidence grade (method: "none").
|
|
161
|
+
export async function produceAttestation(opts) {
|
|
162
|
+
const attestor = opts.attestor ?? createDefaultAttestor();
|
|
163
|
+
try {
|
|
164
|
+
return await attestor.produce({
|
|
165
|
+
projectRoot: opts.projectRoot,
|
|
166
|
+
entryId: opts.entryId,
|
|
167
|
+
entryHash: opts.entryHash,
|
|
168
|
+
subject: opts.subject,
|
|
169
|
+
ci: opts.ci
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return { method: "none" };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export async function verifyAttestation(opts) {
|
|
177
|
+
const attestor = opts.attestor ?? createDefaultAttestor();
|
|
178
|
+
try {
|
|
179
|
+
return await attestor.verify({ projectRoot: opts.projectRoot, entryHash: opts.entryHash, ci: opts.ci, ref: opts.ref });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return { ok: false, reason: errMsg(error) };
|
|
183
|
+
}
|
|
184
|
+
}
|
package/dist/core/audit.js
CHANGED
|
@@ -5,7 +5,7 @@ export const AUDIT_FILE = ".loopgen/audit.jsonl";
|
|
|
5
5
|
// Deterministic JSON: object keys sorted recursively so the hash is stable across runs/machines.
|
|
6
6
|
// undefined-valued keys are skipped (matching JSON.stringify) so the in-memory hash equals the hash
|
|
7
7
|
// recomputed from the JSON-round-tripped entry — otherwise dropped keys would break the chain.
|
|
8
|
-
function canonicalize(value) {
|
|
8
|
+
export function canonicalize(value) {
|
|
9
9
|
if (value === null || typeof value !== "object")
|
|
10
10
|
return JSON.stringify(value);
|
|
11
11
|
if (Array.isArray(value))
|
|
@@ -16,6 +16,7 @@ export function renderGovernanceMarkdown(summary) {
|
|
|
16
16
|
- Runs: **${summary.total}** (${summary.passed} passed / ${summary.failed} failed) — pass rate **${pct(summary.passRate)}**
|
|
17
17
|
- Window: ${summary.firstAt ?? "—"} → ${summary.lastAt ?? "—"}
|
|
18
18
|
- Modes: referee ${summary.byMode.referee} · driven ${summary.byMode.driven}
|
|
19
|
+
- Trust: local evidence ${summary.byTier.local} · **CI-attested** ${summary.byTier.attested}
|
|
19
20
|
- Chain integrity: ${summary.chain.valid ? "**valid**" : `**BROKEN** (entry ${summary.chain.brokenAt})`}
|
|
20
21
|
- Forbidden-path violations: **${summary.forbiddenViolationRuns}** run(s)
|
|
21
22
|
- Blocked attempts (driven, prevented at apply time): **${summary.blockedAttempts}**
|
|
@@ -88,6 +89,7 @@ code{font-family:"IBM Plex Mono",monospace;font-size:13px}
|
|
|
88
89
|
<div class="card"><div class="k">Forbidden violations</div><div class="v ${summary.forbiddenViolationRuns ? "bad" : "good"}">${summary.forbiddenViolationRuns}</div></div>
|
|
89
90
|
<div class="card"><div class="k">Blocked (prevented)</div><div class="v">${summary.blockedAttempts}</div></div>
|
|
90
91
|
<div class="card"><div class="k">Chain</div><div class="v ${summary.chain.valid ? "good" : "bad"}">${summary.chain.valid ? "valid" : "BROKEN"}</div></div>
|
|
92
|
+
<div class="card"><div class="k">CI-attested</div><div class="v">${summary.byTier.attested}/${summary.total}</div></div>
|
|
91
93
|
</div>
|
|
92
94
|
<h2>By loop</h2>
|
|
93
95
|
<table><thead><tr><th>Loop</th><th>Runs</th><th>Passed</th><th>Pass rate</th></tr></thead><tbody>${loopRows || '<tr><td colspan="4">No runs.</td></tr>'}</tbody></table>
|
package/dist/core/governance.js
CHANGED
|
@@ -55,6 +55,7 @@ function summarizeEntries(entries, sources, chain) {
|
|
|
55
55
|
const byLoop = {};
|
|
56
56
|
const byActor = {};
|
|
57
57
|
const byMode = { referee: 0, driven: 0 };
|
|
58
|
+
const byTier = { local: 0, attested: 0 };
|
|
58
59
|
let passed = 0;
|
|
59
60
|
let blockedAttempts = 0;
|
|
60
61
|
let forbiddenViolationRuns = 0;
|
|
@@ -67,6 +68,10 @@ function summarizeEntries(entries, sources, chain) {
|
|
|
67
68
|
byMode.driven += 1;
|
|
68
69
|
else
|
|
69
70
|
byMode.referee += 1;
|
|
71
|
+
if (entry.provenance?.tier === "attested")
|
|
72
|
+
byTier.attested += 1;
|
|
73
|
+
else
|
|
74
|
+
byTier.local += 1;
|
|
70
75
|
byLoop[entry.loopId] ??= { total: 0, passed: 0 };
|
|
71
76
|
byLoop[entry.loopId].total += 1;
|
|
72
77
|
if (entry.passed)
|
|
@@ -95,6 +100,7 @@ function summarizeEntries(entries, sources, chain) {
|
|
|
95
100
|
passRate: total ? passed / total : 0,
|
|
96
101
|
byLoop,
|
|
97
102
|
byMode,
|
|
103
|
+
byTier,
|
|
98
104
|
byActor,
|
|
99
105
|
blockedAttempts,
|
|
100
106
|
forbiddenViolationRuns,
|
|
@@ -118,6 +124,12 @@ export function evaluatePolicy(entries, policy) {
|
|
|
118
124
|
if (violations.length)
|
|
119
125
|
failures.push(`${violations.length} run(s) modified forbidden paths`);
|
|
120
126
|
}
|
|
127
|
+
if (policy.requireAttested) {
|
|
128
|
+
// Checks the in-band claim only; cryptographic verification is `loopgen audit verify --attestation`.
|
|
129
|
+
const notAttested = scoped.filter((entry) => entry.provenance?.tier !== "attested");
|
|
130
|
+
if (notAttested.length)
|
|
131
|
+
failures.push(`${notAttested.length} run(s) are not CI-attested (local self-attested only)`);
|
|
132
|
+
}
|
|
121
133
|
for (const loopId of policy.requireLoops ?? []) {
|
|
122
134
|
const ok = scoped.some((entry) => entry.loopId === loopId && entry.passed);
|
|
123
135
|
if (!ok)
|
package/dist/core/report.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function renderProofReport(loop, entry, verification, iterationLogs) {
|
|
1
|
+
export function renderProofReport(loop, entry, verification, iterationLogs, attestation) {
|
|
2
2
|
const banner = entry.passed ? "✅ PASS" : "❌ FAIL";
|
|
3
3
|
const changed = [...entry.changedFiles.tracked, ...entry.changedFiles.untracked];
|
|
4
4
|
const commandBlocks = verification.results
|
|
@@ -21,6 +21,7 @@ Loop: \`${entry.loopId}\` · Mode: ${entry.mode} · Iterations: ${entry.iteratio
|
|
|
21
21
|
Generated: ${entry.timestamp}
|
|
22
22
|
By: ${entry.actor.user ?? "unknown"}@${entry.actor.host ?? "unknown"}
|
|
23
23
|
Audit entry: \`${entry.hash}\`
|
|
24
|
+
Trust: ${trustLine(entry, attestation)}
|
|
24
25
|
|
|
25
26
|
## Goal
|
|
26
27
|
|
|
@@ -51,7 +52,7 @@ ${forbiddenSection}
|
|
|
51
52
|
${entry.driven && iterationLogs ? `\n${renderIterationHistory(iterationLogs)}` : ""}
|
|
52
53
|
---
|
|
53
54
|
|
|
54
|
-
${scopeFooter(entry)}
|
|
55
|
+
${scopeFooter(entry, attestation)}
|
|
55
56
|
`;
|
|
56
57
|
}
|
|
57
58
|
function renderIterationHistory(iterationLogs) {
|
|
@@ -75,9 +76,28 @@ ${log.reasoning ? `> ${log.reasoning}\n` : ""}- Applied: ${applied}
|
|
|
75
76
|
});
|
|
76
77
|
return `## Iteration history\n\n${blocks.join("\n\n")}\n`;
|
|
77
78
|
}
|
|
78
|
-
function scopeFooter(entry) {
|
|
79
|
+
function scopeFooter(entry, attestation) {
|
|
79
80
|
if (entry.driven) {
|
|
80
|
-
return `> Scope: **bounded + enforced**. loopgen drove a local model (${entry.driven.model.adapter} · ${entry.driven.model.modelName}), blocked forbidden writes and non-allowlisted commands **at apply time**, bounded iterations, and verified each one (stop reason: ${entry.driven.stopReason}). The model still proposes actions — this is enforcement, not a sandbox
|
|
81
|
+
return `> Scope: **bounded + enforced**. loopgen drove a local model (${entry.driven.model.adapter} · ${entry.driven.model.modelName}), blocked forbidden writes and non-allowlisted commands **at apply time**, bounded iterations, and verified each one (stop reason: ${entry.driven.stopReason}). The model still proposes actions — this is enforcement, not a sandbox.${trustFooter(entry, attestation)}`;
|
|
81
82
|
}
|
|
82
|
-
return `> Scope: this is **detection, not prevention**. loopgen ran the verification commands above and diffed the working tree after the work session; it does not sandbox the agent or block reads
|
|
83
|
+
return `> Scope: this is **detection, not prevention**. loopgen ran the verification commands above and diffed the working tree after the work session; it does not sandbox the agent or block reads.${trustFooter(entry, attestation)}`;
|
|
84
|
+
}
|
|
85
|
+
// Evidence (local) vs proof (CI-attested) — stated precisely so the report never over-claims.
|
|
86
|
+
function isSigned(entry, attestation) {
|
|
87
|
+
return entry.provenance?.tier === "attested" && Boolean(attestation && attestation.method !== "none");
|
|
88
|
+
}
|
|
89
|
+
function trustLine(entry, attestation) {
|
|
90
|
+
if (isSigned(entry, attestation)) {
|
|
91
|
+
return "**attested** (CI) — audit hash signed against Sigstore/Rekor; verify with `loopgen audit verify --attestation`";
|
|
92
|
+
}
|
|
93
|
+
if (entry.provenance?.tier === "attested") {
|
|
94
|
+
return "attested (CI) requested, but no signer was available — **local evidence only**";
|
|
95
|
+
}
|
|
96
|
+
return "**local** — tamper-evident evidence (re-run in CI for a verifiable signed attestation)";
|
|
97
|
+
}
|
|
98
|
+
function trustFooter(entry, attestation) {
|
|
99
|
+
if (isSigned(entry, attestation)) {
|
|
100
|
+
return ` This run is **CI-attested**: the audit entry hash is signed against the Sigstore/Rekor public transparency log and bound to commit \`${entry.git.shaAfter ?? "(none)"}\` — **verifiable proof** (\`loopgen audit verify --attestation\`).`;
|
|
101
|
+
}
|
|
102
|
+
return ` The audit entry is hash-chained in \`.loopgen/audit.jsonl\` — **tamper-evident local evidence**, not signed proof. Re-run in CI for a verifiable signed attestation.`;
|
|
83
103
|
}
|
package/dist/core/runner.js
CHANGED
|
@@ -3,6 +3,7 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { runDrivenLoop } from "./agent-loop.js";
|
|
6
|
+
import { buildAttestationSubject, detectCiProvenance, produceAttestation } from "./attest.js";
|
|
6
7
|
import { appendAuditEntry, hashEntry, readAuditLog } from "./audit.js";
|
|
7
8
|
import { checkForbiddenPaths } from "./forbidden.js";
|
|
8
9
|
import * as git from "./git.js";
|
|
@@ -40,6 +41,7 @@ async function runReferee(projectRoot, loop, loopFile, options) {
|
|
|
40
41
|
});
|
|
41
42
|
const passed = verification.passed && forbidden.ok;
|
|
42
43
|
const input = baseEntry(loopFile, loop, "referee", { base, shaBefore, shaAfter, clean }, changed, diffstat, forbidden, verification, 1, passed);
|
|
44
|
+
input.provenance = resolveProvenance(options, { shaAfter, clean }, loop, verification);
|
|
43
45
|
return finalize(projectRoot, loop, input, verification, forbidden, options);
|
|
44
46
|
}
|
|
45
47
|
async function runDriven(projectRoot, loop, loopFile, options) {
|
|
@@ -75,6 +77,7 @@ async function runDriven(projectRoot, loop, loopFile, options) {
|
|
|
75
77
|
const passed = driven.passed && forbidden.ok;
|
|
76
78
|
const input = baseEntry(loopFile, loop, "driven", { base, shaBefore, shaAfter, clean }, changed, diffstat, forbidden, verification, driven.iterations.length, passed);
|
|
77
79
|
input.driven = { stopReason: driven.stopReason, model: modelMeta, attempts: summarizeIterations(driven.iterations) };
|
|
80
|
+
input.provenance = resolveProvenance(options, { shaAfter, clean }, loop, verification);
|
|
78
81
|
return finalize(projectRoot, loop, input, verification, forbidden, options, driven.iterations);
|
|
79
82
|
}
|
|
80
83
|
function baseEntry(loopFile, loop, mode, gitInfo, changed, diffstat, forbidden, verification, iterations, passed) {
|
|
@@ -112,13 +115,26 @@ async function finalize(projectRoot, loop, input, verification, forbidden, optio
|
|
|
112
115
|
else {
|
|
113
116
|
entry = await appendAuditEntry(projectRoot, input);
|
|
114
117
|
}
|
|
118
|
+
// Root of trust: the signature's subject IS entry.hash, so this runs AFTER the entry is hashed and
|
|
119
|
+
// appended. A missing/failed signer never fails the run — it just leaves local-grade evidence.
|
|
120
|
+
let attestation;
|
|
121
|
+
if (!options.dryRun && entry.provenance?.tier === "attested" && entry.provenance.subject) {
|
|
122
|
+
attestation = await produceAttestation({
|
|
123
|
+
projectRoot,
|
|
124
|
+
entryId: entry.entryId,
|
|
125
|
+
entryHash: entry.hash,
|
|
126
|
+
subject: entry.provenance.subject,
|
|
127
|
+
ci: entry.provenance.ci,
|
|
128
|
+
attestor: options.attestor
|
|
129
|
+
});
|
|
130
|
+
}
|
|
115
131
|
let reportPath;
|
|
116
132
|
if (!options.dryRun && options.writeReport !== false) {
|
|
117
133
|
const stamp = entry.timestamp.replace(/[:.]/g, "-");
|
|
118
134
|
reportPath = path.join(".loopgen", "reports", `${loop.id}-${stamp}.md`);
|
|
119
135
|
const absolute = path.join(projectRoot, reportPath);
|
|
120
136
|
await fs.mkdir(path.dirname(absolute), { recursive: true });
|
|
121
|
-
await fs.writeFile(absolute, renderProofReport(loop, entry, verification, iterationLogs), "utf8");
|
|
137
|
+
await fs.writeFile(absolute, renderProofReport(loop, entry, verification, iterationLogs, attestation), "utf8");
|
|
122
138
|
}
|
|
123
139
|
if (!options.dryRun) {
|
|
124
140
|
await appendStateEntry(projectRoot, loop, entry);
|
|
@@ -131,9 +147,20 @@ async function finalize(projectRoot, loop, input, verification, forbidden, optio
|
|
|
131
147
|
forbidden,
|
|
132
148
|
reportPath,
|
|
133
149
|
dryRun: Boolean(options.dryRun),
|
|
134
|
-
iterationLogs
|
|
150
|
+
iterationLogs,
|
|
151
|
+
attestation
|
|
135
152
|
};
|
|
136
153
|
}
|
|
154
|
+
// Capture the trust tier + CI/OIDC claims + the digest set a signature will bind to.
|
|
155
|
+
// All fields are known BEFORE the entry is hashed, so this rides the chain like `driven`.
|
|
156
|
+
function resolveProvenance(options, gitInfo, loop, verification) {
|
|
157
|
+
const { ci, canAttest } = detectCiProvenance();
|
|
158
|
+
const subject = buildAttestationSubject(gitInfo, loop, verification);
|
|
159
|
+
// Default: auto-attest when an ambient OIDC identity exists; --attest forces, --no-attest disables.
|
|
160
|
+
const requested = options.attest === undefined ? canAttest : options.attest;
|
|
161
|
+
const tier = requested && Boolean(ci) && canAttest ? "attested" : "local";
|
|
162
|
+
return { tier, ci, subject };
|
|
163
|
+
}
|
|
137
164
|
function summarizeIterations(logs) {
|
|
138
165
|
return logs.map((log) => ({
|
|
139
166
|
iteration: log.iteration,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Reference workflow — produce a VERIFIABLE signed attestation for an AI-assisted change, then gate the
|
|
2
|
+
# merge on it. Copy into your repo's .github/workflows/.
|
|
3
|
+
#
|
|
4
|
+
# Why id-token: write? It hands the job a GitHub OIDC identity, so cosign can sign the audit entry hash
|
|
5
|
+
# keylessly against the Sigstore / Rekor PUBLIC transparency log — loopgen holds no keys. The signature
|
|
6
|
+
# binds an accountable CI identity (repo + workflow) to the exact commit + verification result.
|
|
7
|
+
# See docs/THREAT-MODEL.md for what this does and does not prove.
|
|
8
|
+
name: loopgen attested gate
|
|
9
|
+
on: [pull_request]
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write # GitHub OIDC identity → keyless Sigstore signing
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
attested-gate:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: 20
|
|
23
|
+
- uses: sigstore/cosign-installer@v3 # the keyless signer used by `loopgen run --attest`
|
|
24
|
+
- run: npm install -g loopgen
|
|
25
|
+
|
|
26
|
+
# Run the loop's verification IN CI and sign the audit entry against Sigstore/Rekor.
|
|
27
|
+
# In CI with an OIDC identity, attestation is automatic; pass --no-attest to opt out.
|
|
28
|
+
- run: loopgen run test-repair --attest
|
|
29
|
+
|
|
30
|
+
# Gate the merge on a CRYPTOGRAPHICALLY VERIFIABLE attestation — not just a local self-attested log.
|
|
31
|
+
# require-attested makes the action install cosign and run `audit verify --attestation`.
|
|
32
|
+
- uses: Nagiici/Loopgen/.github/actions/audit-check@v0.7.0
|
|
33
|
+
with:
|
|
34
|
+
require: test-repair
|
|
35
|
+
require-no-violations: "true"
|
|
36
|
+
require-chain: "true"
|
|
37
|
+
require-attested: "true"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopgen",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "The vendor-neutral verification & provenance gate for AI-generated code — bring your own agent. Local runs leave tamper-evident evidence; CI runs produce a verifiable, signed attestation (Sigstore/SLSA) you can gate merges on.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=18"
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"keywords": [
|
|
29
29
|
"ai-agents",
|
|
30
30
|
"verification",
|
|
31
|
+
"provenance",
|
|
32
|
+
"attestation",
|
|
33
|
+
"sigstore",
|
|
34
|
+
"slsa",
|
|
35
|
+
"supply-chain",
|
|
31
36
|
"audit",
|
|
32
37
|
"ci",
|
|
33
38
|
"governance",
|