altimate-receipts 0.16.0 → 0.18.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 +3 -0
- package/dist/{chunk-MBLQFVDR.js → chunk-DNKSESAA.js} +120 -132
- package/dist/chunk-DNKSESAA.js.map +1 -0
- package/dist/{chunk-XLL4N2XR.js → chunk-FPDXOAYN.js} +576 -122
- package/dist/chunk-FPDXOAYN.js.map +1 -0
- package/dist/chunk-IDAIDLZT.js +105 -0
- package/dist/chunk-IDAIDLZT.js.map +1 -0
- package/dist/{chunk-LGRPVOYZ.js → chunk-UTOGDDME.js} +2 -2
- package/dist/chunk-YO6MVZTD.js +175 -0
- package/dist/chunk-YO6MVZTD.js.map +1 -0
- package/dist/cli.js +184 -52
- package/dist/cli.js.map +1 -1
- package/dist/gen-catalog.js +22 -0
- package/dist/gen-catalog.js.map +1 -0
- package/dist/index.js +4 -2
- package/dist/mcp/server.js +3 -2
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
- package/schema/agent-execution-receipt-v1.json +45 -0
- package/dist/chunk-MBLQFVDR.js.map +0 -1
- package/dist/chunk-XLL4N2XR.js.map +0 -1
- /package/dist/{chunk-LGRPVOYZ.js.map → chunk-UTOGDDME.js.map} +0 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
destructiveOutsideRepo,
|
|
4
|
+
regenerableBuildArtifact,
|
|
5
|
+
regenerableReceiptRm
|
|
6
|
+
} from "./chunk-IDAIDLZT.js";
|
|
2
7
|
|
|
3
8
|
// src/findings/gitParse.ts
|
|
4
9
|
var WRAPPER = /* @__PURE__ */ new Set(["sudo", "env", "nohup", "command", "stdbuf", "time", "timeout", "xargs"]);
|
|
@@ -177,8 +182,8 @@ function shellExecutorArg(clause) {
|
|
|
177
182
|
}
|
|
178
183
|
return void 0;
|
|
179
184
|
}
|
|
180
|
-
function cwdAtFirstGit(command,
|
|
181
|
-
let cur =
|
|
185
|
+
function cwdAtFirstGit(command, base6) {
|
|
186
|
+
let cur = base6;
|
|
182
187
|
let moved = false;
|
|
183
188
|
let unknown = false;
|
|
184
189
|
for (const clause of splitClauses(stripHeredocs(command))) {
|
|
@@ -1061,8 +1066,8 @@ function isInScope(path2, promptLc, readSet) {
|
|
|
1061
1066
|
if (promptLc.includes(lc)) {
|
|
1062
1067
|
return true;
|
|
1063
1068
|
}
|
|
1064
|
-
const
|
|
1065
|
-
if (
|
|
1069
|
+
const base6 = lc.split("/").pop();
|
|
1070
|
+
if (base6 && base6.length > 2 && promptLc.includes(base6)) {
|
|
1066
1071
|
return true;
|
|
1067
1072
|
}
|
|
1068
1073
|
const dirs = lc.split("/").filter((d) => d.length > 2);
|
|
@@ -1362,10 +1367,74 @@ var CODE_EXT = /\.(?:py|js|ts|tsx|jsx|go|rs|rb|java|kt|sh|bash|toml|cfg|ini|mk|g
|
|
|
1362
1367
|
var TEST_FOCUS = /\b(?:it|describe|test|context)\.only\s*\(|\bfdescribe\s*\(|\bfit\s*\(/;
|
|
1363
1368
|
var CONFIG_FILE = /(?:^|\/)(?:tsconfig[^/]*\.json|\.eslintrc[^/]*|eslint\.config\.[a-z]+|\.flake8|setup\.cfg|pyproject\.toml|jest\.config\.[a-z]+|vitest\.config\.[a-z]+|\.pre-commit-config\.ya?ml)$/i;
|
|
1364
1369
|
var CONFIG_WEAKEN = /"strict"\s*:\s*false|"noImplicitAny"\s*:\s*false|"strictNullChecks"\s*:\s*false|"skipLibCheck"\s*:\s*true|:\s*["']off["']|coverageThreshold|--passWithNoTests/i;
|
|
1370
|
+
var STEERING_FILE = /(?:^|\/)(?:CLAUDE|AGENTS|GEMINI|MEMORY|\.cursorrules|\.windsurfrules|\.clinerules|copilot-instructions)(?:\.md)?$/i;
|
|
1371
|
+
var SELF_PERMISSION_BYPASS = /\b(?:skip|disable|bypass|don'?t\s+(?:run|bother(?:\s+with)?|worry\s+about)|no\s+need\s+to\s+(?:run|add|write)|you\s+(?:may|can|don'?t\s+(?:have|need)\s+to)\s+(?:skip|disable|bypass|run))\b[^.\n]{0,40}\b(?:tests?|test\s+suite|checks?|lint(?:er|ing)?|hooks?|pre-?commit|ci\b|verif\w+|coverage|type[\s-]?check\w*)\b/i;
|
|
1365
1372
|
var CICD_FILE = /(?:^|\/)(?:\.github\/workflows\/[^/]+\.ya?ml|\.gitlab-ci\.yml|Jenkinsfile(?:\.[\w.]+)?|\.circleci\/config\.yml|azure-pipelines\.yml|bitbucket-pipelines\.yml|\.drone\.yml)$/i;
|
|
1366
1373
|
function base2(p) {
|
|
1367
1374
|
return p.split("/").pop() || p;
|
|
1368
1375
|
}
|
|
1376
|
+
var CONTROL_FILE = /(?:^|\/)\.claude\/settings(?:\.[A-Za-z0-9_-]+)*\.json$|(?:^|\/)\.claude\/agents\/[^/]+\.md$|(?:^|\/)\.claude\/hooks\/|(?:^|\/)\.githooks\/[^/]+$|(?:^|\/)\.git\/hooks\/[^/]+$|(?:^|\/)policy\/guardrails\.policy\.json$/i;
|
|
1377
|
+
function bashWritesControlFile(cmd) {
|
|
1378
|
+
const redir = cmd.match(/(?:>>?|\btee\b)\s+(?:-\S+\s+)*["']?([^\s"'|;&<>]+)/);
|
|
1379
|
+
if (redir && CONTROL_FILE.test(redir[1])) {
|
|
1380
|
+
return redir[1];
|
|
1381
|
+
}
|
|
1382
|
+
const tokens = cmd.split(/[\s|;&]+/).map((t) => t.replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
1383
|
+
if (/\bsed\b[^|]*\s-i\b/.test(cmd)) {
|
|
1384
|
+
const t = tokens.find((x) => !x.startsWith("-") && CONTROL_FILE.test(x));
|
|
1385
|
+
if (t) {
|
|
1386
|
+
return t;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (/\b(?:cp|mv|install|rsync)\b/.test(cmd)) {
|
|
1390
|
+
const ops = tokens.filter((t) => !t.startsWith("-") && !/^(?:cp|mv|install|rsync)$/.test(t));
|
|
1391
|
+
const dest = ops[ops.length - 1];
|
|
1392
|
+
if (dest && CONTROL_FILE.test(dest)) {
|
|
1393
|
+
return dest;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return void 0;
|
|
1397
|
+
}
|
|
1398
|
+
function deriveSelfPermission(sum) {
|
|
1399
|
+
const out = [];
|
|
1400
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1401
|
+
const push = (spanId, path2, how) => {
|
|
1402
|
+
if (seen.has(path2)) {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
seen.add(path2);
|
|
1406
|
+
out.push({
|
|
1407
|
+
id: `self-permission-${spanId}`,
|
|
1408
|
+
severity: "critical",
|
|
1409
|
+
title: `Edited its own agent controls: ${base2(path2)}`,
|
|
1410
|
+
detail: `\`${path2}\` is one of the agent's own Claude Code control files (permission allow-list, subagent tool allow-list, or a hook script) \u2014 it was written ${how} this session. Changing it loosens the very fence the agent runs inside. Confirm the agent was meant to change its own permissions/hooks; revert if not.`,
|
|
1411
|
+
impactLabel: "self-permissioning",
|
|
1412
|
+
confidence: 0.9,
|
|
1413
|
+
score: 1e3 * 0.9 * 0.9,
|
|
1414
|
+
evidenceSpanId: spanId,
|
|
1415
|
+
filePath: path2,
|
|
1416
|
+
guardrailRule: "Don't let an agent edit its own .claude/settings, agent definitions, or hooks \u2014 change those yourself, in review."
|
|
1417
|
+
});
|
|
1418
|
+
};
|
|
1419
|
+
for (const s of sum.spans) {
|
|
1420
|
+
if (s.kind !== "tool") {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
if (isEditTool(s.name)) {
|
|
1424
|
+
const fp = filePathOf(s.input);
|
|
1425
|
+
if (fp && CONTROL_FILE.test(fp)) {
|
|
1426
|
+
push(s.spanId, fp, "via an edit");
|
|
1427
|
+
}
|
|
1428
|
+
} else if (isCommandTool(s.name)) {
|
|
1429
|
+
const cmd = commandOf(s.input);
|
|
1430
|
+
const target = cmd ? bashWritesControlFile(cmd) : void 0;
|
|
1431
|
+
if (target) {
|
|
1432
|
+
push(s.spanId, target, `via the command \`${cmd.trim().slice(0, 120)}\``);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return out;
|
|
1437
|
+
}
|
|
1369
1438
|
function deriveBypassFindings(sum) {
|
|
1370
1439
|
const out = [];
|
|
1371
1440
|
const tools = sum.spans.filter((s) => s.kind === "tool").sort((a, b) => a.startTime - b.startTime || a.spanId.localeCompare(b.spanId));
|
|
@@ -1374,6 +1443,11 @@ function deriveBypassFindings(sum) {
|
|
|
1374
1443
|
let focus;
|
|
1375
1444
|
let configWeaken;
|
|
1376
1445
|
let ciCdTouch;
|
|
1446
|
+
let specTamper;
|
|
1447
|
+
const promptLc = (sum.prompt ?? "").toLowerCase();
|
|
1448
|
+
const editingInstructionsIsTask = /\b(?:updat|edit|chang|add(?:ing)?|writ|revis|amend|fix)\w*\b[^.\n]{0,40}\b(?:instruction|rule|guideline|guardrail|memory|steering|claude\.?md|agents\.?md|context\s+file|prompt)/i.test(
|
|
1449
|
+
promptLc
|
|
1450
|
+
);
|
|
1377
1451
|
for (const s of tools) {
|
|
1378
1452
|
if (isCommandTool(s.name)) {
|
|
1379
1453
|
const invs = parseGitInvocations(commandOf(s.input));
|
|
@@ -1441,6 +1515,20 @@ function deriveBypassFindings(sum) {
|
|
|
1441
1515
|
guardrailRule: `Don't disable type-strictness, lint rules, or coverage thresholds to pass; fix the code the check flags.`
|
|
1442
1516
|
};
|
|
1443
1517
|
}
|
|
1518
|
+
if (!specTamper && STEERING_FILE.test(fp) && !editingInstructionsIsTask && SELF_PERMISSION_BYPASS.test(newStr) && !SELF_PERMISSION_BYPASS.test(oldStr)) {
|
|
1519
|
+
specTamper = {
|
|
1520
|
+
id: `spec-tamper-${s.spanId}`,
|
|
1521
|
+
severity: "high",
|
|
1522
|
+
title: `Granted itself permission to bypass a check in ${base2(fp)}`,
|
|
1523
|
+
detail: `An edit to \`${base2(fp)}\` \u2014 the agent's own steering/instructions file \u2014 added language permitting it to skip or disable an oversight mechanism (tests, checks, lint, hooks, or CI). An agent rewriting the rules it operates under to license skipping verification is a spec-gaming signal (Specification Self-Correction, arXiv:2507.18742). Review what oversight it exempted itself from.`,
|
|
1524
|
+
impactLabel: "rewrote its own rules",
|
|
1525
|
+
confidence: 0.8,
|
|
1526
|
+
score: 100 * 0.9 * 0.8,
|
|
1527
|
+
evidenceSpanId: s.spanId,
|
|
1528
|
+
filePath: fp,
|
|
1529
|
+
guardrailRule: "Never edit your own instructions/steering file to permit skipping tests, checks, or hooks; satisfy the oversight instead."
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1444
1532
|
if (!ciCdTouch && CICD_FILE.test(fp)) {
|
|
1445
1533
|
ciCdTouch = {
|
|
1446
1534
|
id: `ci-cd-touch-${s.spanId}`,
|
|
@@ -1457,7 +1545,7 @@ function deriveBypassFindings(sum) {
|
|
|
1457
1545
|
}
|
|
1458
1546
|
}
|
|
1459
1547
|
}
|
|
1460
|
-
for (const f of [hookBypass, forcePush, focus, configWeaken, ciCdTouch]) {
|
|
1548
|
+
for (const f of [hookBypass, forcePush, focus, configWeaken, ciCdTouch, specTamper]) {
|
|
1461
1549
|
if (f) {
|
|
1462
1550
|
out.push(f);
|
|
1463
1551
|
}
|
|
@@ -1465,6 +1553,112 @@ function deriveBypassFindings(sum) {
|
|
|
1465
1553
|
return out;
|
|
1466
1554
|
}
|
|
1467
1555
|
|
|
1556
|
+
// src/findings/commandSecurityFindings.ts
|
|
1557
|
+
var isPrintLead = (cmd) => /^\s*(echo|printf|:|#|cat\s*<<)/.test(cmd);
|
|
1558
|
+
var deQuote = (cmd) => cmd.replace(/'[^']*'/g, "''").replace(/"[^"]*"/g, '""');
|
|
1559
|
+
var METADATA = /\b(?:169\.254\.169\.254|169\.254\.170\.2|metadata\.google\.internal|metadata\.goog)\b|\[fd00:ec2::254\]/i;
|
|
1560
|
+
var TLS_DISABLE = /\bcurl\b[^|;&]*\s(?:-k|--insecure)\b|--no-check-certificate\b|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*0|PYTHONHTTPSVERIFY\s*=\s*0|\bgit\b[^|;&]*-c\s+http\.sslVerify=false|--strict-ssl[ =]false|\bcurl\b[^|;&]*\s--cacert\s+\/dev\/null/i;
|
|
1561
|
+
var SETUID = /\bchmod\b[^|;&]*\s(?:[ug]\+s|\+s)\b|\bchmod\b[^|;&]*\s(?:[2467])[0-7]{3}\b/i;
|
|
1562
|
+
var OBFUSCATED_PIPE = /\bbase64\s+(?:-d|--decode)\b[^|]*\|\s*(?:sudo\s+)?(?:ba|z)?sh\b|\bxxd\s+-r\b[^|]*\|\s*(?:ba)?sh\b|\bbase64\s+(?:-d|--decode)\b[^|]*\|\s*python/i;
|
|
1563
|
+
var OBFUSCATED_INLINE = /python[0-9.]*\s+-c\s*["'][^"']*\b(?:exec|eval)\(|node\s+-e\s*["'][^"']*\b(?:eval|Function|atob)\(|\beval\s+"\$\(/i;
|
|
1564
|
+
var EVAL_ALLOW = /\beval\s+"\$\(\s*(?:ssh-agent|direnv\s+hook|rbenv\s+init|pyenv\s+init|nodenv\s+init|brew\s+shellenv|zoxide\s+init|starship\s+init|fnm\s+env|mise\s+activate|conda\s+shell)/i;
|
|
1565
|
+
var PIP_INSTALL = /\b(?:pip3?|uv\s+pip)\s+install\b/i;
|
|
1566
|
+
var NPM_INSTALL = /\b(?:npm\s+(?:i|install|add)|yarn\s+add|pnpm\s+add)\b/i;
|
|
1567
|
+
var NONREGISTRY = /\b(?:git\+[a-z]+:\/\/|hg\+|svn\+|bzr\+|https?:\/\/\S+\.(?:whl|tar\.gz|tgz)|git\+ssh|github:|gitlab:|bitbucket:)/i;
|
|
1568
|
+
var VERIFY_CMD = /\b(?:pytest|go\s+test|cargo\s+(?:test|clippy)|(?:npm|yarn|pnpm)\s+(?:run\s+)?(?:test|lint|typecheck)|jest|vitest|mocha|rspec|phpunit|dotnet\s+test|gradle\s+test|mvn\s+test|tox|nox|ruff|flake8|pylint|mypy|pyright|eslint|tsc|golangci-lint|(?:black|prettier)\b[^|;&]*--check)\b/i;
|
|
1569
|
+
var MASK = /\|\|\s*(?:true\b|exit\s+0\b|:(?![\w-]))|;\s*(?:true|exit\s+0)\s*$/i;
|
|
1570
|
+
function classify(rawCmd) {
|
|
1571
|
+
const raw = stripHeredocs(rawCmd);
|
|
1572
|
+
const dq = deQuote(raw);
|
|
1573
|
+
const print = isPrintLead(raw);
|
|
1574
|
+
const clip = (c) => c.trim().slice(0, 140);
|
|
1575
|
+
if (!print && METADATA.test(dq)) {
|
|
1576
|
+
return {
|
|
1577
|
+
id: "metadata-egress",
|
|
1578
|
+
severity: "high",
|
|
1579
|
+
title: "Contacted the cloud metadata endpoint",
|
|
1580
|
+
detail: `A command reached a cloud instance-metadata / link-local endpoint (\`${clip(rawCmd)}\`) \u2014 the SSRF / cloud-credential surface. Confirm this access was intended; rotate instance credentials if it wasn't.`,
|
|
1581
|
+
guardrail: "Agents should never contact 169.254.169.254 / metadata.google.internal; it exposes cloud IAM credentials."
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
if (!print && TLS_DISABLE.test(dq)) {
|
|
1585
|
+
return {
|
|
1586
|
+
id: "tls-verify-disabled",
|
|
1587
|
+
severity: "high",
|
|
1588
|
+
title: "Disabled TLS / certificate verification",
|
|
1589
|
+
detail: `A command turned off TLS/cert verification (\`${clip(rawCmd)}\`), exposing whatever it fetched to man-in-the-middle. Re-run with verification; confirm nothing sensitive crossed an unverified channel.`,
|
|
1590
|
+
guardrail: "Never disable TLS verification (curl -k, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False); fix the cert instead."
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
if (!print && SETUID.test(dq)) {
|
|
1594
|
+
return {
|
|
1595
|
+
id: "setuid-create",
|
|
1596
|
+
severity: "high",
|
|
1597
|
+
title: "Set the setuid/setgid bit on a file",
|
|
1598
|
+
detail: `A command set the setuid/setgid bit (\`${clip(rawCmd)}\`) \u2014 a binary that runs with the owner's privileges regardless of caller, the canonical privilege-escalation backdoor. Confirm a setuid binary was intended; remove the bit if not.`,
|
|
1599
|
+
guardrail: "Don't create setuid/setgid binaries (chmod u+s / 4755) from an agent session."
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
if (OBFUSCATED_PIPE.test(dq) || !print && !EVAL_ALLOW.test(raw) && OBFUSCATED_INLINE.test(raw)) {
|
|
1603
|
+
return {
|
|
1604
|
+
id: "obfuscated-exec",
|
|
1605
|
+
severity: "high",
|
|
1606
|
+
title: "Decoded and executed an obfuscated payload",
|
|
1607
|
+
detail: `A command decoded an encoded blob and executed it (\`${clip(rawCmd)}\`) \u2014 the executed code is hidden from the diff. Inspect the decoded payload before trusting this change.`,
|
|
1608
|
+
guardrail: `Don't decode-then-execute (base64 -d | sh, eval "$(\u2026)"); download, inspect, then run.`
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
if (!print && (PIP_INSTALL.test(dq) || NPM_INSTALL.test(dq)) && NONREGISTRY.test(dq)) {
|
|
1612
|
+
return {
|
|
1613
|
+
id: "untrusted-dep-source",
|
|
1614
|
+
severity: "high",
|
|
1615
|
+
title: "Installed a dependency from a non-registry source",
|
|
1616
|
+
detail: `An install pulled a package from a URL / VCS / tarball rather than the package registry (\`${clip(rawCmd)}\`) \u2014 unvetted code in the build. Confirm the source is trusted and pinned.`,
|
|
1617
|
+
guardrail: "Install dependencies from the registry with a pinned version; review any git+/URL/tarball source."
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
if (!print && VERIFY_CMD.test(dq) && MASK.test(dq)) {
|
|
1621
|
+
return {
|
|
1622
|
+
id: "masked-failure",
|
|
1623
|
+
severity: "high",
|
|
1624
|
+
title: "Suppressed a verification command's failure",
|
|
1625
|
+
detail: `A test/lint/typecheck command was forced to succeed (\`${clip(rawCmd)}\`) \u2014 its non-zero exit was discarded with \`|| true\`/\`; true\`, so a failing gate reads as passed. Re-run it without the success-forcer and fix the real failure before trusting this as verified.`,
|
|
1626
|
+
guardrail: "Don't mask a verification command with `|| true` / `; true`; a forced-green gate proves nothing."
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
return void 0;
|
|
1630
|
+
}
|
|
1631
|
+
function deriveCommandSecurityFindings(sum) {
|
|
1632
|
+
const out = [];
|
|
1633
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1634
|
+
for (const s of sum.spans) {
|
|
1635
|
+
if (s.kind !== "tool" || !isCommandTool(s.name)) {
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
const cmd = commandOf(s.input);
|
|
1639
|
+
if (!cmd) {
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
const hit = classify(cmd);
|
|
1643
|
+
if (!hit || seen.has(hit.id)) {
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
seen.add(hit.id);
|
|
1647
|
+
out.push({
|
|
1648
|
+
id: `${hit.id}-${s.spanId}`,
|
|
1649
|
+
severity: hit.severity,
|
|
1650
|
+
title: hit.title,
|
|
1651
|
+
detail: hit.detail,
|
|
1652
|
+
impactLabel: hit.id,
|
|
1653
|
+
confidence: 0.9,
|
|
1654
|
+
score: 100 * 0.9,
|
|
1655
|
+
evidenceSpanId: s.spanId,
|
|
1656
|
+
guardrailRule: hit.guardrail
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
return out;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1468
1662
|
// src/findings/compositeFindings.ts
|
|
1469
1663
|
var COMPLETION = /\b(all tests? (?:now )?pass(?:ing|ed)?|tests? (?:now )?pass(?:ing|ed)?|all green|everything passes|works now|done|fixed|resolved)\b/i;
|
|
1470
1664
|
var HEDGE = /\b(should|probably|might|maybe|may|i think|hopefully|in theory|ought to|expects? to|expected to|seems? to|presumably)\b/i;
|
|
@@ -2525,6 +2719,261 @@ function deriveInjectionFindings(sum) {
|
|
|
2525
2719
|
return [];
|
|
2526
2720
|
}
|
|
2527
2721
|
|
|
2722
|
+
// src/findings/mcpFindings.ts
|
|
2723
|
+
var MUTATING_VERBS = /* @__PURE__ */ new Set([
|
|
2724
|
+
"create",
|
|
2725
|
+
"update",
|
|
2726
|
+
"delete",
|
|
2727
|
+
"remove",
|
|
2728
|
+
"post",
|
|
2729
|
+
"send",
|
|
2730
|
+
"write",
|
|
2731
|
+
"insert",
|
|
2732
|
+
"upsert",
|
|
2733
|
+
"merge",
|
|
2734
|
+
"deploy",
|
|
2735
|
+
"publish",
|
|
2736
|
+
"set",
|
|
2737
|
+
"add",
|
|
2738
|
+
"move",
|
|
2739
|
+
"rename",
|
|
2740
|
+
"archive",
|
|
2741
|
+
"close",
|
|
2742
|
+
"comment",
|
|
2743
|
+
"approve",
|
|
2744
|
+
"revoke",
|
|
2745
|
+
"trigger",
|
|
2746
|
+
"run",
|
|
2747
|
+
"execute",
|
|
2748
|
+
"invoke",
|
|
2749
|
+
"edit",
|
|
2750
|
+
"patch",
|
|
2751
|
+
"put",
|
|
2752
|
+
"upload",
|
|
2753
|
+
"push",
|
|
2754
|
+
"schedule",
|
|
2755
|
+
"share",
|
|
2756
|
+
"duplicate"
|
|
2757
|
+
]);
|
|
2758
|
+
var DURABLE_SERVER = /github|gitlab|bitbucket|linear|jira|asana|slack|discord|notion|confluence|hubspot|salesforce|stripe|postgres|mysql|sql|mongo|database|s3|gcs|sheets|gmail|calendar|drive|fireflies|clay|apollo/i;
|
|
2759
|
+
var EPHEMERAL_SERVER = /chrome|playwright|puppeteer|browser|selenium|webdriver/i;
|
|
2760
|
+
function parseMcp(name) {
|
|
2761
|
+
if (!name.startsWith("mcp__")) {
|
|
2762
|
+
return void 0;
|
|
2763
|
+
}
|
|
2764
|
+
const rest = name.slice("mcp__".length);
|
|
2765
|
+
const i = rest.lastIndexOf("__");
|
|
2766
|
+
if (i < 0) {
|
|
2767
|
+
return { server: rest, action: "" };
|
|
2768
|
+
}
|
|
2769
|
+
return { server: rest.slice(0, i), action: rest.slice(i + 2) };
|
|
2770
|
+
}
|
|
2771
|
+
function isMutatingAction(action) {
|
|
2772
|
+
return action.toLowerCase().split(/[-_]+/).some((tok) => MUTATING_VERBS.has(tok));
|
|
2773
|
+
}
|
|
2774
|
+
function deriveMcpFindings(sum) {
|
|
2775
|
+
const calls = /* @__PURE__ */ new Map();
|
|
2776
|
+
let firstSpanId;
|
|
2777
|
+
for (const s of sum.spans) {
|
|
2778
|
+
if (s.kind !== "tool" || typeof s.name !== "string") {
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
const p = parseMcp(s.name);
|
|
2782
|
+
if (!p || !p.action || !isMutatingAction(p.action) || EPHEMERAL_SERVER.test(p.server)) {
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
const key = `${p.server}::${p.action}`;
|
|
2786
|
+
const prev = calls.get(key);
|
|
2787
|
+
if (prev) {
|
|
2788
|
+
prev.count++;
|
|
2789
|
+
} else {
|
|
2790
|
+
calls.set(key, { server: p.server, action: p.action, count: 1 });
|
|
2791
|
+
}
|
|
2792
|
+
if (!firstSpanId) {
|
|
2793
|
+
firstSpanId = s.spanId;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
if (calls.size === 0) {
|
|
2797
|
+
return [];
|
|
2798
|
+
}
|
|
2799
|
+
const list = [...calls.values()];
|
|
2800
|
+
const total = list.reduce((n, c) => n + c.count, 0);
|
|
2801
|
+
const durable = list.some((c) => DURABLE_SERVER.test(c.server));
|
|
2802
|
+
const label = list.slice(0, 8).map((c) => `\`${c.server}::${c.action}\`${c.count > 1 ? ` \xD7${c.count}` : ""}`).join(", ");
|
|
2803
|
+
return [
|
|
2804
|
+
{
|
|
2805
|
+
id: `mcp-write-${firstSpanId ?? "0"}`,
|
|
2806
|
+
severity: durable ? "high" : "medium",
|
|
2807
|
+
title: `${total} external write${total === 1 ? "" : "s"} via MCP tools`,
|
|
2808
|
+
detail: `The session made ${total} mutating MCP tool call${total === 1 ? "" : "s"} \u2014 external side-effects on systems the PR diff does not show: ${label}. Confirm these external effects were intended; undo any that weren't.`,
|
|
2809
|
+
impactLabel: "external side-effects",
|
|
2810
|
+
confidence: 0.9,
|
|
2811
|
+
score: (durable ? 100 : 10) * 0.9,
|
|
2812
|
+
evidenceSpanId: firstSpanId,
|
|
2813
|
+
guardrailRule: "An agent's MCP writes (issues, messages, DB rows, files in external systems) aren't in the diff \u2014 review them as part of the change."
|
|
2814
|
+
}
|
|
2815
|
+
];
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// src/findings/persistenceFindings.ts
|
|
2819
|
+
var AUTORUN_PATH = /(?:^|\/)\.(?:bashrc|zshrc|bash_profile|zprofile|profile|bash_login|zshenv)$|(?:^|\/)\.config\/fish\/config\.fish$|^\/etc\/profile(?:\.d\/.+)?$|(?:^|\/)Library\/LaunchAgents\/[^/]+|^\/Library\/Launch(?:Daemons|Agents)\/[^/]+|^\/etc\/systemd\/system\/[^/]+|(?:^|\/)\.config\/systemd\/user\/[^/]+|^\/etc\/cron[^/]*|^\/var\/spool\/cron\//i;
|
|
2820
|
+
var PERSIST_CMD = /\bcrontab\b(?!\s+(?:-l|-r)\b)|\blaunchctl\s+(?:load|bootstrap|enable)\b|\bsystemctl\s+(?:--user\s+)?enable\b|\bgit\b[^|;&]*\bconfig\b[^|;&]*--global[^|;&]*core\.hooksPath/i;
|
|
2821
|
+
var isPrintLead2 = (cmd) => /^\s*(echo|printf|:|#|cat\s*<<)/.test(cmd);
|
|
2822
|
+
var deQuote2 = (cmd) => cmd.replace(/'[^']*'/g, "''").replace(/"[^"]*"/g, '""');
|
|
2823
|
+
function commandWritesAutorun(cmd) {
|
|
2824
|
+
const m = cmd.match(/(?:>>?|\btee\b)\s+(?:-\S+\s+)*["']?([^\s"'|;&<>]+)/);
|
|
2825
|
+
if (m && AUTORUN_PATH.test(m[1])) {
|
|
2826
|
+
return m[1];
|
|
2827
|
+
}
|
|
2828
|
+
return void 0;
|
|
2829
|
+
}
|
|
2830
|
+
function derivePersistenceFindings(sum) {
|
|
2831
|
+
const out = [];
|
|
2832
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2833
|
+
const push = (spanId, what, how) => {
|
|
2834
|
+
if (seen.has(what)) {
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
seen.add(what);
|
|
2838
|
+
out.push({
|
|
2839
|
+
id: `persistence-write-${spanId}`,
|
|
2840
|
+
severity: "high",
|
|
2841
|
+
title: `Established auto-run persistence: ${what}`,
|
|
2842
|
+
detail: `\`${what}\` is an OS auto-run surface (shell startup / cron / launchd / systemd) \u2014 code placed here runs on the next shell, login, or boot. It was written ${how} this session, outside the PR diff. Confirm the auto-run was intended; remove it if not.`,
|
|
2843
|
+
impactLabel: "persistence",
|
|
2844
|
+
confidence: 0.9,
|
|
2845
|
+
score: 100 * 0.9,
|
|
2846
|
+
evidenceSpanId: spanId,
|
|
2847
|
+
guardrailRule: "Don't let an agent write shell-rc / cron / launchd / systemd auto-run surfaces; make persistence changes yourself, in review."
|
|
2848
|
+
});
|
|
2849
|
+
};
|
|
2850
|
+
for (const s of sum.spans) {
|
|
2851
|
+
if (s.kind !== "tool") {
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
if (isEditTool(s.name)) {
|
|
2855
|
+
const fp = filePathOf(s.input);
|
|
2856
|
+
if (fp && AUTORUN_PATH.test(fp)) {
|
|
2857
|
+
push(s.spanId, fp, "via an edit");
|
|
2858
|
+
}
|
|
2859
|
+
} else if (isCommandTool(s.name)) {
|
|
2860
|
+
const raw = stripHeredocs(commandOf(s.input));
|
|
2861
|
+
if (!raw) {
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
const dq = deQuote2(raw);
|
|
2865
|
+
const written = commandWritesAutorun(dq);
|
|
2866
|
+
if (written) {
|
|
2867
|
+
push(s.spanId, written, `via the command \`${raw.trim().slice(0, 120)}\``);
|
|
2868
|
+
} else if (!isPrintLead2(raw) && PERSIST_CMD.test(dq)) {
|
|
2869
|
+
push(s.spanId, raw.trim().slice(0, 60), `via the command \`${raw.trim().slice(0, 120)}\``);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
return out;
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
// src/findings/sensitiveAccess.ts
|
|
2877
|
+
var CRITICAL = [
|
|
2878
|
+
/(?:^|\/)\.ssh(?:\/|$)/i,
|
|
2879
|
+
/(?:^|\/)\.aws(?:\/|$)/i,
|
|
2880
|
+
/(?:^|\/)\.gnupg(?:\/|$)/i,
|
|
2881
|
+
/(?:^|\/)\.kube(?:\/|$)/i,
|
|
2882
|
+
/(?:^|\/)id_rsa\b/i,
|
|
2883
|
+
/(?:^|\/)id_ed25519\b/i,
|
|
2884
|
+
/(?:^|\/)id_ecdsa\b/i,
|
|
2885
|
+
/\.pem$/i,
|
|
2886
|
+
/\.key$/i,
|
|
2887
|
+
/\.p12$/i,
|
|
2888
|
+
/\.keychain$/i,
|
|
2889
|
+
/(?:^|\/)\.git-credentials$/i,
|
|
2890
|
+
/^\/etc\/shadow\b/i,
|
|
2891
|
+
/^\/etc\/gshadow\b/i,
|
|
2892
|
+
/^\/etc\/passwd\b/i,
|
|
2893
|
+
/^\/etc\/sudoers(?:\.d)?(?:\/|$)/i,
|
|
2894
|
+
/^\/etc\/ssh(?:\/|$)/i,
|
|
2895
|
+
/^\/root(?:\/|$)/i,
|
|
2896
|
+
/^\/(?:private\/)?etc\/master\.passwd\b/i
|
|
2897
|
+
];
|
|
2898
|
+
var HIGH = [
|
|
2899
|
+
/(?:^|\/)\.env(?:\.[A-Za-z0-9_-]+)?$/i,
|
|
2900
|
+
/(?:^|\/)\.netrc$/i,
|
|
2901
|
+
/(?:^|\/)\.npmrc$/i,
|
|
2902
|
+
/(?:^|\/)\.pypirc$/i,
|
|
2903
|
+
/(?:^|\/)\.bash_history$/i,
|
|
2904
|
+
/(?:^|\/)\.zsh_history$/i
|
|
2905
|
+
];
|
|
2906
|
+
var EXCLUDE = /\.(?:example|sample|template|dist)$/i;
|
|
2907
|
+
function sensitivity(path2) {
|
|
2908
|
+
const p = path2.replace(/^["']|["']$/g, "").trim();
|
|
2909
|
+
if (!p || EXCLUDE.test(p)) {
|
|
2910
|
+
return void 0;
|
|
2911
|
+
}
|
|
2912
|
+
if (CRITICAL.some((re) => re.test(p))) {
|
|
2913
|
+
return "critical";
|
|
2914
|
+
}
|
|
2915
|
+
if (HIGH.some((re) => re.test(p))) {
|
|
2916
|
+
return "high";
|
|
2917
|
+
}
|
|
2918
|
+
return void 0;
|
|
2919
|
+
}
|
|
2920
|
+
function sensitiveInCommand(cmd) {
|
|
2921
|
+
for (const raw of cmd.split(/[\s|<>;&=()]+/)) {
|
|
2922
|
+
const tok = raw.replace(/^["']|["']$/g, "");
|
|
2923
|
+
if (!tok || tok.startsWith("-")) {
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
const tier = sensitivity(tok);
|
|
2927
|
+
if (tier) {
|
|
2928
|
+
return { path: tok, tier };
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
return void 0;
|
|
2932
|
+
}
|
|
2933
|
+
var base5 = (p) => p.replace(/\/+$/, "").split("/").pop() || p;
|
|
2934
|
+
function deriveSensitiveAccess(sum) {
|
|
2935
|
+
const findings = [];
|
|
2936
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2937
|
+
for (const s of sum.spans) {
|
|
2938
|
+
let hit;
|
|
2939
|
+
if (isReadTool(s.name)) {
|
|
2940
|
+
const fp = filePathOf(s.input);
|
|
2941
|
+
const tier = fp ? sensitivity(fp) : void 0;
|
|
2942
|
+
if (fp && tier) {
|
|
2943
|
+
hit = { path: fp, tier, how: "read" };
|
|
2944
|
+
}
|
|
2945
|
+
} else if (isCommandTool(s.name)) {
|
|
2946
|
+
const cmd = commandOf(s.input);
|
|
2947
|
+
const m = cmd ? sensitiveInCommand(cmd) : void 0;
|
|
2948
|
+
if (m) {
|
|
2949
|
+
hit = { path: m.path, tier: m.tier, how: `the command \`${cmd.trim().slice(0, 120)}\`` };
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
if (!hit) {
|
|
2953
|
+
continue;
|
|
2954
|
+
}
|
|
2955
|
+
const key = `${hit.path}|${hit.how === "read" ? "read" : "cmd"}`;
|
|
2956
|
+
if (seen.has(key)) {
|
|
2957
|
+
continue;
|
|
2958
|
+
}
|
|
2959
|
+
seen.add(key);
|
|
2960
|
+
const verb = hit.how === "read" ? "Read" : "Accessed";
|
|
2961
|
+
const via = hit.how === "read" ? "via a file read" : `via ${hit.how}`;
|
|
2962
|
+
findings.push({
|
|
2963
|
+
id: `sensitive-access-${s.spanId}`,
|
|
2964
|
+
severity: hit.tier,
|
|
2965
|
+
title: `${verb} a credential file: ${base5(hit.path)}`,
|
|
2966
|
+
detail: `\`${hit.path}\` was accessed this session ${via} \u2014 a credential / identity file outside the change under review. Confirm the access was intended; rotate the credential if it wasn't meant to be exposed to the agent.`,
|
|
2967
|
+
confidence: 0.9,
|
|
2968
|
+
score: 0,
|
|
2969
|
+
evidenceSpanId: s.spanId,
|
|
2970
|
+
filePath: hit.path,
|
|
2971
|
+
guardrailRule: "Don't read secret or identity files (~/.ssh, ~/.aws, .env, *.pem) from an agent session; inject what's needed through the environment instead."
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
return findings;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2528
2977
|
// src/findings/toolUseFindings.ts
|
|
2529
2978
|
var WRAP = /<tool_use_error>([\s\S]*?)<\/tool_use_error>/i;
|
|
2530
2979
|
var FABRICATED = /no such tool|unknown tool|tool .{0,30}not found|not a valid tool|no tool named/i;
|
|
@@ -2595,6 +3044,63 @@ var SEVERITY_WEIGHT = {
|
|
|
2595
3044
|
function scoreOf(sev, impact, confidence) {
|
|
2596
3045
|
return SEVERITY_WEIGHT[sev] * (0.2 + Math.min(1, impact)) * confidence;
|
|
2597
3046
|
}
|
|
3047
|
+
function deriveTruncatedTurn(sum) {
|
|
3048
|
+
const truncated = sum.spans.filter(
|
|
3049
|
+
(s) => s.kind === "generation" && (s.finishReason === "length" || s.finishReason === "content_filter" || s.finishReason === "error")
|
|
3050
|
+
);
|
|
3051
|
+
if (truncated.length === 0) {
|
|
3052
|
+
return [];
|
|
3053
|
+
}
|
|
3054
|
+
const t = truncated[0];
|
|
3055
|
+
const more = truncated.length > 1 ? ` (${truncated.length} turns)` : "";
|
|
3056
|
+
const why = t.finishReason === "length" ? "hit the output-token ceiling (max_tokens)" : t.finishReason === "content_filter" ? "was stopped by a content filter" : "ended in an error";
|
|
3057
|
+
return [
|
|
3058
|
+
{
|
|
3059
|
+
id: `truncated-turn-${t.spanId}`,
|
|
3060
|
+
severity: "high",
|
|
3061
|
+
title: `Turn cut off mid-output${more}`,
|
|
3062
|
+
detail: `A generation ${why}, so the agent's last edit/command/tool-call in that turn may be truncated. Re-check the work produced right after it.`,
|
|
3063
|
+
impactLabel: "turn truncated",
|
|
3064
|
+
confidence: 0.8,
|
|
3065
|
+
score: scoreOf("high", 0.8, 0.8),
|
|
3066
|
+
evidenceSpanId: t.spanId,
|
|
3067
|
+
guardrailRule: "A length-stopped turn likely left work half-done; raise max_tokens or continue the turn, then re-check the last edit/command."
|
|
3068
|
+
}
|
|
3069
|
+
];
|
|
3070
|
+
}
|
|
3071
|
+
var DETECTORS = [
|
|
3072
|
+
// correctness & trust findings
|
|
3073
|
+
{ name: "correctness", fn: (s) => deriveCorrectnessFindings(s) },
|
|
3074
|
+
// edit-body scan: reward-hacking against the test suite
|
|
3075
|
+
{ name: "edit-scan", fn: (s) => deriveEditScanFindings(s) },
|
|
3076
|
+
// composite (M13): correlate a green claim with a same-session test suppression never re-greened.
|
|
3077
|
+
// Pure correlation over the rows already pushed above — reads `prior`.
|
|
3078
|
+
{ name: "composite", fn: (s, prior) => deriveCompositeFindings(s, prior) },
|
|
3079
|
+
// audit composites (M80, experimental): gated until validated on the live fleet.
|
|
3080
|
+
{
|
|
3081
|
+
name: "audit",
|
|
3082
|
+
fn: (s) => process.env.RECEIPTS_EXPERIMENTAL_DETECTORS === "1" ? deriveAuditFindings(s) : []
|
|
3083
|
+
},
|
|
3084
|
+
// tool-use validity: fabricated tools / malformed arguments
|
|
3085
|
+
{ name: "tool-use", fn: (s) => deriveToolUseFindings(s) },
|
|
3086
|
+
// check / guardrail bypass: agent circumvented its own oversight
|
|
3087
|
+
{ name: "bypass", fn: (s) => deriveBypassFindings(s) },
|
|
3088
|
+
// self-permissioning (M101): agent edited its own .claude controls / hooks
|
|
3089
|
+
{ name: "self-permission", fn: (s) => deriveSelfPermission(s) },
|
|
3090
|
+
// prompt injection: external content the agent ingested tried to hijack it
|
|
3091
|
+
{ name: "injection", fn: (s) => deriveInjectionFindings(s) },
|
|
3092
|
+
// sensitive-path access (M100): the agent read a credential / identity file
|
|
3093
|
+
{ name: "sensitive-access", fn: (s) => deriveSensitiveAccess(s) },
|
|
3094
|
+
// MCP external side-effects (M105): mutating MCP tool calls the diff can't show
|
|
3095
|
+
{ name: "mcp", fn: (s) => deriveMcpFindings(s) },
|
|
3096
|
+
// command-pattern security (M106/108/109/110/111/112): metadata egress, untrusted dep source,
|
|
3097
|
+
// TLS-disable, setuid, obfuscated exec, masked verification failure
|
|
3098
|
+
{ name: "command-security", fn: (s) => deriveCommandSecurityFindings(s) },
|
|
3099
|
+
// persistence (M107): agent wrote an OS auto-run surface (shell-rc/cron/launchd/systemd)
|
|
3100
|
+
{ name: "persistence", fn: (s) => derivePersistenceFindings(s) },
|
|
3101
|
+
// truncated turn (M35): the provider cut a turn off; the last edit/command may be half-written
|
|
3102
|
+
{ name: "truncated-turn", fn: (s) => deriveTruncatedTurn(s) }
|
|
3103
|
+
];
|
|
2598
3104
|
function deriveFindings(sum) {
|
|
2599
3105
|
const findings = [];
|
|
2600
3106
|
const total = sum.totalCost || 0;
|
|
@@ -2809,33 +3315,8 @@ function deriveFindings(sum) {
|
|
|
2809
3315
|
fixPrompt: `In a recent session, ${simpleExpensive.length} short steps ran on a frontier model when a cheaper tier would have sufficed (~${formatCostAlways(saveable)} of avoidable spend). Suggest which steps to route to a cheaper model and how.`
|
|
2810
3316
|
});
|
|
2811
3317
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
findings.push(...deriveCompositeFindings(sum, findings));
|
|
2815
|
-
if (process.env.RECEIPTS_EXPERIMENTAL_DETECTORS === "1") {
|
|
2816
|
-
findings.push(...deriveAuditFindings(sum));
|
|
2817
|
-
}
|
|
2818
|
-
findings.push(...deriveToolUseFindings(sum));
|
|
2819
|
-
findings.push(...deriveBypassFindings(sum));
|
|
2820
|
-
findings.push(...deriveInjectionFindings(sum));
|
|
2821
|
-
const truncated = sum.spans.filter(
|
|
2822
|
-
(s) => s.kind === "generation" && (s.finishReason === "length" || s.finishReason === "content_filter" || s.finishReason === "error")
|
|
2823
|
-
);
|
|
2824
|
-
if (truncated.length > 0) {
|
|
2825
|
-
const t = truncated[0];
|
|
2826
|
-
const more = truncated.length > 1 ? ` (${truncated.length} turns)` : "";
|
|
2827
|
-
const why = t.finishReason === "length" ? "hit the output-token ceiling (max_tokens)" : t.finishReason === "content_filter" ? "was stopped by a content filter" : "ended in an error";
|
|
2828
|
-
findings.push({
|
|
2829
|
-
id: `truncated-turn-${t.spanId}`,
|
|
2830
|
-
severity: "high",
|
|
2831
|
-
title: `Turn cut off mid-output${more}`,
|
|
2832
|
-
detail: `A generation ${why}, so the agent's last edit/command/tool-call in that turn may be truncated. Re-check the work produced right after it.`,
|
|
2833
|
-
impactLabel: "turn truncated",
|
|
2834
|
-
confidence: 0.8,
|
|
2835
|
-
score: scoreOf("high", 0.8, 0.8),
|
|
2836
|
-
evidenceSpanId: t.spanId,
|
|
2837
|
-
guardrailRule: "A length-stopped turn likely left work half-done; raise max_tokens or continue the turn, then re-check the last edit/command."
|
|
2838
|
-
});
|
|
3318
|
+
for (const detector of DETECTORS) {
|
|
3319
|
+
findings.push(...detector.fn(sum, findings));
|
|
2839
3320
|
}
|
|
2840
3321
|
const rewriteSpans = new Set(
|
|
2841
3322
|
findings.filter((f) => f.id.startsWith("history-rewrite")).map((f) => f.evidenceSpanId)
|
|
@@ -2888,74 +3369,6 @@ function gradeLetter(main) {
|
|
|
2888
3369
|
|
|
2889
3370
|
// src/trace/diffScope.ts
|
|
2890
3371
|
import { spawnSync } from "child_process";
|
|
2891
|
-
|
|
2892
|
-
// src/findings/surface.ts
|
|
2893
|
-
var OPERATOR_KINDS = /* @__PURE__ */ new Set([
|
|
2894
|
-
"loop",
|
|
2895
|
-
"bottleneck",
|
|
2896
|
-
"cost-concentration",
|
|
2897
|
-
"cache-opportunity",
|
|
2898
|
-
"model-downgrade"
|
|
2899
|
-
]);
|
|
2900
|
-
var PRIVILEGED_PREFIXES = [
|
|
2901
|
-
"ci-cd-touch",
|
|
2902
|
-
"lockfile-edit",
|
|
2903
|
-
"hook-bypass",
|
|
2904
|
-
"config-weaken",
|
|
2905
|
-
"grader-edit",
|
|
2906
|
-
"eval-override",
|
|
2907
|
-
"test-focus",
|
|
2908
|
-
"test-skipped",
|
|
2909
|
-
"test-trivialised",
|
|
2910
|
-
"green-by-suppression",
|
|
2911
|
-
"untested-test",
|
|
2912
|
-
"history-rewrite",
|
|
2913
|
-
"force-push"
|
|
2914
|
-
];
|
|
2915
|
-
var TEST_PATH3 = /(?:^|\/)(?:tests?|specs?|__tests__)(?:\/|$)|\.(?:test|spec)\.|_test\./;
|
|
2916
|
-
function privileged(id, filePath) {
|
|
2917
|
-
if (PRIVILEGED_PREFIXES.some((p) => id.startsWith(p))) {
|
|
2918
|
-
return true;
|
|
2919
|
-
}
|
|
2920
|
-
return id.startsWith("file-shrink") && !!filePath && TEST_PATH3.test(filePath);
|
|
2921
|
-
}
|
|
2922
|
-
function findingSurface(id) {
|
|
2923
|
-
if (OPERATOR_KINDS.has(id) || id.startsWith("errcluster-")) {
|
|
2924
|
-
return "operator";
|
|
2925
|
-
}
|
|
2926
|
-
return "merge";
|
|
2927
|
-
}
|
|
2928
|
-
function destructiveOutsideRepo(title) {
|
|
2929
|
-
const clause = title.replace(/^Destructive op:\s*/i, "").replace(/\s*×\d+\s*$/, "");
|
|
2930
|
-
const m = clause.match(/^\s*(rm|rmdir)\b(.*)$/i);
|
|
2931
|
-
if (!m) {
|
|
2932
|
-
return false;
|
|
2933
|
-
}
|
|
2934
|
-
const operands = m[2].split(/\s+/).filter((t) => t && !t.startsWith("-"));
|
|
2935
|
-
if (operands.length === 0) {
|
|
2936
|
-
return false;
|
|
2937
|
-
}
|
|
2938
|
-
const scratch = (t) => {
|
|
2939
|
-
const p = t.replace(/^["']|["']$/g, "");
|
|
2940
|
-
return /\$/.test(p) || p.startsWith("/tmp") || p.startsWith("/var/folders") || p.startsWith("/private/var/folders") || p.startsWith("~");
|
|
2941
|
-
};
|
|
2942
|
-
return operands.every(scratch);
|
|
2943
|
-
}
|
|
2944
|
-
function regenerableReceiptRm(title) {
|
|
2945
|
-
const clause = title.replace(/^Destructive op:\s*/i, "").replace(/\s*×\d+\s*$/, "");
|
|
2946
|
-
const m = clause.match(/^\s*(rm|rmdir)\b(.*)$/i);
|
|
2947
|
-
if (!m) {
|
|
2948
|
-
return false;
|
|
2949
|
-
}
|
|
2950
|
-
const operands = m[2].split(/\s+/).filter((t) => t && !t.startsWith("-"));
|
|
2951
|
-
if (operands.length === 0) {
|
|
2952
|
-
return false;
|
|
2953
|
-
}
|
|
2954
|
-
const receiptJson = (t) => /(?:^|\/)\.receipts\/[^/]+\.json$/.test(t.replace(/^["']|["']$/g, ""));
|
|
2955
|
-
return operands.every(receiptJson);
|
|
2956
|
-
}
|
|
2957
|
-
|
|
2958
|
-
// src/trace/diffScope.ts
|
|
2959
3372
|
function git(args, cwd) {
|
|
2960
3373
|
const r = spawnSync("git", args, { encoding: "utf8", cwd });
|
|
2961
3374
|
return r.status === 0 ? r.stdout : null;
|
|
@@ -2966,22 +3379,22 @@ function changedFiles(baseOverride, opts) {
|
|
|
2966
3379
|
if (!root) {
|
|
2967
3380
|
return null;
|
|
2968
3381
|
}
|
|
2969
|
-
let
|
|
2970
|
-
if (!
|
|
3382
|
+
let base6 = baseOverride;
|
|
3383
|
+
if (!base6) {
|
|
2971
3384
|
const sym = git(["symbolic-ref", "refs/remotes/origin/HEAD"], root)?.trim();
|
|
2972
3385
|
if (sym) {
|
|
2973
|
-
|
|
3386
|
+
base6 = sym.replace("refs/remotes/", "");
|
|
2974
3387
|
} else if (git(["rev-parse", "--verify", "--quiet", "origin/main"], root) !== null) {
|
|
2975
|
-
|
|
3388
|
+
base6 = "origin/main";
|
|
2976
3389
|
} else {
|
|
2977
|
-
|
|
3390
|
+
base6 = "main";
|
|
2978
3391
|
}
|
|
2979
3392
|
}
|
|
2980
|
-
let out = git(["diff", "--name-only", `${
|
|
3393
|
+
let out = git(["diff", "--name-only", `${base6}...HEAD`], root);
|
|
2981
3394
|
if (out === null && !baseOverride) {
|
|
2982
3395
|
out = git(["diff", "--name-only", "main...HEAD"], root);
|
|
2983
3396
|
if (out !== null) {
|
|
2984
|
-
|
|
3397
|
+
base6 = "main";
|
|
2985
3398
|
}
|
|
2986
3399
|
}
|
|
2987
3400
|
const pending = opts?.includeWorkingTree ? workingTreeFiles(root) : [];
|
|
@@ -2992,7 +3405,7 @@ function changedFiles(baseOverride, opts) {
|
|
|
2992
3405
|
if (files.length === 0) {
|
|
2993
3406
|
return null;
|
|
2994
3407
|
}
|
|
2995
|
-
return { base:
|
|
3408
|
+
return { base: base6, files, repoRoot: root };
|
|
2996
3409
|
}
|
|
2997
3410
|
function workingTreeFiles(root) {
|
|
2998
3411
|
const out = git(["status", "--porcelain", "-z", "--untracked-files=all"], root);
|
|
@@ -3014,12 +3427,12 @@ function workingTreeFiles(root) {
|
|
|
3014
3427
|
return files;
|
|
3015
3428
|
}
|
|
3016
3429
|
function inDiff(filePath, files) {
|
|
3017
|
-
const
|
|
3430
|
+
const base6 = filePath.split("/").pop();
|
|
3018
3431
|
for (const d of files) {
|
|
3019
3432
|
if (d === filePath || d.endsWith(`/${filePath}`) || filePath.endsWith(`/${d}`)) {
|
|
3020
3433
|
return true;
|
|
3021
3434
|
}
|
|
3022
|
-
if (
|
|
3435
|
+
if (base6 && d.split("/").pop() === base6) {
|
|
3023
3436
|
return true;
|
|
3024
3437
|
}
|
|
3025
3438
|
}
|
|
@@ -3060,7 +3473,7 @@ function keepUnderDiff(f, files, commandFor, locusFor) {
|
|
|
3060
3473
|
return cmd ? gitTouchesDiff(cmd, files) : false;
|
|
3061
3474
|
}
|
|
3062
3475
|
if (isDestructive(f.id)) {
|
|
3063
|
-
if (destructiveOutsideRepo(f.title) || regenerableReceiptRm(f.title)) {
|
|
3476
|
+
if (destructiveOutsideRepo(f.title) || regenerableReceiptRm(f.title) || regenerableBuildArtifact(f.title)) {
|
|
3064
3477
|
return false;
|
|
3065
3478
|
}
|
|
3066
3479
|
const cmd = commandFor?.(f) ?? f.title.replace(/^Destructive op:\s*/i, "").replace(/\s*×\d+\s*$/, "");
|
|
@@ -3136,12 +3549,12 @@ function applyDiffScope(derived, findings, files, projectPath) {
|
|
|
3136
3549
|
if (!projectPath || !f.evidenceSpanId) {
|
|
3137
3550
|
return void 0;
|
|
3138
3551
|
}
|
|
3139
|
-
const
|
|
3140
|
-
const state = cwdAtFirstGit(spanCmd.get(f.evidenceSpanId) ?? "",
|
|
3552
|
+
const base6 = spanCwd.get(f.evidenceSpanId);
|
|
3553
|
+
const state = cwdAtFirstGit(spanCmd.get(f.evidenceSpanId) ?? "", base6);
|
|
3141
3554
|
if (state.kind === "unknown") {
|
|
3142
3555
|
return "elsewhere";
|
|
3143
3556
|
}
|
|
3144
|
-
const at = state.kind === "known" ? state.path :
|
|
3557
|
+
const at = state.kind === "known" ? state.path : base6;
|
|
3145
3558
|
if (!at) {
|
|
3146
3559
|
return void 0;
|
|
3147
3560
|
}
|
|
@@ -3156,7 +3569,7 @@ function applyDiffScope(derived, findings, files, projectPath) {
|
|
|
3156
3569
|
const destructiveCount = derived.spans.filter((s) => s.destructive).filter((s) => {
|
|
3157
3570
|
const cmd = commandOf(s.input);
|
|
3158
3571
|
const clause = destructiveMatch(cmd) ?? cmd;
|
|
3159
|
-
if (destructiveOutsideRepo(clause) || regenerableReceiptRm(clause)) {
|
|
3572
|
+
if (destructiveOutsideRepo(clause) || regenerableReceiptRm(clause) || regenerableBuildArtifact(clause)) {
|
|
3160
3573
|
return false;
|
|
3161
3574
|
}
|
|
3162
3575
|
if (/(^|[\s;&|])git\s/.test(cmd)) {
|
|
@@ -3253,8 +3666,9 @@ var intAfter = (line, re) => {
|
|
|
3253
3666
|
const m = line.match(re);
|
|
3254
3667
|
return m ? Number.parseInt(m[1], 10) : void 0;
|
|
3255
3668
|
};
|
|
3669
|
+
var PYTEST_SUMMARY = /\b\d+\s+(?:passed|failed|skipped|errors?)\b[^\n]*?\bin\s+\d+(?:\.\d+)?s\b/;
|
|
3256
3670
|
function parsePytest(o) {
|
|
3257
|
-
const line = o.split("\n").reverse().find((l) =>
|
|
3671
|
+
const line = o.split("\n").reverse().find((l) => PYTEST_SUMMARY.test(l));
|
|
3258
3672
|
if (!line) {
|
|
3259
3673
|
return void 0;
|
|
3260
3674
|
}
|
|
@@ -3274,6 +3688,30 @@ function parsePytest(o) {
|
|
|
3274
3688
|
durationMs: dur2 ? Math.round(Number.parseFloat(dur2[1]) * 1e3) : void 0
|
|
3275
3689
|
};
|
|
3276
3690
|
}
|
|
3691
|
+
function parseUnittest(o) {
|
|
3692
|
+
const ran = o.match(/^Ran\s+(\d+)\s+tests?\s+in\s+([\d.]+)s/m);
|
|
3693
|
+
if (!ran) {
|
|
3694
|
+
return void 0;
|
|
3695
|
+
}
|
|
3696
|
+
const passedVerdict = /^OK\b/m.test(o);
|
|
3697
|
+
const failedVerdict = o.match(/^FAILED\s*\(([^)]*)\)/m);
|
|
3698
|
+
if (!passedVerdict && !failedVerdict) {
|
|
3699
|
+
return void 0;
|
|
3700
|
+
}
|
|
3701
|
+
const detail = failedVerdict ? failedVerdict[1] : o.match(/^OK\s*\(([^)]*)\)/m)?.[1] ?? "";
|
|
3702
|
+
const failures = intAfter(detail, /failures=(\d+)/) ?? 0;
|
|
3703
|
+
const errors = intAfter(detail, /errors=(\d+)/) ?? 0;
|
|
3704
|
+
const skipped = intAfter(detail, /skipp?ed=(\d+)/);
|
|
3705
|
+
const total = Number.parseInt(ran[1], 10);
|
|
3706
|
+
const failed = failures + errors;
|
|
3707
|
+
return {
|
|
3708
|
+
runner: "unittest",
|
|
3709
|
+
passed: Math.max(0, total - failed - (skipped ?? 0)),
|
|
3710
|
+
failed,
|
|
3711
|
+
skipped,
|
|
3712
|
+
durationMs: Math.round(Number.parseFloat(ran[2]) * 1e3)
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
3277
3715
|
function parseJestVitest(o) {
|
|
3278
3716
|
const line = o.split("\n").find((l) => /^\s*Tests?\b/.test(l) && /\b\d+\s+(passed|failed)/.test(l));
|
|
3279
3717
|
if (!line) {
|
|
@@ -3313,8 +3751,21 @@ function parseGo(o) {
|
|
|
3313
3751
|
skipped: skipped || void 0
|
|
3314
3752
|
};
|
|
3315
3753
|
}
|
|
3754
|
+
var PARSERS = [
|
|
3755
|
+
{ name: "pytest", fn: parsePytest },
|
|
3756
|
+
{ name: "unittest", fn: parseUnittest },
|
|
3757
|
+
{ name: "jest/vitest", fn: parseJestVitest },
|
|
3758
|
+
{ name: "cargo", fn: parseCargo },
|
|
3759
|
+
{ name: "go", fn: parseGo }
|
|
3760
|
+
];
|
|
3316
3761
|
function parseTestMetrics(output) {
|
|
3317
|
-
|
|
3762
|
+
let p;
|
|
3763
|
+
for (const parser of PARSERS) {
|
|
3764
|
+
p = parser.fn(output);
|
|
3765
|
+
if (p) {
|
|
3766
|
+
break;
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3318
3769
|
if (!p) {
|
|
3319
3770
|
return void 0;
|
|
3320
3771
|
}
|
|
@@ -3378,7 +3829,7 @@ function renderLedger(rows) {
|
|
|
3378
3829
|
// src/receipt/build.ts
|
|
3379
3830
|
var PREDICATE_TYPE = "https://receipts.dev/agent-execution/v1";
|
|
3380
3831
|
var STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
3381
|
-
var TEST_CMD = /\b(pytest|jest|vitest|npm (run )?test|yarn test|go test|cargo test|tsc|eslint|dbt (test|build)|mocha|rspec|phpunit|gradle test|mvn test)\b/i;
|
|
3832
|
+
var TEST_CMD = /\b(pytest|unittest|manage\.py test|jest|vitest|npm (run )?test|yarn test|go test|cargo test|tsc|eslint|dbt (test|build)|mocha|rspec|phpunit|gradle test|mvn test)\b/i;
|
|
3382
3833
|
var round = (n, places) => {
|
|
3383
3834
|
const f = 10 ** places;
|
|
3384
3835
|
return Math.round((n || 0) * f) / f;
|
|
@@ -3529,6 +3980,12 @@ async function buildReceipt(session, derived, findings, opts = {}) {
|
|
|
3529
3980
|
if (opts.scope) {
|
|
3530
3981
|
predicate.scope = opts.scope;
|
|
3531
3982
|
}
|
|
3983
|
+
if (opts.guardEvents?.length) {
|
|
3984
|
+
predicate.guardEvents = opts.guardEvents;
|
|
3985
|
+
}
|
|
3986
|
+
if (opts.subagents && (opts.subagents.surfaced.length > 0 || opts.subagents.total > 0)) {
|
|
3987
|
+
predicate.subagents = opts.subagents;
|
|
3988
|
+
}
|
|
3532
3989
|
const claims = deriveClaims(
|
|
3533
3990
|
finalAssistantText(derived),
|
|
3534
3991
|
predicate.evidence.testsRan,
|
|
@@ -5184,9 +5641,6 @@ export {
|
|
|
5184
5641
|
deriveFindings,
|
|
5185
5642
|
gradeLetter,
|
|
5186
5643
|
renderLedger,
|
|
5187
|
-
privileged,
|
|
5188
|
-
findingSurface,
|
|
5189
|
-
destructiveOutsideRepo,
|
|
5190
5644
|
changedFiles,
|
|
5191
5645
|
inDiff,
|
|
5192
5646
|
narrowEffort,
|
|
@@ -5221,4 +5675,4 @@ export {
|
|
|
5221
5675
|
redact,
|
|
5222
5676
|
redactReceipt
|
|
5223
5677
|
};
|
|
5224
|
-
//# sourceMappingURL=chunk-
|
|
5678
|
+
//# sourceMappingURL=chunk-FPDXOAYN.js.map
|