tamperward 1.14.2 → 1.14.3

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
@@ -43,7 +43,7 @@ Earlier controlled experiments → **[the research series](./docs/blog/index.md)
43
43
 
44
44
  > **Scope.** The rows above are one model, one pressure prompt, one JS/TS
45
45
  > ecosystem — evidence for that setting, not a universal claim. Round 2 tested
46
- > the v1.9.0 stack; the current **v1.14.1** adds post-study envelope hardening
46
+ > the v1.9.0 stack; the current **v1.14.3** adds post-study envelope hardening
47
47
  > (externally reviewed, with findings tracked individually as REPRO or AUDIT in
48
48
  > [SECURITY-ENVELOPE.md](./SECURITY-ENVELOPE.md) and closed with regression and
49
49
  > mutation checks — see [CHANGELOG](./CHANGELOG.md)). Rounds 3 and 3.1 are
@@ -53,74 +53,119 @@ Earlier controlled experiments → **[the research series](./docs/blog/index.md)
53
53
  > and 3.1 all ran on a verifier carrying it. No evidence any agent used it; the
54
54
  > layer's authority in those rounds was nonetheless weaker than described at the
55
55
  > time. Round 4 is the fresh pool.
56
- > No round-3 outcomes are included in the claims above.
57
56
 
58
57
  ## Architecture
59
58
 
60
- The final envelope adjudication is anchored to state recorded before the agent
61
- starts; the in-loop hooks read the live tree and remain steering controls. The
62
- final adjudication boundary sits outside the agent's normal lifecycle.
59
+ Tamperward separates **steering** from **adjudication**.
63
60
 
61
+ > **Core invariant:** the agent may author the candidate tree, but it must not
62
+ > choose the trusted baseline, the governing policy, the verifier, or the final
63
+ > verdict.
64
+
65
+ | plane | mechanism | role |
66
+ | --- | --- | --- |
67
+ | **In-loop steering** | Claude Code `PreToolUse`, protected-tree drift checks, the `Stop` sweep, and optional filesystem-event observation | Immediate corrective feedback. Not the final authority. |
68
+ | **Local adjudication** | `tamperward run -- <agent…>` | Wraps the runtime and independently judges the tree it leaves behind. |
69
+ | **Commit backstop** | `tamperward check --staged` | Catches human changes, and agents without supported hooks. |
70
+ | **Repository authority** | Required PR checks plus branch protection | Decides what may reach the protected branch. |
71
+
72
+ | layer | the question it answers |
73
+ | --- | --- |
74
+ | **Change semantics** | Does a proposed or recorded change match a known weakening class? (PreToolUse deny, staged diff, range diff) |
75
+ | **Effect** | What did the protected state actually become, however the mutation arrived? (drift check, fs-event observer, Stop sweep) |
76
+ | **Outcome** | Does the candidate still pass when protected verification state from the base is restored? (pristine verification — no diff interpreted at all) |
77
+ | **Authority** | Where is the final verdict rendered, and what assumptions make it authoritative? (run envelope, CI, SHA-bound human sign-off) |
78
+
79
+ ### The local enforcement envelope
80
+
81
+ ```mermaid
82
+ flowchart TB
83
+ subgraph R["tamperward run — outer enforcement envelope"]
84
+ E["Capture entry state"]
85
+ subgraph U["Agent-controlled lifecycle"]
86
+ H["In-loop steering hooks"]
87
+ A["Agent runtime"]
88
+ W["Candidate HEAD and worktree"]
89
+ H -.-> A
90
+ A --> W
91
+ end
92
+ J["Post-exit adjudication"]
93
+ C["Committed and worktree checks"]
94
+ V["Visible and pristine verification"]
95
+ Q["Ancestry, dependency drift, quiescence"]
96
+ X["Final exit verdict"]
97
+ E --> A
98
+ E --> J
99
+ W --> J
100
+ J --> C
101
+ J --> V
102
+ J --> Q
103
+ C --> X
104
+ V --> X
105
+ Q --> X
106
+ end
64
107
  ```
65
- RECORDED AT ENVELOPE ENTRY:
66
- entry SHA · frozen policy · frozen verifier command/budget · dependency fingerprint
67
- │
68
- ┌─────────────────────────────┴───────────────────────────────┐
69
- │ agent loop (Claude Code hooks today) │
70
- │ PreToolUse gate: every proposed Edit/Write/Bash judged; │
71
- │ known weakening DENIED before the tool runs │
72
- │ │ │
73
- │ working tree ▼ │
74
- │ effect observation: per-call protected-tree drift check │
75
- │ + filesystem-event observer (`tamperward watch`) │
76
- │ │ │
77
- │ Stop sweep ▼ │
78
- │ end-of-turn net diff re-checked; transient protected │
79
- │ mutations judged from the event log │
80
- │ │ │
81
- │ pristine verification ▼ (`tamperward verify`) │
82
- │ suite runs twice in separate copies: the candidate tree │
83
- │ as-is, and with protected tests/snapshots/config │
84
- │ restored from the trusted base; visible-green + │
85
- │ pristine-red = MASKED FAILURE │
86
- └─────────────────────────────┬───────────────────────────────┘
87
- │ agent runtime exits — exit code NOT trusted
88
- ▼
89
- `tamperward run -- <agent…>` — the outer envelope
90
- policy + verifier resolved from the entry state, never the tree
91
- the agent wrote · post-agent HEAD must descend from the entry
92
- SHA · policy check over base...HEAD (committed weakening) and
93
- over the worktree incl. untracked files (uncommitted) ·
94
- dependency-tree fingerprint compared · quiescence guard ·
95
- pristine verify against the entry base · exit 1 on any blocking
96
- finding or masked failure · exit 2 (fail closed) when
97
- adjudication is impossible
98
- │
99
- ▼
100
- pre-commit backstop: `tamperward check --staged`
101
- │
102
- ▼
103
- PR CI: `check --diff base...head` + `verify --require-ancestor`
104
- policy read from the merge-base — a PR cannot govern its own verdict
105
- │
106
- ▼
107
- out-of-band human exception only: PR label
108
- `tamperward:allow:<rule>@<head-sha>` — SHA-bound, applied by a human
109
- with write access, never a file the branch can commit
110
- │
111
- ▼
112
- protected main
113
- ```
114
108
 
115
- CI is the final authority in this design, and that authority is conditional on
116
- the deployment assumptions documented in
117
- [docs/guide/enforcement.md](./docs/guide/enforcement.md) and
118
- [SPEC.md](./SPEC.md): verification is anchored to a base the agent cannot
119
- rewrite (a protected branch), the repository's branch protection actually
120
- enforces the workflow's verdict on merges, and sign-off labels can only be
121
- applied by humans with write access. Mechanisms, residuals, and what is
122
- explicitly outside the trust boundary: [SPEC.md](./SPEC.md) and
123
- [SECURITY-ENVELOPE.md](./SECURITY-ENVELOPE.md).
109
+ Before starting the agent, the envelope
110
+
111
+ 1. records the exact entry commit;
112
+ 2. loads the policy and the verifier command and budget from that commit;
113
+ 3. records the installed `node_modules` content fingerprint, where one exists; and
114
+ 4. refuses an already-dirty tree unless the operator explicitly accepts the risk.
115
+
116
+ The hooks then steer the agent while it works: they read live state, deny known
117
+ weakening operations, detect protected-tree drift, and sweep the turn's net
118
+ changes. The `Stop` hook runs that sweep — it does **not** invoke `verify`.
119
+ `tamperward watch` can additionally record transient mutations, but it is an
120
+ optional observer that `init` neither starts nor supervises, and it is not an
121
+ enforcement boundary.
122
+
123
+ After the runtime exits its exit code is treated as untrusted, and the envelope
124
+ checks that post-agent `HEAD` still descends from the entry commit; the committed
125
+ changes over `entry...HEAD`; staged, unstaged and untracked non-ignored worktree
126
+ changes; dependency drift and whether the tree stayed quiescent; and the
127
+ verification outcome.
128
+
129
+ `tamperward verify` materialises two temporary copies — they are **not
130
+ sandboxes**, and both execute agent-controlled code. The visible copy runs the
131
+ candidate as-is. In the pristine copy, files on the protected overlay classes and
132
+ on a separate enumerated **verification surface** (the configuration a test runner
133
+ auto-consults) are restored from the trusted base, and agent-added files on those
134
+ surfaces are removed. That surface list deliberately **bounds** runner-configuration
135
+ inheritance rather than eliminating it — see
136
+ [docs/THREAT-MODEL-pristine-run.md](./docs/THREAT-MODEL-pristine-run.md).
137
+
138
+ Visible green followed by pristine red is a `MASKED_FAILURE`. Two green runs
139
+ establish only that the candidate passes the base suite; they establish neither
140
+ semantic correctness nor a substitute for independent held-out tests.
141
+
142
+ | result | envelope behaviour |
143
+ | --- | --- |
144
+ | Enforcement clean and agent exits 0 | exit 0 |
145
+ | Blocking finding, masked failure, dependency drift, or non-quiescent tree | exit 1 |
146
+ | Required adjudication cannot be completed | exit 2, failing closed |
147
+ | Enforcement clean but the agent failed | the agent's non-zero exit is preserved |
148
+
149
+ ### CI authority
150
+
151
+ Pre-commit is an early backstop; protected CI is the merge authority. CI runs, independently:
152
+
153
+ 1. `check --diff base...head`, with policy from the trusted merge-base; and
154
+ 2. `verify --require-ancestor --base <base-sha>`, which since **1.14.3** also reads
155
+ its policy, verification surface and verifier command and budget from that same
156
+ trusted base — so neither job's verdict is governed by the candidate.
157
+
158
+ Legitimate exceptions are out-of-band PR labels bound to the exact head SHA; a new
159
+ push invalidates the approval.
160
+
161
+ This guarantee depends on a protected and immutable base, required status checks, a
162
+ pinned Tamperward version, and label permissions restricted to trusted humans.
163
+ Tamperward provides no OS isolation, no network confinement, no secret isolation,
164
+ and no semantic-correctness oracle.
165
+
166
+ Full assumptions and residual risks: [SPEC.md](./SPEC.md),
167
+ [SECURITY-ENVELOPE.md](./SECURITY-ENVELOPE.md), and the
168
+ [series limitations page](./docs/blog/limitations.md).
124
169
 
125
170
  ## Quick start
126
171
 
@@ -155,14 +200,7 @@ npx tamperward verify --base main # pristine-suite re-execution
155
200
  npx tamperward run -- <agent command...> # the outer envelope around an agent
156
201
  ```
157
202
 
158
- ## Four layers, different jobs
159
-
160
- | layer | the question it answers |
161
- | --- | --- |
162
- | **Change semantics** | Does a proposed or recorded change match a known weakening class? (PreToolUse deny, staged diff, range diff) |
163
- | **Effect** | What did the protected state actually become, however the mutation arrived? (drift check, fs-event observer, Stop sweep) |
164
- | **Outcome** | Does the candidate still pass when protected verification state from the base is restored? (pristine verification — no diff interpreted at all) |
165
- | **Authority** | Where is the final verdict rendered, and what assumptions make that verdict authoritative? (run envelope, CI, SHA-bound human sign-off) |
203
+ ### The rules
166
204
 
167
205
  Sixteen rules are specified and fourteen ship (see the table in
168
206
  [SPEC.md](./SPEC.md)). The families: test protection (`test-deletion`,
@@ -225,8 +263,10 @@ and its totals.
225
263
  | v1.9.0 | `tamperward verify`: the round-1 pristine oracle productized — visible-green + pristine-red is a masked failure, and "could not verify" fails closed |
226
264
  | Round 2, 9–0 | On 22 fresh repositories, v1.9.0 produced b=9/c=0 false-green discordance (p = 0.0039); four gated false greens remained, through two documented residual mechanisms |
227
265
  | cssstyle lifecycle escape (round 2) | The `tamperward run` envelope: the gate denied 42 mutations and verify refused all 25 stop attempts, and the runtime completed the session anyway over a masked tree — hooks decide, runtimes terminate, so the adjudication boundary moved outside the runtime |
266
+ | v1.14.3, external review | Standalone `verify` loaded its policy from the working tree, so the generated CI workflow let a pull request supply the `verify:` command for its own re-execution. `check --diff` flagged the edit as hook-tampering, so the workflow caught it as a pair — but only where both jobs are required, and `verify` alone had no protection. With a `--base`, policy now comes from that commit |
267
+ | v1.14.2, threat model | 1.14.1 removed agent-added files only inside the protected classes, whose `config` list is JS/TS-only; an added `pytest.ini`, `setup.cfg`, `tox.ini` or `pyproject.toml` still reached the pristine run. `verify` now owns a verification surface covering runner-consulted configuration |
228
268
  | v1.14.1, article audit | `tamperward verify` kept agent-added protected files in the pristine run on the premise that they "only add strictness". An added `conftest.py` could deselect the restored base tests by node id, so a masked failure reported VERIFIED and the envelope printed GREEN MEANS GREEN over an unfixed bug. Added protected files are now removed; PoC and mutation-checked regression committed |
229
- | v1.10.1–v1.14.0, owner + two-pass external review | Frozen entry-time policy and verifier, entry-SHA ancestry enforcement, quiescence guard, dependency-tree fingerprint, a CI verify step in the generated workflow, the gate pinned to its own version in CI, and SHA-bound sign-off labels |
269
+ | v1.10.1–v1.14.0, owner + two-pass external review | Frozen entry-time policy and verifier, entry-SHA ancestry enforcement, quiescence guard, `node_modules` content fingerprint, a CI verify step in the generated workflow, the gate pinned to its own version in CI, and SHA-bound sign-off labels |
230
270
 
231
271
  Each row's primary artifact: [CHANGELOG.md](./CHANGELOG.md), [SPEC.md](./SPEC.md),
232
272
  and the posts in [docs/blog/](./docs/blog/index.md).
@@ -247,7 +287,7 @@ via npm trusted publishing with SLSA provenance. Full rule:
247
287
 
248
288
  ```bash
249
289
  npm install && npm run build # bundles the CLI to dist/cli/index.js
250
- npm test # 365 tests at v1.14.1 — parser, detectors, engine, policy, renderers
290
+ npm test # 367 tests at v1.14.3 — parser, detectors, engine, policy, renderers
251
291
  npm run typecheck
252
292
  ```
253
293
 
package/dist/cli/index.js CHANGED
@@ -3014,7 +3014,9 @@ function runVerify(opts) {
3014
3014
  const out2 = (s) => void process.stdout.write(s + "\n");
3015
3015
  let policy;
3016
3016
  try {
3017
- policy = opts.policyOverride ?? loadPolicy(cwd);
3017
+ if (opts.policyOverride) policy = opts.policyOverride;
3018
+ else if (opts.base) policy = loadPolicyAt(resolveBase(opts.base, cwd), cwd) ?? defaultPolicy();
3019
+ else policy = loadPolicy(cwd);
3018
3020
  } catch (e) {
3019
3021
  out2(`verify: cannot load policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
3020
3022
  return 2;
@@ -3030,7 +3032,9 @@ function runVerify(opts) {
3030
3032
  }
3031
3033
  const cmd = opts.cmd ?? policy.verify?.command;
3032
3034
  if (!cmd) {
3033
- out2("verify: no suite command \u2014 set policy `verify: { command: ... }` or pass --cmd");
3035
+ out2(
3036
+ opts.base ? "verify: no suite command in the policy at the trusted base \u2014 the base governs the verifier, so a `verify:` block added only on the candidate is not used. Add it at the base, or pass --cmd explicitly." : "verify: no suite command \u2014 set policy `verify: { command: ... }` or pass --cmd"
3037
+ );
3034
3038
  return 2;
3035
3039
  }
3036
3040
  const budget = opts.budget ?? policy.verify?.budget ?? 300;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "1.14.2",
3
+ "version": "1.14.3",
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",