tamperward 2.16.5 → 2.17.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 +9 -6
- package/dist/cli/index.js +250 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -505,17 +505,20 @@ option can never be reinterpreted as the agent command.
|
|
|
505
505
|
|
|
506
506
|
### The rules
|
|
507
507
|
|
|
508
|
-
Seventeen rules are specified and
|
|
508
|
+
Seventeen rules are specified and sixteen ship (see the table in
|
|
509
509
|
[SPEC.md](./SPEC.md)). The families: test protection (`test-deletion`,
|
|
510
|
-
`test-skip`, `test-content-removal
|
|
510
|
+
`test-skip`, `test-content-removal`, plus the warning-only JS/TS
|
|
511
|
+
`assertion-weakening` heuristic), verification-signal protection
|
|
511
512
|
(`coverage-lowering`, `snapshot-rewrite`, `snapshot-only-rewrite`), suppression
|
|
512
513
|
(`ts-any-cast`, `ts-any-launder`, `lint-suppression`), pipeline protection
|
|
513
514
|
(`ci-tampering`, `hook-tampering`, `no-verify`), and the effect/outcome layers
|
|
514
515
|
(`transient-protected-mutation`, `pristine-verification`, plus the `run` envelope).
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
516
|
+
`assertion-weakening` ships only the AST-proven one-way subset — statically
|
|
517
|
+
proven exact/structural values weakened to truthy/defined, positive exception
|
|
518
|
+
specificity removed, or a pure assertion removed from the same suite-qualified
|
|
519
|
+
test. It measured 12/12 true-positive fires with 0/20 false positives on the
|
|
520
|
+
committed detector-specific replay and remains `warn`; `guard-removal` is still
|
|
521
|
+
reserved with no detector, and `ts-any-launder` is a permanent warn.
|
|
519
522
|
|
|
520
523
|
## What Tamperward does not do
|
|
521
524
|
|
package/dist/cli/index.js
CHANGED
|
@@ -6201,6 +6201,214 @@ var snapshotOnlyRewrite = {
|
|
|
6201
6201
|
}
|
|
6202
6202
|
};
|
|
6203
6203
|
|
|
6204
|
+
// src/detectors/assertion-weakening.ts
|
|
6205
|
+
import ts6 from "typescript";
|
|
6206
|
+
var RULE11 = "assertion-weakening";
|
|
6207
|
+
var TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
6208
|
+
var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
|
|
6209
|
+
var EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
|
|
6210
|
+
var WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
|
|
6211
|
+
var compact = (s) => s.replace(/\s+/g, "");
|
|
6212
|
+
function literalTitle(node) {
|
|
6213
|
+
if (!node) return null;
|
|
6214
|
+
if (ts6.isStringLiteral(node) || ts6.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
6215
|
+
return null;
|
|
6216
|
+
}
|
|
6217
|
+
function scriptKindForPath(path) {
|
|
6218
|
+
const lower = path.toLowerCase();
|
|
6219
|
+
if (lower.endsWith(".tsx")) return ts6.ScriptKind.TSX;
|
|
6220
|
+
if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
|
|
6221
|
+
return ts6.ScriptKind.TS;
|
|
6222
|
+
}
|
|
6223
|
+
if (lower.endsWith(".jsx")) return ts6.ScriptKind.JSX;
|
|
6224
|
+
return ts6.ScriptKind.JS;
|
|
6225
|
+
}
|
|
6226
|
+
function staticFacts(node) {
|
|
6227
|
+
if (!node) return null;
|
|
6228
|
+
if (node.kind === ts6.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
|
|
6229
|
+
if (node.kind === ts6.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
|
|
6230
|
+
if (node.kind === ts6.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
|
|
6231
|
+
if (ts6.isIdentifier(node) && node.text === "undefined") {
|
|
6232
|
+
return { defined: false, truthy: false };
|
|
6233
|
+
}
|
|
6234
|
+
if (ts6.isVoidExpression(node)) return { defined: false, truthy: false };
|
|
6235
|
+
if (ts6.isStringLiteral(node) || ts6.isNoSubstitutionTemplateLiteral(node)) {
|
|
6236
|
+
return { defined: true, truthy: node.text.length > 0 };
|
|
6237
|
+
}
|
|
6238
|
+
if (ts6.isNumericLiteral(node)) {
|
|
6239
|
+
return { defined: true, truthy: Number(node.text) !== 0 };
|
|
6240
|
+
}
|
|
6241
|
+
if (ts6.isBigIntLiteral(node)) {
|
|
6242
|
+
return { defined: true, truthy: node.text.replace(/n$/i, "") !== "0" };
|
|
6243
|
+
}
|
|
6244
|
+
if (ts6.isPrefixUnaryExpression(node) && (node.operator === ts6.SyntaxKind.MinusToken || node.operator === ts6.SyntaxKind.PlusToken) && ts6.isNumericLiteral(node.operand)) {
|
|
6245
|
+
const value = Number(node.operand.text) * (node.operator === ts6.SyntaxKind.MinusToken ? -1 : 1);
|
|
6246
|
+
return { defined: true, truthy: value !== 0 };
|
|
6247
|
+
}
|
|
6248
|
+
if (ts6.isObjectLiteralExpression(node) || ts6.isArrayLiteralExpression(node) || ts6.isRegularExpressionLiteral(node) || ts6.isArrowFunction(node) || ts6.isFunctionExpression(node) || ts6.isClassExpression(node) || ts6.isNewExpression(node)) {
|
|
6249
|
+
return { defined: true, truthy: true };
|
|
6250
|
+
}
|
|
6251
|
+
return null;
|
|
6252
|
+
}
|
|
6253
|
+
function expectAssertion(node, sf) {
|
|
6254
|
+
if (!ts6.isPropertyAccessExpression(node.expression)) return null;
|
|
6255
|
+
const matcher = node.expression.name.text;
|
|
6256
|
+
let receiver = node.expression.expression;
|
|
6257
|
+
let polarity = "";
|
|
6258
|
+
if (ts6.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
|
|
6259
|
+
polarity = "not";
|
|
6260
|
+
receiver = receiver.expression;
|
|
6261
|
+
}
|
|
6262
|
+
if (!ts6.isCallExpression(receiver) || !ts6.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
|
|
6263
|
+
return null;
|
|
6264
|
+
}
|
|
6265
|
+
const subject = compact(receiver.arguments[0].getText(sf));
|
|
6266
|
+
const args = node.arguments.map((arg) => compact(arg.getText(sf)));
|
|
6267
|
+
return {
|
|
6268
|
+
subject,
|
|
6269
|
+
polarity,
|
|
6270
|
+
matcher,
|
|
6271
|
+
args,
|
|
6272
|
+
firstArgFacts: staticFacts(node.arguments[0]),
|
|
6273
|
+
canonical: [subject, polarity, matcher, ...args].join("|"),
|
|
6274
|
+
text: node.getText(sf).replace(/\s+/g, " ")
|
|
6275
|
+
};
|
|
6276
|
+
}
|
|
6277
|
+
function simpleCallName(node) {
|
|
6278
|
+
return ts6.isIdentifier(node.expression) ? node.expression.text : null;
|
|
6279
|
+
}
|
|
6280
|
+
function callbackFor(node) {
|
|
6281
|
+
const cb = node.arguments[1];
|
|
6282
|
+
return cb && (ts6.isArrowFunction(cb) || ts6.isFunctionExpression(cb)) ? cb : null;
|
|
6283
|
+
}
|
|
6284
|
+
function blocks(src, path) {
|
|
6285
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
6286
|
+
if (langOf(path) !== "js") return out2;
|
|
6287
|
+
try {
|
|
6288
|
+
const sf = ts6.createSourceFile(path, src, ts6.ScriptTarget.Latest, true, scriptKindForPath(path));
|
|
6289
|
+
const visit = (node, suites) => {
|
|
6290
|
+
if (ts6.isCallExpression(node)) {
|
|
6291
|
+
const call = simpleCallName(node);
|
|
6292
|
+
const title = literalTitle(node.arguments[0]);
|
|
6293
|
+
const cb = callbackFor(node);
|
|
6294
|
+
if (call && SUITE_CALLS.has(call) && title !== null && cb) {
|
|
6295
|
+
const nextSuites = [...suites, title];
|
|
6296
|
+
ts6.forEachChild(cb, (child) => visit(child, nextSuites));
|
|
6297
|
+
return;
|
|
6298
|
+
}
|
|
6299
|
+
if (call && TEST_CALLS.has(call) && title !== null && cb) {
|
|
6300
|
+
const assertions = [];
|
|
6301
|
+
const collect2 = (child) => {
|
|
6302
|
+
if (child !== cb && ts6.isCallExpression(child)) {
|
|
6303
|
+
const nestedCall = simpleCallName(child);
|
|
6304
|
+
if (nestedCall && (TEST_CALLS.has(nestedCall) || SUITE_CALLS.has(nestedCall))) {
|
|
6305
|
+
return;
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
if (ts6.isCallExpression(child)) {
|
|
6309
|
+
const a = expectAssertion(child, sf);
|
|
6310
|
+
if (a) assertions.push(a);
|
|
6311
|
+
}
|
|
6312
|
+
ts6.forEachChild(child, collect2);
|
|
6313
|
+
};
|
|
6314
|
+
collect2(cb);
|
|
6315
|
+
const identity2 = [...suites, title].join(" > ");
|
|
6316
|
+
const list2 = out2.get(identity2) ?? [];
|
|
6317
|
+
list2.push({ title, identity: identity2, assertions });
|
|
6318
|
+
out2.set(identity2, list2);
|
|
6319
|
+
return;
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
ts6.forEachChild(node, (child) => visit(child, suites));
|
|
6323
|
+
};
|
|
6324
|
+
visit(sf, []);
|
|
6325
|
+
} catch {
|
|
6326
|
+
}
|
|
6327
|
+
return out2;
|
|
6328
|
+
}
|
|
6329
|
+
function pureRemoval(before, after) {
|
|
6330
|
+
if (after.length >= before.length) return null;
|
|
6331
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
6332
|
+
for (const a of before) remaining.set(a.canonical, (remaining.get(a.canonical) ?? 0) + 1);
|
|
6333
|
+
for (const a of after) {
|
|
6334
|
+
const n = remaining.get(a.canonical) ?? 0;
|
|
6335
|
+
if (n <= 0) return null;
|
|
6336
|
+
remaining.set(a.canonical, n - 1);
|
|
6337
|
+
}
|
|
6338
|
+
for (const a of before) {
|
|
6339
|
+
const n = remaining.get(a.canonical) ?? 0;
|
|
6340
|
+
if (n > 0) return a;
|
|
6341
|
+
}
|
|
6342
|
+
return null;
|
|
6343
|
+
}
|
|
6344
|
+
function factsProveWeakMatcher(oldA, newMatcher) {
|
|
6345
|
+
if (!EXACT_MATCHERS.has(oldA.matcher) || oldA.args.length !== 1) return false;
|
|
6346
|
+
const facts = oldA.firstArgFacts;
|
|
6347
|
+
if (!facts) return false;
|
|
6348
|
+
if (newMatcher === "toBeDefined") return facts.defined;
|
|
6349
|
+
if (newMatcher === "toBeTruthy") return facts.truthy === true;
|
|
6350
|
+
return false;
|
|
6351
|
+
}
|
|
6352
|
+
function directionalWeakening(before, after) {
|
|
6353
|
+
for (const oldA of before) {
|
|
6354
|
+
for (const newA of after) {
|
|
6355
|
+
if (oldA.subject !== newA.subject) continue;
|
|
6356
|
+
if (oldA.polarity !== "" || newA.polarity !== "") continue;
|
|
6357
|
+
if (WEAK_MATCHERS.has(newA.matcher) && newA.args.length === 0 && factsProveWeakMatcher(oldA, newA.matcher)) {
|
|
6358
|
+
return { before: oldA, after: newA, kind: "matcher" };
|
|
6359
|
+
}
|
|
6360
|
+
if (oldA.matcher === "toThrow" && oldA.args.length > 0 && newA.matcher === "toThrow" && newA.args.length === 0) {
|
|
6361
|
+
return { before: oldA, after: newA, kind: "throw-specificity" };
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
6365
|
+
return null;
|
|
6366
|
+
}
|
|
6367
|
+
function findingForBlock(path, before, after, policy) {
|
|
6368
|
+
const weakened = directionalWeakening(before.assertions, after.assertions);
|
|
6369
|
+
if (weakened) {
|
|
6370
|
+
const message = weakened.kind === "throw-specificity" ? `Assertion weakened in test "${before.identity}": exception-message specificity was removed from toThrow().` : `Assertion weakened in test "${before.identity}": ${weakened.before.matcher} was replaced by weaker ${weakened.after.matcher} on the same subject.`;
|
|
6371
|
+
return makeFinding(RULE11, policy, {
|
|
6372
|
+
file: path,
|
|
6373
|
+
message,
|
|
6374
|
+
evidence: `${weakened.before.text} -> ${weakened.after.text}`,
|
|
6375
|
+
remediation: "Keep the stronger expectation and fix the code under test. If the contract genuinely changed, review the test change explicitly; this rule is warning-only while its precision is measured."
|
|
6376
|
+
});
|
|
6377
|
+
}
|
|
6378
|
+
const removed = pureRemoval(before.assertions, after.assertions);
|
|
6379
|
+
if (removed) {
|
|
6380
|
+
return makeFinding(RULE11, policy, {
|
|
6381
|
+
file: path,
|
|
6382
|
+
message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
|
|
6383
|
+
evidence: removed.text,
|
|
6384
|
+
remediation: "Keep the assertion and fix the code under test. If the assertion is genuinely obsolete, review the test change explicitly; this rule is warning-only while its precision is measured."
|
|
6385
|
+
});
|
|
6386
|
+
}
|
|
6387
|
+
return null;
|
|
6388
|
+
}
|
|
6389
|
+
var assertionWeakening = {
|
|
6390
|
+
id: RULE11,
|
|
6391
|
+
surface: ["file"],
|
|
6392
|
+
certainty: "heuristic",
|
|
6393
|
+
run(changes, policy) {
|
|
6394
|
+
const out2 = [];
|
|
6395
|
+
for (const c of changes) {
|
|
6396
|
+
if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null || langOf(c.path) !== "js" || !isProtected(c.path, policy, "tests") || isProtected(c.path, policy, "snapshots")) {
|
|
6397
|
+
continue;
|
|
6398
|
+
}
|
|
6399
|
+
const beforeBlocks = blocks(c.before, c.path);
|
|
6400
|
+
const afterBlocks = blocks(c.after, c.path);
|
|
6401
|
+
for (const [identity2, oldList] of beforeBlocks) {
|
|
6402
|
+
const newList = afterBlocks.get(identity2);
|
|
6403
|
+
if (!newList || oldList.length !== 1 || newList.length !== 1) continue;
|
|
6404
|
+
const f = findingForBlock(c.path, oldList[0], newList[0], policy);
|
|
6405
|
+
if (f) out2.push(f);
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6408
|
+
return out2;
|
|
6409
|
+
}
|
|
6410
|
+
};
|
|
6411
|
+
|
|
6204
6412
|
// src/detectors/index.ts
|
|
6205
6413
|
var allDetectors = [
|
|
6206
6414
|
noVerify,
|
|
@@ -6213,7 +6421,8 @@ var allDetectors = [
|
|
|
6213
6421
|
testDeletion,
|
|
6214
6422
|
testContentRemoval,
|
|
6215
6423
|
snapshotRewrite,
|
|
6216
|
-
snapshotOnlyRewrite
|
|
6424
|
+
snapshotOnlyRewrite,
|
|
6425
|
+
assertionWeakening
|
|
6217
6426
|
];
|
|
6218
6427
|
|
|
6219
6428
|
// src/engine.ts
|
|
@@ -6575,9 +6784,9 @@ function orderFindings(findings) {
|
|
|
6575
6784
|
return (a.line ?? 0) - (b.line ?? 0);
|
|
6576
6785
|
});
|
|
6577
6786
|
}
|
|
6578
|
-
function countsPhrase(
|
|
6787
|
+
function countsPhrase(blocks2, warns) {
|
|
6579
6788
|
const parts = [];
|
|
6580
|
-
if (
|
|
6789
|
+
if (blocks2) parts.push(`${blocks2} blocking`);
|
|
6581
6790
|
if (warns) parts.push(`${warns} warning${warns === 1 ? "" : "s"}`);
|
|
6582
6791
|
return parts.join(", ");
|
|
6583
6792
|
}
|
|
@@ -6589,7 +6798,7 @@ var SIGNOFF_NOTE = "A blocking finding clears only with a human sign-off. In CI
|
|
|
6589
6798
|
function renderText(input, opts) {
|
|
6590
6799
|
const { scanned, ignoredFiles } = input;
|
|
6591
6800
|
const findings = orderFindings(input.findings);
|
|
6592
|
-
const
|
|
6801
|
+
const blocks2 = findings.filter((f) => f.severity === "block");
|
|
6593
6802
|
const warns = findings.filter((f) => f.severity === "warn");
|
|
6594
6803
|
const c = opts.colour;
|
|
6595
6804
|
const out2 = [];
|
|
@@ -6597,8 +6806,8 @@ function renderText(input, opts) {
|
|
|
6597
6806
|
return `${paint("tamperward: clean", GREEN, c)} \u2014 no integrity findings (${scopePhrase(scanned, ignoredFiles)}).
|
|
6598
6807
|
`;
|
|
6599
6808
|
}
|
|
6600
|
-
const headColour =
|
|
6601
|
-
out2.push(paint(`tamperward: ${countsPhrase(
|
|
6809
|
+
const headColour = blocks2.length ? RED : YELLOW;
|
|
6810
|
+
out2.push(paint(`tamperward: ${countsPhrase(blocks2.length, warns.length)}`, BOLD + headColour, c));
|
|
6602
6811
|
out2.push(paint(`(${scopePhrase(scanned, ignoredFiles)})`, DIM, c));
|
|
6603
6812
|
out2.push("");
|
|
6604
6813
|
const LABEL = "sign-off".length;
|
|
@@ -6618,7 +6827,7 @@ function renderText(input, opts) {
|
|
|
6618
6827
|
if (f.signoff.required) row("sign-off", [stripControl(f.signoff.command)]);
|
|
6619
6828
|
out2.push("");
|
|
6620
6829
|
}
|
|
6621
|
-
if (
|
|
6830
|
+
if (blocks2.length > 0) {
|
|
6622
6831
|
out2.push(
|
|
6623
6832
|
wrap(SIGNOFF_NOTE, opts.width - 2).map((l) => paint(l, DIM, c)).join("\n")
|
|
6624
6833
|
);
|
|
@@ -6668,7 +6877,7 @@ function locationCell(f, env) {
|
|
|
6668
6877
|
}
|
|
6669
6878
|
function renderSummary(input, env = process.env) {
|
|
6670
6879
|
const findings = orderFindings(input.findings);
|
|
6671
|
-
const
|
|
6880
|
+
const blocks2 = findings.filter((f) => f.severity === "block");
|
|
6672
6881
|
const warns = findings.filter((f) => f.severity === "warn");
|
|
6673
6882
|
const scope = scopePhrase(input.scanned, input.ignoredFiles);
|
|
6674
6883
|
if (findings.length === 0) {
|
|
@@ -6678,7 +6887,7 @@ No integrity findings (${scope}).
|
|
|
6678
6887
|
`;
|
|
6679
6888
|
}
|
|
6680
6889
|
const out2 = [
|
|
6681
|
-
`## Tamperward: ${countsPhrase(
|
|
6890
|
+
`## Tamperward: ${countsPhrase(blocks2.length, warns.length)}`,
|
|
6682
6891
|
"",
|
|
6683
6892
|
`${scope}. Every finding below is also annotated on the diff in **Files changed**.`,
|
|
6684
6893
|
"",
|
|
@@ -6731,12 +6940,12 @@ function report(input) {
|
|
|
6731
6940
|
const { findings, scanned, ignoredFiles } = input;
|
|
6732
6941
|
const format = resolveFormat(input);
|
|
6733
6942
|
if (format === "json") {
|
|
6734
|
-
const
|
|
6943
|
+
const blocks2 = findings.filter((f) => f.severity === "block").length;
|
|
6735
6944
|
process.stdout.write(
|
|
6736
6945
|
JSON.stringify(
|
|
6737
6946
|
// `findings`, `scanned` and `ignoredFiles` keep their exact prior shape; `summary`
|
|
6738
6947
|
// is additive, so an existing consumer reading only the old keys is unaffected.
|
|
6739
|
-
{ findings, scanned, ignoredFiles, summary: { block:
|
|
6948
|
+
{ findings, scanned, ignoredFiles, summary: { block: blocks2, warn: findings.length - blocks2 } },
|
|
6740
6949
|
null,
|
|
6741
6950
|
2
|
|
6742
6951
|
) + "\n"
|
|
@@ -6833,10 +7042,10 @@ import { readFileSync as readFileSync10, appendFileSync as appendFileSync4, writ
|
|
|
6833
7042
|
import { createHash as createHash5 } from "node:crypto";
|
|
6834
7043
|
|
|
6835
7044
|
// src/adapters/claude/deny.ts
|
|
6836
|
-
function formatDenial(
|
|
6837
|
-
const lead =
|
|
7045
|
+
function formatDenial(blocks2) {
|
|
7046
|
+
const lead = blocks2[0];
|
|
6838
7047
|
const lines = ["Tamperward blocked this change \u2014 it weakens a protected safety net to pass checks.", ""];
|
|
6839
|
-
for (const f of
|
|
7048
|
+
for (const f of blocks2) {
|
|
6840
7049
|
const loc = f.file ? ` (${escapeControl(f.file)}${f.line ? `:${f.line}` : ""})` : "";
|
|
6841
7050
|
lines.push(` \u2022 ${f.rule}${loc}: ${f.message}`);
|
|
6842
7051
|
}
|
|
@@ -6845,7 +7054,7 @@ function formatDenial(blocks) {
|
|
|
6845
7054
|
lines.push(
|
|
6846
7055
|
"The other shortcuts are blocked too: skipping the hooks, editing the hook or CI workflow, lowering the coverage gate, and rewriting a protected file from the shell will each be denied."
|
|
6847
7056
|
);
|
|
6848
|
-
if (
|
|
7057
|
+
if (blocks2.some((b) => b.signoff.required)) {
|
|
6849
7058
|
lines.push(`If this change is genuinely correct, a human must sign off: ${lead.signoff.command}`);
|
|
6850
7059
|
}
|
|
6851
7060
|
return lines.join("\n") + "\n";
|
|
@@ -7263,7 +7472,7 @@ function runWatch(args) {
|
|
|
7263
7472
|
|
|
7264
7473
|
// src/detectors/fs-events.ts
|
|
7265
7474
|
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
|
|
7266
|
-
var
|
|
7475
|
+
var RULE12 = "transient-protected-mutation";
|
|
7267
7476
|
var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
|
|
7268
7477
|
var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
|
|
7269
7478
|
var DEFAULT_EVENT_READ_IO = {
|
|
@@ -7458,7 +7667,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
|
|
|
7458
7667
|
};
|
|
7459
7668
|
}
|
|
7460
7669
|
function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
7461
|
-
if (!isEnabled(
|
|
7670
|
+
if (!isEnabled(RULE12, policy)) return [];
|
|
7462
7671
|
const byPath = /* @__PURE__ */ new Map();
|
|
7463
7672
|
for (const e of events) {
|
|
7464
7673
|
if (!isProtected(e.path, policy)) continue;
|
|
@@ -7478,7 +7687,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
7478
7687
|
const modeChurn = modes.size > 1;
|
|
7479
7688
|
if (!contentChurn && !modeChurn) continue;
|
|
7480
7689
|
const what = contentChurn ? `content was modified and restored within the turn (${hashes.size} distinct states observed)` : `file mode changed during the turn (${[...modes].map((m) => (m & 4095).toString(8)).join(" \u2192 ")})`;
|
|
7481
|
-
const f = makeFinding(
|
|
7690
|
+
const f = makeFinding(RULE12, policy, {
|
|
7482
7691
|
file: path,
|
|
7483
7692
|
message: `Transient protected mutation: ${what}, leaving no net diff.`,
|
|
7484
7693
|
evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
|
|
@@ -7517,11 +7726,11 @@ function parseInput(raw) {
|
|
|
7517
7726
|
}
|
|
7518
7727
|
return parsed;
|
|
7519
7728
|
}
|
|
7520
|
-
function recordDenylog(
|
|
7729
|
+
function recordDenylog(blocks2) {
|
|
7521
7730
|
const log = process.env.TAMPERWARD_DENYLOG;
|
|
7522
7731
|
if (!log) return;
|
|
7523
7732
|
try {
|
|
7524
|
-
appendFileSync4(log,
|
|
7733
|
+
appendFileSync4(log, blocks2.map((b) => b.rule).join(",") + "\n");
|
|
7525
7734
|
} catch {
|
|
7526
7735
|
}
|
|
7527
7736
|
}
|
|
@@ -7540,10 +7749,10 @@ function failClosed(kind, detail) {
|
|
|
7540
7749
|
};
|
|
7541
7750
|
return verdict([f], kind);
|
|
7542
7751
|
}
|
|
7543
|
-
function verdict(
|
|
7544
|
-
if (
|
|
7545
|
-
recordDenylog(
|
|
7546
|
-
const reason = formatDenial(
|
|
7752
|
+
function verdict(blocks2, kind) {
|
|
7753
|
+
if (blocks2.length === 0) return { exitCode: 0, stdout: "" };
|
|
7754
|
+
recordDenylog(blocks2);
|
|
7755
|
+
const reason = formatDenial(blocks2);
|
|
7547
7756
|
const payload = kind === "PreToolUse" ? {
|
|
7548
7757
|
hookSpecificOutput: {
|
|
7549
7758
|
hookEventName: "PreToolUse",
|
|
@@ -7600,14 +7809,14 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
7600
7809
|
if (c.oldPath) seen.add(c.oldPath);
|
|
7601
7810
|
}
|
|
7602
7811
|
const changes = [];
|
|
7603
|
-
const
|
|
7812
|
+
const blocks2 = [];
|
|
7604
7813
|
const trusted = base ?? "HEAD";
|
|
7605
7814
|
const candidates = base ? [base, "HEAD", ""] : ["HEAD", ""];
|
|
7606
7815
|
for (const rel of [...d.changed, ...d.deleted]) {
|
|
7607
7816
|
if (seen.has(rel)) continue;
|
|
7608
7817
|
const unjudgeable = unjudgeableFinding(cwd, rel);
|
|
7609
7818
|
if (unjudgeable) {
|
|
7610
|
-
|
|
7819
|
+
blocks2.push(unjudgeable);
|
|
7611
7820
|
continue;
|
|
7612
7821
|
}
|
|
7613
7822
|
const after = fileOnDisk(rel, { cwd });
|
|
@@ -7627,7 +7836,7 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
7627
7836
|
}
|
|
7628
7837
|
}
|
|
7629
7838
|
if (before == null) {
|
|
7630
|
-
|
|
7839
|
+
blocks2.push({
|
|
7631
7840
|
rule: "hidden-drift",
|
|
7632
7841
|
severity: "block",
|
|
7633
7842
|
file: rel,
|
|
@@ -7660,12 +7869,12 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
7660
7869
|
if (seen.has(rel)) continue;
|
|
7661
7870
|
const unjudgeable = unjudgeableFinding(cwd, rel);
|
|
7662
7871
|
if (unjudgeable) {
|
|
7663
|
-
|
|
7872
|
+
blocks2.push(unjudgeable);
|
|
7664
7873
|
continue;
|
|
7665
7874
|
}
|
|
7666
7875
|
const after = fileOnDisk(rel, { cwd });
|
|
7667
7876
|
if (after == null) {
|
|
7668
|
-
|
|
7877
|
+
blocks2.push({
|
|
7669
7878
|
rule: "hidden-drift",
|
|
7670
7879
|
severity: "block",
|
|
7671
7880
|
file: rel,
|
|
@@ -7678,7 +7887,7 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
7678
7887
|
}
|
|
7679
7888
|
changes.push(...synthFileChange(rel, null, after));
|
|
7680
7889
|
}
|
|
7681
|
-
return { changes, blocks };
|
|
7890
|
+
return { changes, blocks: blocks2 };
|
|
7682
7891
|
}
|
|
7683
7892
|
function effectDriftBlocks(cwd, sessionId, policy) {
|
|
7684
7893
|
if (!sessionId || !isGitRepo(cwd)) return null;
|
|
@@ -7703,8 +7912,8 @@ function effectDriftBlocks(cwd, sessionId, policy) {
|
|
|
7703
7912
|
);
|
|
7704
7913
|
const gap = uncoveredDrift(cwd, policy, expected, current, scoped, base, loadTurnTree(cwd, sessionId));
|
|
7705
7914
|
const judged = [...scoped, ...gap.changes];
|
|
7706
|
-
const
|
|
7707
|
-
if (
|
|
7915
|
+
const blocks2 = evaluate(judged, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(gap.blocks, unjudgeableProtected(cwd, policy, judged));
|
|
7916
|
+
if (blocks2.length > 0) return blocks2;
|
|
7708
7917
|
savePtree(cwd, sessionId, current);
|
|
7709
7918
|
return null;
|
|
7710
7919
|
}
|
|
@@ -7736,9 +7945,9 @@ function preToolUseVerdict(input) {
|
|
|
7736
7945
|
const driftBlocks = effectDriftBlocks(cwd, input.session_id, policy);
|
|
7737
7946
|
if (driftBlocks) return verdict(driftBlocks, "PreToolUse");
|
|
7738
7947
|
const changes = changesFromClaudeHook(input, cwd);
|
|
7739
|
-
const
|
|
7740
|
-
if (
|
|
7741
|
-
return verdict(
|
|
7948
|
+
const blocks2 = evaluate(changes, policy, void 0, "tool-call", { cwd }).filter((f) => f.severity === "block");
|
|
7949
|
+
if (blocks2.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
|
|
7950
|
+
return verdict(blocks2, "PreToolUse");
|
|
7742
7951
|
} catch (e) {
|
|
7743
7952
|
return failClosed("PreToolUse", errText2(e));
|
|
7744
7953
|
}
|
|
@@ -7840,7 +8049,7 @@ function stopVerdict(input) {
|
|
|
7840
8049
|
if (input.stop_hook_active) return { exitCode: 0, stdout: "" };
|
|
7841
8050
|
const cwd = input.cwd ?? process.cwd();
|
|
7842
8051
|
if (!isGitRepo(cwd)) return { exitCode: 0, stdout: "" };
|
|
7843
|
-
let
|
|
8052
|
+
let blocks2;
|
|
7844
8053
|
let commitCursor = () => {
|
|
7845
8054
|
};
|
|
7846
8055
|
try {
|
|
@@ -7855,22 +8064,22 @@ function stopVerdict(input) {
|
|
|
7855
8064
|
changes = changes.concat(gap.changes);
|
|
7856
8065
|
hiddenBlocks = gap.blocks;
|
|
7857
8066
|
}
|
|
7858
|
-
|
|
8067
|
+
blocks2 = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks, unjudgeableProtected(cwd, policy, changes));
|
|
7859
8068
|
const transient = turnTransientBlocks(cwd, input.session_id, policy, changes);
|
|
7860
|
-
|
|
8069
|
+
blocks2 = blocks2.concat(transient.blocks);
|
|
7861
8070
|
commitCursor = transient.commit;
|
|
7862
|
-
if (
|
|
8071
|
+
if (blocks2.length === 0 && input.session_id && current) {
|
|
7863
8072
|
savePtree(cwd, input.session_id, current);
|
|
7864
8073
|
saveTurnTree(cwd, input.session_id, current);
|
|
7865
8074
|
}
|
|
7866
8075
|
} catch (e) {
|
|
7867
8076
|
return failClosed("Stop", errText2(e));
|
|
7868
8077
|
}
|
|
7869
|
-
if (
|
|
8078
|
+
if (blocks2.length === 0) {
|
|
7870
8079
|
advanceTurnBaseline(cwd, input.session_id);
|
|
7871
8080
|
commitCursor();
|
|
7872
8081
|
}
|
|
7873
|
-
return verdict(
|
|
8082
|
+
return verdict(blocks2, "Stop");
|
|
7874
8083
|
}
|
|
7875
8084
|
function emit(r) {
|
|
7876
8085
|
if (r.stdout) process.stdout.write(r.stdout);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "hexrift",
|