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 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 **v2.0.0** adds post-study envelope hardening
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 # 520+ tests at 2.0.0 — parser, detectors, engine, policy, renderers
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 m = rest.match(/^(.*) (b\/.*)$/);
63
- if (!m) return [null, null];
64
- first = m[1];
65
- rest = m[2];
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
- maxBuffer: 64 * 1024 * 1024,
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" || c.binary) return c;
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(["diff", "--no-color", "-M", rev], opts.cwd);
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(["diff", "--no-color", "-M", `${base}...${head}`], opts.cwd);
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(["diff", "--no-color", "-M", "--cached"], opts.cwd);
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(["diff", "--no-color", "-M", "HEAD"], opts.cwd);
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 write access \u2014
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
- from the trusted base; a visible-green /
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
- left (policy over base...HEAD and the
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] enforcement point: Claude Code hooks,
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 code: check \u2192 1 if any blocking finding. hook/sweep \u2192 always 0; a deny is
4130
- emitted as JSON on stdout (exit 2 makes Claude Code ignore the JSON).
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.0",
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",