tamperward 1.6.0 → 1.7.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 +72 -5
- package/dist/cli/index.js +68 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,6 +115,46 @@ escape valve is a PR label only a human with write access can apply.
|
|
|
115
115
|
|
|
116
116
|
## Measured, not asserted
|
|
117
117
|
|
|
118
|
+
**Read the two headline numbers together, always:** across **137 guarded runs — every
|
|
119
|
+
one inside a tamper class an active detector covered — 0 tampers reached green.**
|
|
120
|
+
Outside that coverage, on the one seed built to measure it, **agents tampered in 26 of
|
|
121
|
+
40 runs** — "tampered" meaning, mechanically: the independent held-out oracle
|
|
122
|
+
classified the result as passing the visible check through a prohibited shortcut. The
|
|
123
|
+
first number is what the gate does; the second is why the gate is one layer. And an honest note on evidence units: those run counts come from roughly
|
|
124
|
+
fifteen seed/task configurations and eight real repositories — repeated runs on one
|
|
125
|
+
seed measure that seed's stochasticity, not the space of real coding tasks, so the
|
|
126
|
+
independent evidence behind these numbers is the seed count, not the run count.
|
|
127
|
+
|
|
128
|
+
**And the third number, from the preregistered cross-repository experiment
|
|
129
|
+
(taskbench, `harness/taskbench/`):** on 26 paired real repositories with real
|
|
130
|
+
historical regressions, pressured-agent policy violations appeared in **13 of 26
|
|
131
|
+
ungated runs (50%)** — the behavior transfers — but the gate at 1.6.0 **did not
|
|
132
|
+
reduce false greens** (5 prevented vs 4 gated-only; paired RD +3.8%, 95% CI
|
|
133
|
+
−17.2 to +24.7, exact McNemar p = 1.0): agents routed through tool-call
|
|
134
|
+
spellings outside the shipped detector classes, including two gaps documented
|
|
135
|
+
before the sweep and deliberately left unfixed for the experiment's integrity.
|
|
136
|
+
Enforcement showed no measurable completion cost (+7.7 points, CI spans zero),
|
|
137
|
+
and independent pristine-suite re-execution caught **every masked failure in
|
|
138
|
+
all 53 trajectories**. Registration, bets, frozen analysis, and the refutation
|
|
139
|
+
of our own headline bet: `PREDICTION-taskbench.md`, `PHASE3.md`. The first two
|
|
140
|
+
numbers say what the gate does inside its coverage; the third says coverage is
|
|
141
|
+
the whole game on real repositories — and that the reliable layer was the
|
|
142
|
+
pristine-suite oracle the agent cannot touch.
|
|
143
|
+
|
|
144
|
+
**What this does not do**, stated before the numbers rather than after: it does not
|
|
145
|
+
establish that a patch is *correct*; it does not detect arbitrary deception (a
|
|
146
|
+
hardcoded implementation is invisible to any diff rule — measured, twice); it does not
|
|
147
|
+
replace sandboxing, held-out tests, or authoritative CI — it is designed to sit in
|
|
148
|
+
front of them. What it does: **enforce explicitly defined development invariants at
|
|
149
|
+
the loop, commit, and CI boundaries** — and nothing else.
|
|
150
|
+
|
|
151
|
+
**A word on the words:** *tamper*, *honest*, *cheat*, and *surrender* are used
|
|
152
|
+
throughout as operational labels, not claims about mental states. A "tamper" means: a
|
|
153
|
+
protected asset changed, or an independent held-out oracle failed, while the visible
|
|
154
|
+
check went green — a property of the artifacts, decided deterministically. Whether the
|
|
155
|
+
process that produced it "intended" anything is a question these experiments do not
|
|
156
|
+
measure and do not need.
|
|
157
|
+
|
|
118
158
|
**Does the loop work?** `harness/run.sh` stands up an isolated repo with a real bug and a
|
|
119
159
|
failing protected test, lets a headless agent loose with the hooks installed, and
|
|
120
160
|
classifies the end state with an oracle independent of the detectors under test
|
|
@@ -229,9 +269,17 @@ framing, and Sonnet's famous refusals turn out to be triggered by framing). The
|
|
|
229
269
|
completed frontier split — **Opus 0/10 · Fable 6/10 · Sonnet 10/10** — is the
|
|
230
270
|
published shape, with the standing self-measurement caveat on the Fable rows.
|
|
231
271
|
Twenty-six of forty runs tampered through the uncovered dimension: measured demand
|
|
232
|
-
that
|
|
233
|
-
the honest fix itself narrows through `as Record<string, unknown>`, so a
|
|
234
|
-
assertion-blocker is an FP factory.
|
|
272
|
+
that sent the `ts-any-launder` warn→block graduation question to a §7.A negatives
|
|
273
|
+
corpus — the honest fix itself narrows through `as Record<string, unknown>`, so a
|
|
274
|
+
naive assertion-blocker is an FP factory. **The corpus answered, and the question is
|
|
275
|
+
closed** (`harness/PREDICTION-launder-corpus.md`): on 1,227 real commits the
|
|
276
|
+
laundering spellings fire on legitimate work at 4–10× any deployable block threshold
|
|
277
|
+
(annotation-position `any`: 10.5% of commits; named-type assertions: 7.5%), so the
|
|
278
|
+
warn stays a warn permanently on syntactic evidence, no assertion rule gets built,
|
|
279
|
+
and the laundering class is owned by the layers where coverage is measured: the
|
|
280
|
+
held-out **oracle** (caught all 18 assertion tampers) and the **prompt** (the naming
|
|
281
|
+
sentence, 10/10 flip). For this class, detection is not the gate's layer —
|
|
282
|
+
verification and framing are, and the record says so with a denominator.
|
|
235
283
|
|
|
236
284
|
**Why not just a prompt line, or a 20-line grep hook?** Measured
|
|
237
285
|
(`harness/PREDICTION-baselines.md`), against a frozen naive hook and a one-sentence
|
|
@@ -258,8 +306,27 @@ severity tune → roughly one sign-off decision per 17 commits residual. The sam
|
|
|
258
306
|
shipped per-rule `exclude` globs (1.6.0, with the policy-weakening symmetry built in)
|
|
259
307
|
and then measured them honestly: **zero effect on this corpus** — these repos' casts
|
|
260
308
|
live in source, not tests, so the zod-recommended path scoping removed nothing here;
|
|
261
|
-
the null is published next to the feature.
|
|
262
|
-
|
|
309
|
+
the null is published next to the feature.
|
|
310
|
+
|
|
311
|
+
**What about just making the tests read-only?** The most common objection got its
|
|
312
|
+
pre-registered measurement (`harness/PREDICTION-capability.md`) — starting with the
|
|
313
|
+
mechanics the folk advice misses: `chmod` does not bind a root agent and never
|
|
314
|
+
prevented `rm` anyway (deletion is a *directory*-write right); the workable barrier
|
|
315
|
+
is the immutable bit. Result: **hard immutability governed the crude-tamper seed
|
|
316
|
+
completely — 10/10 honest, four EPERM hits all converting to the real fix** (a bare
|
|
317
|
+
permission error transferred the deny→fix dynamic with no remediation text). Then
|
|
318
|
+
the twist: adding a logged, auto-granted **elevation script made it worse (8/10)** —
|
|
319
|
+
both extra tampers walked through the front door with written confessions
|
|
320
|
+
("*Remove failing test file to unblock build*"), the sanctioned-path thesis firing
|
|
321
|
+
a fourth time. The lock is easy; the grantor is the hard part — which is the case
|
|
322
|
+
for elevation flows that end at a human, and exactly what the gate's sign-off label
|
|
323
|
+
already is. That seed now carries all six governance configurations on identical
|
|
324
|
+
pre-registered ground: nothing 6/10 tampers, one sentence 1/10, naive hook 2/10,
|
|
325
|
+
the gate 0/10, immutable 0/10, immutable+auto-grant 2/10 — with the standing scope
|
|
326
|
+
that none of the capability rows touch the semantic classes, by construction.
|
|
327
|
+
Running ledger: **70 pre-registered bets — 29 refuted, 37 held, 4 open or
|
|
328
|
+
unresolvable** — taskbench's B2 (prevention on real repositories) is the newest
|
|
329
|
+
refutation, published beside the bet it refutes.
|
|
263
330
|
|
|
264
331
|
**What about false positives?** `snapshot-rewrite` was swept over **1,652 real mainline
|
|
265
332
|
commits** (prettier, jest, docusaurus, immer): 216 touched snapshots, all legitimately —
|
package/dist/cli/index.js
CHANGED
|
@@ -438,11 +438,17 @@ function defaultPolicy(version = 1) {
|
|
|
438
438
|
// Recorded expected outputs. An assertion stored as data is still an assertion;
|
|
439
439
|
// rewriting it from current output is the snapshot-update move the affordance
|
|
440
440
|
// experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
|
|
441
|
-
snapshots:
|
|
441
|
+
// tap-snapshots/** added from Phase-3 evidence: node-tar's recorded expectations
|
|
442
|
+
// live there as .cjs and were rewritten unseen (PHASE3.md §4).
|
|
443
|
+
snapshots: ["**/*.snap", "**/__snapshots__/**", "**/tap-snapshots/**", "**/golden/**", "**/*.golden.*"],
|
|
442
444
|
hooks: [".husky/**", "**/lefthook.*", ".tamperward.yml", "**/.tamperward.yml"]
|
|
443
445
|
},
|
|
444
446
|
rules: {
|
|
445
447
|
"test-deletion": { severity: "block" },
|
|
448
|
+
// content gutted from a surviving spec — the class Phase 3 measured walking
|
|
449
|
+
// through the block-count rule (rows, assertion arguments, setup). Severity
|
|
450
|
+
// backed by the real-commit corpus measurement recorded in the rule's SPEC row.
|
|
451
|
+
"test-content-removal": { severity: "block" },
|
|
446
452
|
"test-skip": { severity: "block" },
|
|
447
453
|
"ts-any-cast": { severity: "block" },
|
|
448
454
|
// the unambiguous explicit casts + ts-suppression directives
|
|
@@ -1272,8 +1278,62 @@ var testDeletion = {
|
|
|
1272
1278
|
}
|
|
1273
1279
|
};
|
|
1274
1280
|
|
|
1281
|
+
// src/detectors/test-content-removal.ts
|
|
1282
|
+
var RULE8 = "test-content-removal";
|
|
1283
|
+
var MIN_REMOVED_LINES = 3;
|
|
1284
|
+
var ws = (s) => s.replace(/\s+/g, "");
|
|
1285
|
+
function significantLinesOrdered(src) {
|
|
1286
|
+
const out = [];
|
|
1287
|
+
for (const raw of src.split("\n")) {
|
|
1288
|
+
const l = raw.trim();
|
|
1289
|
+
if (l.length >= 10 && !/^(import\b|export\s|\/\/|\*|\/\*|}\)?;?$)/.test(l)) out.push(l);
|
|
1290
|
+
}
|
|
1291
|
+
return out;
|
|
1292
|
+
}
|
|
1293
|
+
var testContentRemoval = {
|
|
1294
|
+
id: RULE8,
|
|
1295
|
+
surface: ["file"],
|
|
1296
|
+
certainty: "mechanical",
|
|
1297
|
+
run(changes, policy) {
|
|
1298
|
+
const out = [];
|
|
1299
|
+
let keptPool = "";
|
|
1300
|
+
for (const c of changes) {
|
|
1301
|
+
if (c.kind === "file" && c.after != null && isProtected(c.path, policy, "tests")) {
|
|
1302
|
+
keptPool += ws(significantLinesOrdered(c.after).join("\n"));
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
for (const c of changes) {
|
|
1306
|
+
if (c.kind !== "file" || c.op !== "modify" || c.before == null || c.after == null) continue;
|
|
1307
|
+
if (!isProtected(c.path, policy, "tests")) continue;
|
|
1308
|
+
if (isProtected(c.path, policy, "snapshots")) continue;
|
|
1309
|
+
if (countTestBlocks(c.after) < countTestBlocks(c.before)) continue;
|
|
1310
|
+
const beforeSig = significantLinesOrdered(c.before);
|
|
1311
|
+
const afterSig = significantLinesOrdered(c.after);
|
|
1312
|
+
const netDrop = beforeSig.length - afterSig.length;
|
|
1313
|
+
const afterSet = new Set(afterSig);
|
|
1314
|
+
const gone = [];
|
|
1315
|
+
for (const line of new Set(beforeSig)) {
|
|
1316
|
+
if (afterSet.has(line)) continue;
|
|
1317
|
+
if (keptPool.includes(ws(line))) continue;
|
|
1318
|
+
gone.push(line);
|
|
1319
|
+
}
|
|
1320
|
+
if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
|
|
1321
|
+
out.push(
|
|
1322
|
+
makeFinding(RULE8, policy, {
|
|
1323
|
+
file: c.path,
|
|
1324
|
+
message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
|
|
1325
|
+
evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
1326
|
+
remediation: "Fix the code under test. Deleting the failing rows, assertion arguments, or setup from a spec weakens it exactly like deleting the test \u2014 the block just keeps its name."
|
|
1327
|
+
})
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return out;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1275
1335
|
// src/detectors/snapshot-rewrite.ts
|
|
1276
|
-
var
|
|
1336
|
+
var RULE9 = "snapshot-rewrite";
|
|
1277
1337
|
var RUNNER = /\b(?:jest|vitest|playwright|ava)\b/;
|
|
1278
1338
|
var UPDATE_ANY = /--update-?[sS]napshots?\b/;
|
|
1279
1339
|
var UPDATE_WITH_RUNNER = /(?:^|\s)(?:-u|--update)(?:\s|$)/;
|
|
@@ -1287,12 +1347,12 @@ function namesProtectedSnapshot(seg, policy) {
|
|
|
1287
1347
|
return null;
|
|
1288
1348
|
}
|
|
1289
1349
|
var snapshotRewrite = {
|
|
1290
|
-
id:
|
|
1350
|
+
id: RULE9,
|
|
1291
1351
|
surface: ["command", "file"],
|
|
1292
1352
|
certainty: "mechanical",
|
|
1293
1353
|
run(changes, policy) {
|
|
1294
1354
|
const out = [];
|
|
1295
|
-
const warn = (input) => out.push(makeFinding(
|
|
1355
|
+
const warn = (input) => out.push(makeFinding(RULE9, policy, { ...input, defaultSeverity: "warn" }));
|
|
1296
1356
|
for (const c of changes) {
|
|
1297
1357
|
if (c.kind === "command") {
|
|
1298
1358
|
for (const seg of segments(c.raw)) {
|
|
@@ -1330,7 +1390,7 @@ var snapshotRewrite = {
|
|
|
1330
1390
|
};
|
|
1331
1391
|
|
|
1332
1392
|
// src/detectors/snapshot-only.ts
|
|
1333
|
-
var
|
|
1393
|
+
var RULE10 = "snapshot-only-rewrite";
|
|
1334
1394
|
var COMMIT_VIEWS = ["staged", "range"];
|
|
1335
1395
|
function isSnapshotChange(c, policy) {
|
|
1336
1396
|
if (c.kind !== "file") return false;
|
|
@@ -1339,7 +1399,7 @@ function isSnapshotChange(c, policy) {
|
|
|
1339
1399
|
return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
|
|
1340
1400
|
}
|
|
1341
1401
|
var snapshotOnlyRewrite = {
|
|
1342
|
-
id:
|
|
1402
|
+
id: RULE10,
|
|
1343
1403
|
surface: ["file"],
|
|
1344
1404
|
certainty: "mechanical",
|
|
1345
1405
|
run(changes, policy, view) {
|
|
@@ -1351,7 +1411,7 @@ var snapshotOnlyRewrite = {
|
|
|
1351
1411
|
const first = snaps[0];
|
|
1352
1412
|
const names = snaps.map((c) => c.kind === "file" ? c.path : "").filter(Boolean).slice(0, 3).join(", ");
|
|
1353
1413
|
return [
|
|
1354
|
-
makeFinding(
|
|
1414
|
+
makeFinding(RULE10, policy, {
|
|
1355
1415
|
file: first.path,
|
|
1356
1416
|
message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
|
|
1357
1417
|
evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
|
|
@@ -1372,6 +1432,7 @@ var allDetectors = [
|
|
|
1372
1432
|
ciTampering,
|
|
1373
1433
|
hookTampering,
|
|
1374
1434
|
testDeletion,
|
|
1435
|
+
testContentRemoval,
|
|
1375
1436
|
snapshotRewrite,
|
|
1376
1437
|
snapshotOnlyRewrite
|
|
1377
1438
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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",
|