tamperward 2.1.0 → 2.1.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 +6 -4
- package/dist/cli/index.js +43 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,8 @@ impossible.
|
|
|
37
37
|
> and hardened. Use it as one layer of defence in depth alongside protected CI,
|
|
38
38
|
> independent tests and human review. Findings, limitations and corrections are
|
|
39
39
|
> published openly. The 2.0 major marks the Node 18 drop, not a declaration of
|
|
40
|
-
> security maturity
|
|
40
|
+
> security maturity; the distance to that is tracked, milestone by milestone, in
|
|
41
|
+
> [SPEC §9.1](./SPEC.md#91-maturity-milestones).
|
|
41
42
|
|
|
42
43
|
## What we have actually measured
|
|
43
44
|
|
|
@@ -63,7 +64,7 @@ Earlier controlled experiments → **[the research series](./docs/blog/index.md)
|
|
|
63
64
|
> **Scope.** The rows above cover specific models, pressure prompts, treatment
|
|
64
65
|
> versions, and finite JavaScript/TypeScript and Python repository samples —
|
|
65
66
|
> evidence for those settings, not a universal claim. Round 2 tested
|
|
66
|
-
> the v1.9.0 stack; the current **
|
|
67
|
+
> the v1.9.0 stack; the current **2.x** line adds post-study envelope hardening
|
|
67
68
|
> (externally reviewed, with findings tracked individually as REPRO or AUDIT in
|
|
68
69
|
> [SECURITY-ENVELOPE.md](./SECURITY-ENVELOPE.md) and closed with regression and
|
|
69
70
|
> mutation checks — see [CHANGELOG](./CHANGELOG.md)). Rounds 3 and 3.1 are
|
|
@@ -352,7 +353,8 @@ and its totals.
|
|
|
352
353
|
|
|
353
354
|
**[The research series](./docs/blog/index.md)** ·
|
|
354
355
|
**[The harness](./harness/)** ·
|
|
355
|
-
**[Errata](./docs/blog/errata.md)**
|
|
356
|
+
**[Errata](./docs/blog/errata.md)** ·
|
|
357
|
+
**[Maturity milestones](./SPEC.md#91-maturity-milestones)**
|
|
356
358
|
|
|
357
359
|
## The architecture was earned by failures
|
|
358
360
|
|
|
@@ -393,7 +395,7 @@ via npm trusted publishing with SLSA provenance. Full rule:
|
|
|
393
395
|
|
|
394
396
|
```bash
|
|
395
397
|
npm install && npm run build # bundles the CLI to dist/cli/index.js
|
|
396
|
-
npm test #
|
|
398
|
+
npm test # 540+ tests — parser, detectors, engine, policy, renderers
|
|
397
399
|
npm run typecheck
|
|
398
400
|
```
|
|
399
401
|
|
package/dist/cli/index.js
CHANGED
|
@@ -33,6 +33,11 @@ function unquotePath(p) {
|
|
|
33
33
|
}
|
|
34
34
|
return Buffer.from(bytes).toString("utf8");
|
|
35
35
|
}
|
|
36
|
+
function pathToken(raw) {
|
|
37
|
+
if (raw.startsWith('"')) return raw;
|
|
38
|
+
const tab = raw.indexOf(" ");
|
|
39
|
+
return tab >= 0 ? raw.slice(0, tab) : raw;
|
|
40
|
+
}
|
|
36
41
|
function endOfQuoted(s) {
|
|
37
42
|
for (let i = 1; i < s.length; i++) {
|
|
38
43
|
if (s[i] === "\\") {
|
|
@@ -59,10 +64,17 @@ function headerPaths(line) {
|
|
|
59
64
|
first = rest.slice(0, q);
|
|
60
65
|
rest = rest.slice(q + 1);
|
|
61
66
|
} else {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const mid = (rest.length - 1) / 2;
|
|
68
|
+
const sym = Number.isInteger(mid) && rest[mid] === " " && rest.startsWith("a/") && rest.slice(mid + 1).startsWith("b/") && rest.slice(2, mid) === rest.slice(mid + 3);
|
|
69
|
+
if (sym) {
|
|
70
|
+
first = rest.slice(0, mid);
|
|
71
|
+
rest = rest.slice(mid + 1);
|
|
72
|
+
} else {
|
|
73
|
+
const m = rest.match(/^(.*) (b\/.*)$/);
|
|
74
|
+
if (!m) return [null, null];
|
|
75
|
+
first = m[1];
|
|
76
|
+
rest = m[2];
|
|
77
|
+
}
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
return [stripAB(first), stripAB(rest)];
|
|
@@ -126,8 +138,8 @@ function parseDiff(diff) {
|
|
|
126
138
|
renameTo = unquotePath(l.slice(8));
|
|
127
139
|
if (op === "modify") op = "rename";
|
|
128
140
|
} else if (l.startsWith("Binary files ") || l.startsWith("GIT binary patch")) binary = true;
|
|
129
|
-
else if (l.startsWith("--- ")) minusPath = l.slice(4);
|
|
130
|
-
else if (l.startsWith("+++ ")) plusPath = l.slice(4);
|
|
141
|
+
else if (l.startsWith("--- ")) minusPath = pathToken(l.slice(4));
|
|
142
|
+
else if (l.startsWith("+++ ")) plusPath = pathToken(l.slice(4));
|
|
131
143
|
i++;
|
|
132
144
|
}
|
|
133
145
|
let path;
|
|
@@ -196,7 +208,12 @@ function git(args, cwd) {
|
|
|
196
208
|
return execFileSync("git", args, {
|
|
197
209
|
cwd,
|
|
198
210
|
encoding: "utf8",
|
|
199
|
-
|
|
211
|
+
// 256 MiB, matching the hook adapter. Every diff below runs with `--text`, so a
|
|
212
|
+
// changed binary asset now costs its full size in patch output instead of one
|
|
213
|
+
// "Binary files differ" line; a legitimate golden-image update must fit. Past
|
|
214
|
+
// the cap git() throws and the view fails CLOSED (a truncated patch would parse
|
|
215
|
+
// as a smaller change than the one landing), which is the documented stance.
|
|
216
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
200
217
|
stdio: ["ignore", "pipe", "pipe"]
|
|
201
218
|
});
|
|
202
219
|
} catch (e) {
|
|
@@ -220,7 +237,7 @@ function fromDisk(path, cwd) {
|
|
|
220
237
|
}
|
|
221
238
|
}
|
|
222
239
|
function enrich(c, beforeReader, afterReader) {
|
|
223
|
-
if (c.kind !== "file"
|
|
240
|
+
if (c.kind !== "file") return c;
|
|
224
241
|
const before = c.op !== "add" ? beforeReader(c.oldPath ?? c.path) : null;
|
|
225
242
|
const after = c.op !== "delete" ? afterReader(c.path) : null;
|
|
226
243
|
return { ...c, before, after };
|
|
@@ -261,8 +278,9 @@ function headSha(cwd) {
|
|
|
261
278
|
return null;
|
|
262
279
|
}
|
|
263
280
|
}
|
|
281
|
+
var DIFF_ARGS = ["diff", "--no-color", "--text", "-M"];
|
|
264
282
|
function diffSince(rev, opts = {}) {
|
|
265
|
-
const raw = git([
|
|
283
|
+
const raw = git([...DIFF_ARGS, rev], opts.cwd);
|
|
266
284
|
return parseDiff(raw).map(
|
|
267
285
|
(c) => enrich(c, (p) => blobAt(rev, p, opts.cwd), (p) => fromDisk(p, opts.cwd))
|
|
268
286
|
);
|
|
@@ -270,20 +288,20 @@ function diffSince(rev, opts = {}) {
|
|
|
270
288
|
function diffRange(base, head, opts = {}) {
|
|
271
289
|
assertRev(base);
|
|
272
290
|
assertRev(head);
|
|
273
|
-
const raw = git([
|
|
291
|
+
const raw = git([...DIFF_ARGS, `${base}...${head}`], opts.cwd);
|
|
274
292
|
const mergeBase = mergeBaseOf(base, head, opts);
|
|
275
293
|
return parseDiff(raw).map(
|
|
276
294
|
(c) => enrich(c, (p) => blobAt(mergeBase, p, opts.cwd), (p) => blobAt(head, p, opts.cwd))
|
|
277
295
|
);
|
|
278
296
|
}
|
|
279
297
|
function diffStaged(opts = {}) {
|
|
280
|
-
const raw = git([
|
|
298
|
+
const raw = git([...DIFF_ARGS, "--cached"], opts.cwd);
|
|
281
299
|
return parseDiff(raw).map(
|
|
282
300
|
(c) => enrich(c, (p) => blobAt("HEAD", p, opts.cwd), (p) => blobAt("", p, opts.cwd))
|
|
283
301
|
);
|
|
284
302
|
}
|
|
285
303
|
function diffWorktree(opts = {}) {
|
|
286
|
-
const raw = git([
|
|
304
|
+
const raw = git([...DIFF_ARGS, "HEAD"], opts.cwd);
|
|
287
305
|
return parseDiff(raw).map(
|
|
288
306
|
(c) => enrich(c, (p) => blobAt("HEAD", p, opts.cwd), (p) => fromDisk(p, opts.cwd))
|
|
289
307
|
);
|
|
@@ -2302,7 +2320,7 @@ function synthFileChange(displayPath, before, after) {
|
|
|
2302
2320
|
writeFileSync(a, before ?? "");
|
|
2303
2321
|
writeFileSync(b, after ?? "");
|
|
2304
2322
|
try {
|
|
2305
|
-
raw = execFileSync2("git", ["diff", "--no-index", "--no-color", a, b], {
|
|
2323
|
+
raw = execFileSync2("git", ["diff", "--no-index", "--no-color", "--text", a, b], {
|
|
2306
2324
|
encoding: "utf8",
|
|
2307
2325
|
maxBuffer: 256 * 1024 * 1024
|
|
2308
2326
|
});
|
|
@@ -3033,7 +3051,7 @@ var WORKFLOW_CONTENT = `name: tamperward
|
|
|
3033
3051
|
|
|
3034
3052
|
# The CI authority for main: the same engine as the agent hook and pre-commit, run over
|
|
3035
3053
|
# the PR's commit range. A block fails the check and clears ONLY via the out-of-band
|
|
3036
|
-
# label \`tamperward:allow:<rule>@<head-sha>\` applied by someone with
|
|
3054
|
+
# label \`tamperward:allow:<rule>@<head-sha>\` applied by someone with triage access or higher \u2014
|
|
3037
3055
|
# never a file the PR itself can commit. The verify step reads the same labels:
|
|
3038
3056
|
# \`tamperward:allow:verify@<head-sha>\` accepts a masked failure a reviewer has judged.
|
|
3039
3057
|
#
|
|
@@ -4101,7 +4119,7 @@ Formats:
|
|
|
4101
4119
|
observe supported transient effects
|
|
4102
4120
|
tamperward verify [--base R] [--cmd C] pristine-suite re-execution: run the suite
|
|
4103
4121
|
[--budget S] [--json] [--keep] as-is AND with protected files restored
|
|
4104
|
-
|
|
4122
|
+
[--require-ancestor] [--cwd D] from the trusted base; a visible-green /
|
|
4105
4123
|
pristine-red result is a MASKED FAILURE
|
|
4106
4124
|
(exit 1, or 0 under an out-of-band
|
|
4107
4125
|
verify@<head-sha> approval); cannot-verify
|
|
@@ -4109,14 +4127,16 @@ Formats:
|
|
|
4109
4127
|
tamperward run [opts] -- <agent cmd...> enforcement envelope: record the trusted
|
|
4110
4128
|
[--base R] [--cmd C] base, run the agent, treat its exit as
|
|
4111
4129
|
[--budget S] [--allow-dirty] untrusted, then re-adjudicate the tree it
|
|
4112
|
-
|
|
4130
|
+
[--settle S] [--allow-dep-drift] left (policy over base...HEAD and the
|
|
4131
|
+
[--cwd D]
|
|
4113
4132
|
worktree, plus verify). Exit: agent's code
|
|
4114
4133
|
when clean; 1 on any blocking finding or
|
|
4115
4134
|
masked failure; 2 when it cannot
|
|
4116
4135
|
adjudicate (fails closed)
|
|
4117
4136
|
tamperward allow <rule> --reason "..." record a human sign-off (local audit ledger)
|
|
4137
|
+
[--file F] [--cwd D]
|
|
4118
4138
|
tamperward init [--dry-run] wire the policy file plus every
|
|
4119
|
-
[--force-workflow]
|
|
4139
|
+
[--force-workflow] [--cwd D] enforcement point: Claude Code hooks,
|
|
4120
4140
|
pre-commit, CI. Idempotent; never
|
|
4121
4141
|
overwrites your files. Re-running it
|
|
4122
4142
|
MIGRATES a CI workflow it generated and
|
|
@@ -4126,8 +4146,12 @@ Formats:
|
|
|
4126
4146
|
--force-workflow replaces a workflow it
|
|
4127
4147
|
did not write, or one you have edited.
|
|
4128
4148
|
|
|
4129
|
-
Exit
|
|
4130
|
-
|
|
4149
|
+
Exit codes: 0 clean \xB7 1 a blocking finding (check), MASKED_FAILURE or SUITE_RED
|
|
4150
|
+
(verify), any blocking finding or masked failure (run) \xB7 2 cannot
|
|
4151
|
+
evaluate, failing closed: policy error, bad --diff range, no view,
|
|
4152
|
+
unresolvable base, --require-ancestor refused, budget exceeded.
|
|
4153
|
+
hook/sweep \u2192 0 with a deny emitted as JSON on stdout (exit 2 would make
|
|
4154
|
+
Claude Code ignore the JSON); 2 only for an unsupported agent name.
|
|
4131
4155
|
`);
|
|
4132
4156
|
}
|
|
4133
4157
|
function main(argv2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
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",
|