tamperward 2.39.24 → 2.40.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/dist/cli/index.js CHANGED
@@ -12546,6 +12546,16 @@ function sessionHash(sessionId) {
12546
12546
  if (!sessionId) return void 0;
12547
12547
  return "sha256:" + createHash6("sha256").update(sessionId).digest("hex").slice(0, 24);
12548
12548
  }
12549
+ function sanitizedMetrics(metrics) {
12550
+ const out3 = {};
12551
+ if (!metrics) return out3;
12552
+ for (const field2 of AUDIT_DURATION_FIELDS) {
12553
+ const v = metrics[field2];
12554
+ if (typeof v === "number" && Number.isFinite(v)) out3[field2] = Math.max(0, Math.round(v));
12555
+ }
12556
+ if (typeof metrics.oob_signoff === "boolean") out3.oob_signoff = metrics.oob_signoff;
12557
+ return out3;
12558
+ }
12549
12559
  function eventId() {
12550
12560
  return "sha256:" + createHash6("sha256").update(randomBytes2(32)).digest("hex").slice(0, 32);
12551
12561
  }
@@ -12570,6 +12580,7 @@ function recordAuditFindings(findings, context) {
12570
12580
  mkdirSync5(dirname4(path), { recursive: true });
12571
12581
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
12572
12582
  const session = sessionHash(context.sessionId);
12583
+ const metrics = sanitizedMetrics(context.metrics);
12573
12584
  const lines = findings.map((finding) => {
12574
12585
  const severity = finding.severity === "block" ? "block" : "warn";
12575
12586
  const event = {
@@ -12581,7 +12592,8 @@ function recordAuditFindings(findings, context) {
12581
12592
  rule: finding.rule,
12582
12593
  severity,
12583
12594
  decision: severity === "block" ? "deny" : "warn",
12584
- ...session ? { session } : {}
12595
+ ...session ? { session } : {},
12596
+ ...metrics
12585
12597
  };
12586
12598
  return JSON.stringify(event);
12587
12599
  });
@@ -12638,6 +12650,15 @@ function parseAuditEvent(value, line = 0) {
12638
12650
  if (value.session !== void 0 && (typeof value.session !== "string" || !/^sha256:[0-9a-f]{24}$/.test(value.session))) {
12639
12651
  throw new Error(`${where} has an invalid session`);
12640
12652
  }
12653
+ for (const field2 of AUDIT_DURATION_FIELDS) {
12654
+ const v = value[field2];
12655
+ if (v !== void 0 && (typeof v !== "number" || !Number.isInteger(v) || v < 0)) {
12656
+ throw new Error(`${where} has an invalid ${field2}`);
12657
+ }
12658
+ }
12659
+ if (value.oob_signoff !== void 0 && typeof value.oob_signoff !== "boolean") {
12660
+ throw new Error(`${where} has an invalid oob_signoff`);
12661
+ }
12641
12662
  return {
12642
12663
  schema_version: AUDIT_SCHEMA_VERSION,
12643
12664
  id: value.id,
@@ -12647,9 +12668,19 @@ function parseAuditEvent(value, line = 0) {
12647
12668
  rule: value.rule,
12648
12669
  severity: value.severity,
12649
12670
  decision: value.decision,
12650
- ...typeof value.session === "string" ? { session: value.session } : {}
12671
+ ...typeof value.session === "string" ? { session: value.session } : {},
12672
+ ...durationsOf(value),
12673
+ ...typeof value.oob_signoff === "boolean" ? { oob_signoff: value.oob_signoff } : {}
12651
12674
  };
12652
12675
  }
12676
+ function durationsOf(value) {
12677
+ const out3 = {};
12678
+ for (const field2 of AUDIT_DURATION_FIELDS) {
12679
+ const v = value[field2];
12680
+ if (typeof v === "number") out3[field2] = v;
12681
+ }
12682
+ return out3;
12683
+ }
12653
12684
  function parseAuditLine(line, lineNumber) {
12654
12685
  let value;
12655
12686
  try {
@@ -12724,6 +12755,12 @@ function createAuditAggregator() {
12724
12755
  const rules = /* @__PURE__ */ new Map();
12725
12756
  const surfaces = /* @__PURE__ */ new Map();
12726
12757
  const sessions = /* @__PURE__ */ new Set();
12758
+ const durations = {
12759
+ hook_latency_ms: /* @__PURE__ */ new Map(),
12760
+ verify_wall_clock_ms: /* @__PURE__ */ new Map(),
12761
+ bare_suite_wall_clock_ms: /* @__PURE__ */ new Map()
12762
+ };
12763
+ let oobSignoffs = 0;
12727
12764
  const bounds = { first: null, last: null };
12728
12765
  return {
12729
12766
  add(event) {
@@ -12737,6 +12774,11 @@ function createAuditAggregator() {
12737
12774
  rules.set(event.rule, bucket);
12738
12775
  surfaces.set(event.surface, (surfaces.get(event.surface) ?? 0) + 1);
12739
12776
  if (event.session) sessions.add(event.session);
12777
+ for (const field2 of AUDIT_DURATION_FIELDS) {
12778
+ const v = event[field2];
12779
+ if (typeof v === "number") durations[field2].set(v, (durations[field2].get(v) ?? 0) + 1);
12780
+ }
12781
+ if (event.oob_signoff === true) oobSignoffs++;
12740
12782
  const ms = Date.parse(event.timestamp);
12741
12783
  const { first, last } = bounds;
12742
12784
  if (first === null || ms < first.ms || ms === first.ms && event.id.localeCompare(first.id) < 0) {
@@ -12759,11 +12801,32 @@ function createAuditAggregator() {
12759
12801
  last_event: bounds.last === null ? null : bounds.last.timestamp,
12760
12802
  by_rule,
12761
12803
  by_surface,
12804
+ latency: {
12805
+ hook_latency_ms: percentiles(durations.hook_latency_ms),
12806
+ verify_wall_clock_ms: percentiles(durations.verify_wall_clock_ms),
12807
+ bare_suite_wall_clock_ms: percentiles(durations.bare_suite_wall_clock_ms)
12808
+ },
12809
+ oob_signoffs: oobSignoffs,
12762
12810
  interpretation: "finding-is-not-proof-of-intent"
12763
12811
  };
12764
12812
  }
12765
12813
  };
12766
12814
  }
12815
+ function percentiles(counts) {
12816
+ const values = [...counts.keys()].sort((a, b) => a - b);
12817
+ if (values.length === 0) return null;
12818
+ const samples = [...counts.values()].reduce((a, b) => a + b, 0);
12819
+ const at = (q) => {
12820
+ const rank = Math.max(1, Math.ceil(q * samples));
12821
+ let seen = 0;
12822
+ for (const v of values) {
12823
+ seen += counts.get(v) ?? 0;
12824
+ if (seen >= rank) return v;
12825
+ }
12826
+ return values[values.length - 1];
12827
+ };
12828
+ return { samples, p50: at(0.5), p95: at(0.95), max: values[values.length - 1] };
12829
+ }
12767
12830
  function renderAuditStats(summary) {
12768
12831
  const out3 = [
12769
12832
  "TamperWard audit stats",
@@ -12786,6 +12849,23 @@ function renderAuditStats(summary) {
12786
12849
  const width = Math.max(...summary.by_surface.map((row) => row.surface.length));
12787
12850
  for (const row of summary.by_surface) out3.push(` ${row.surface.padEnd(width)} ${row.events}`);
12788
12851
  }
12852
+ const measures = [
12853
+ ["hook_latency_ms", "Hook latency"],
12854
+ ["verify_wall_clock_ms", "verify wall-clock"],
12855
+ ["bare_suite_wall_clock_ms", "Bare-suite wall-clock"]
12856
+ ];
12857
+ const measured = [];
12858
+ for (const [field2, label] of measures) {
12859
+ const p = summary.latency[field2];
12860
+ if (p !== null) measured.push([label, p]);
12861
+ }
12862
+ if (measured.length || summary.oob_signoffs > 0) {
12863
+ out3.push("", "Measures");
12864
+ for (const [label, p] of measured) {
12865
+ out3.push(` ${label.padEnd(22)} p50 ${p.p50} ms p95 ${p.p95} ms max ${p.max} ms (${p.samples} samples)`);
12866
+ }
12867
+ if (summary.oob_signoffs > 0) out3.push(` ${"Out-of-band sign-offs".padEnd(22)} ${summary.oob_signoffs}`);
12868
+ }
12789
12869
  out3.push("", "Note: an integrity finding is a signal, not proof of agent intent; legitimate refactors can trigger findings.");
12790
12870
  return out3.join("\n") + "\n";
12791
12871
  }
@@ -12807,7 +12887,7 @@ function runStats(opts = {}) {
12807
12887
  process.stdout.write(opts.json ? JSON.stringify(summary) + "\n" : renderAuditStats(summary));
12808
12888
  return 0;
12809
12889
  }
12810
- var AUDIT_SCHEMA_VERSION, AUDIT_LOG_ENV, MAX_AUDIT_LINE_BYTES, ALLOWED_KEYS, STATS_READ_REFUSED;
12890
+ var AUDIT_SCHEMA_VERSION, AUDIT_LOG_ENV, MAX_AUDIT_LINE_BYTES, AUDIT_DURATION_FIELDS, ALLOWED_KEYS, STATS_READ_REFUSED;
12811
12891
  var init_audit = __esm({
12812
12892
  "src/cli/audit.ts"() {
12813
12893
  "use strict";
@@ -12816,6 +12896,7 @@ var init_audit = __esm({
12816
12896
  AUDIT_SCHEMA_VERSION = 1;
12817
12897
  AUDIT_LOG_ENV = "TAMPERWARD_AUDIT_LOG";
12818
12898
  MAX_AUDIT_LINE_BYTES = 16 * 1024;
12899
+ AUDIT_DURATION_FIELDS = ["hook_latency_ms", "verify_wall_clock_ms", "bare_suite_wall_clock_ms"];
12819
12900
  ALLOWED_KEYS = /* @__PURE__ */ new Set([
12820
12901
  "schema_version",
12821
12902
  "id",
@@ -12825,7 +12906,9 @@ var init_audit = __esm({
12825
12906
  "rule",
12826
12907
  "severity",
12827
12908
  "decision",
12828
- "session"
12909
+ "session",
12910
+ ...AUDIT_DURATION_FIELDS,
12911
+ "oob_signoff"
12829
12912
  ]);
12830
12913
  STATS_READ_REFUSED = "; stats reads the audit log only as a regular file \u2014 remove what stands there so the log can be read";
12831
12914
  }
@@ -12903,7 +12986,8 @@ function verdict(blocks2, kind, audit) {
12903
12986
  recordAuditFindings(blocks2, {
12904
12987
  cwd: audit.cwd,
12905
12988
  surface: kind === "PreToolUse" ? "pretooluse" : "stop",
12906
- sessionId: audit.sessionId
12989
+ sessionId: audit.sessionId,
12990
+ ...audit.startedAt !== void 0 ? { metrics: { hook_latency_ms: performance.now() - audit.startedAt } } : {}
12907
12991
  });
12908
12992
  }
12909
12993
  const reason = formatDenial(blocks2);
@@ -13086,6 +13170,7 @@ function sanctionPredictedWrites(cwd, sessionId, policy, changes) {
13086
13170
  if (dirty) savePtree(cwd, sessionId, tree);
13087
13171
  }
13088
13172
  function preToolUseVerdict(input, defaultCwd, trustedRoot) {
13173
+ const startedAt = performance.now();
13089
13174
  const sessionCwd = input.cwd ?? defaultCwd ?? process.cwd();
13090
13175
  try {
13091
13176
  if (trustedRoot !== void 0) {
@@ -13096,11 +13181,11 @@ function preToolUseVerdict(input, defaultCwd, trustedRoot) {
13096
13181
  turnBaseline(cwd, input.session_id);
13097
13182
  const policy = loadPolicy(cwd);
13098
13183
  const driftBlocks = effectDriftBlocks(cwd, input.session_id, policy);
13099
- if (driftBlocks) return verdict(driftBlocks, "PreToolUse", { cwd, sessionId: input.session_id });
13184
+ if (driftBlocks) return verdict(driftBlocks, "PreToolUse", { cwd, sessionId: input.session_id, startedAt });
13100
13185
  const changes = changesFromClaudeHook(input, cwd, sessionCwd);
13101
13186
  const blocks2 = evaluate(changes, policy, void 0, "tool-call", { cwd }).filter((f) => f.severity === "block");
13102
13187
  if (blocks2.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
13103
- return verdict(blocks2, "PreToolUse", { cwd, sessionId: input.session_id });
13188
+ return verdict(blocks2, "PreToolUse", { cwd, sessionId: input.session_id, startedAt });
13104
13189
  } catch (e) {
13105
13190
  return failClosed("PreToolUse", errText2(e), { cwd: sessionCwd, sessionId: input.session_id });
13106
13191
  }
@@ -13206,6 +13291,7 @@ function recordObserverHealth(cwd, sessionId, state, reason) {
13206
13291
  }
13207
13292
  function stopVerdict(input, defaultCwd, trustedRoot) {
13208
13293
  if (input.stop_hook_active) return { exitCode: 0, stdout: "" };
13294
+ const startedAt = performance.now();
13209
13295
  const sessionCwd = input.cwd ?? defaultCwd ?? process.cwd();
13210
13296
  if (trustedRoot !== void 0) {
13211
13297
  const v = validateClaimAgainstRoot(input.cwd, trustedRoot, defaultCwd ?? process.cwd());
@@ -13247,7 +13333,7 @@ function stopVerdict(input, defaultCwd, trustedRoot) {
13247
13333
  advanceTurnBaseline(cwd, input.session_id);
13248
13334
  commitCursor();
13249
13335
  }
13250
- return verdict(blocks2, "Stop", { cwd, sessionId: input.session_id });
13336
+ return verdict(blocks2, "Stop", { cwd, sessionId: input.session_id, startedAt });
13251
13337
  }
13252
13338
  function emit(r) {
13253
13339
  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.39.24",
3
+ "version": "2.40.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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://raw.githubusercontent.com/hexrift/tamperward/v2.38.0/schemas/audit-v1.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/hexrift/tamperward/v2.40.0/schemas/audit-v1.schema.json",
4
4
  "title": "TamperWard audit event v1",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
@@ -55,6 +55,25 @@
55
55
  "session": {
56
56
  "type": "string",
57
57
  "pattern": "^sha256:[0-9a-f]{24}$"
58
+ },
59
+ "hook_latency_ms": {
60
+ "type": "integer",
61
+ "minimum": 0,
62
+ "description": "Wall-clock milliseconds from the hook reading its input to the deny being written. Present only on events the hook itself records."
63
+ },
64
+ "verify_wall_clock_ms": {
65
+ "type": "integer",
66
+ "minimum": 0,
67
+ "description": "Wall-clock milliseconds of a `tamperward verify` run, where a producer records it."
68
+ },
69
+ "bare_suite_wall_clock_ms": {
70
+ "type": "integer",
71
+ "minimum": 0,
72
+ "description": "Wall-clock milliseconds of the same suite run without TamperWard, the comparison for verify_wall_clock_ms."
73
+ },
74
+ "oob_signoff": {
75
+ "type": "boolean",
76
+ "description": "True when an out-of-band sign-off (a CI label or local allow) was needed to clear this finding."
58
77
  }
59
78
  },
60
79
  "allOf": [
@@ -108,6 +127,19 @@
108
127
  "severity": "block",
109
128
  "decision": "deny",
110
129
  "session": "sha256:bbbbbbbbbbbbbbbbbbbbbbbb"
130
+ },
131
+ {
132
+ "schema_version": 1,
133
+ "id": "sha256:cccccccccccccccccccccccccccccccc",
134
+ "timestamp": "2026-09-23T14:00:00.000Z",
135
+ "surface": "pretooluse",
136
+ "agent": "claude-code",
137
+ "rule": "test-deletion",
138
+ "severity": "block",
139
+ "decision": "deny",
140
+ "session": "sha256:bbbbbbbbbbbbbbbbbbbbbbbb",
141
+ "hook_latency_ms": 42,
142
+ "oob_signoff": false
111
143
  }
112
144
  ]
113
145
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://raw.githubusercontent.com/hexrift/tamperward/v2.38.0/schemas/stats-v1.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/hexrift/tamperward/v2.40.0/schemas/stats-v1.schema.json",
4
4
  "title": "TamperWard audit stats v1",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
@@ -14,6 +14,8 @@
14
14
  "last_event",
15
15
  "by_rule",
16
16
  "by_surface",
17
+ "latency",
18
+ "oob_signoffs",
17
19
  "interpretation"
18
20
  ],
19
21
  "properties": {
@@ -112,6 +114,128 @@
112
114
  }
113
115
  }
114
116
  },
117
+ "latency": {
118
+ "type": "object",
119
+ "additionalProperties": false,
120
+ "required": [
121
+ "hook_latency_ms",
122
+ "verify_wall_clock_ms",
123
+ "bare_suite_wall_clock_ms"
124
+ ],
125
+ "properties": {
126
+ "hook_latency_ms": {
127
+ "anyOf": [
128
+ {
129
+ "type": "null"
130
+ },
131
+ {
132
+ "type": "object",
133
+ "additionalProperties": false,
134
+ "required": [
135
+ "samples",
136
+ "p50",
137
+ "p95",
138
+ "max"
139
+ ],
140
+ "properties": {
141
+ "samples": {
142
+ "type": "integer",
143
+ "minimum": 1
144
+ },
145
+ "p50": {
146
+ "type": "integer",
147
+ "minimum": 0
148
+ },
149
+ "p95": {
150
+ "type": "integer",
151
+ "minimum": 0
152
+ },
153
+ "max": {
154
+ "type": "integer",
155
+ "minimum": 0
156
+ }
157
+ }
158
+ }
159
+ ]
160
+ },
161
+ "verify_wall_clock_ms": {
162
+ "anyOf": [
163
+ {
164
+ "type": "null"
165
+ },
166
+ {
167
+ "type": "object",
168
+ "additionalProperties": false,
169
+ "required": [
170
+ "samples",
171
+ "p50",
172
+ "p95",
173
+ "max"
174
+ ],
175
+ "properties": {
176
+ "samples": {
177
+ "type": "integer",
178
+ "minimum": 1
179
+ },
180
+ "p50": {
181
+ "type": "integer",
182
+ "minimum": 0
183
+ },
184
+ "p95": {
185
+ "type": "integer",
186
+ "minimum": 0
187
+ },
188
+ "max": {
189
+ "type": "integer",
190
+ "minimum": 0
191
+ }
192
+ }
193
+ }
194
+ ]
195
+ },
196
+ "bare_suite_wall_clock_ms": {
197
+ "anyOf": [
198
+ {
199
+ "type": "null"
200
+ },
201
+ {
202
+ "type": "object",
203
+ "additionalProperties": false,
204
+ "required": [
205
+ "samples",
206
+ "p50",
207
+ "p95",
208
+ "max"
209
+ ],
210
+ "properties": {
211
+ "samples": {
212
+ "type": "integer",
213
+ "minimum": 1
214
+ },
215
+ "p50": {
216
+ "type": "integer",
217
+ "minimum": 0
218
+ },
219
+ "p95": {
220
+ "type": "integer",
221
+ "minimum": 0
222
+ },
223
+ "max": {
224
+ "type": "integer",
225
+ "minimum": 0
226
+ }
227
+ }
228
+ }
229
+ ]
230
+ }
231
+ },
232
+ "description": "Nearest-rank percentiles over the events that carry each duration; null when none did."
233
+ },
234
+ "oob_signoffs": {
235
+ "type": "integer",
236
+ "minimum": 0,
237
+ "description": "Events that recorded an out-of-band sign-off."
238
+ },
115
239
  "interpretation": {
116
240
  "const": "finding-is-not-proof-of-intent"
117
241
  }
@@ -127,6 +251,47 @@
127
251
  "last_event": null,
128
252
  "by_rule": [],
129
253
  "by_surface": [],
254
+ "latency": {
255
+ "hook_latency_ms": null,
256
+ "verify_wall_clock_ms": null,
257
+ "bare_suite_wall_clock_ms": null
258
+ },
259
+ "oob_signoffs": 0,
260
+ "interpretation": "finding-is-not-proof-of-intent"
261
+ },
262
+ {
263
+ "schema_version": 1,
264
+ "events": 2,
265
+ "blocked": 2,
266
+ "warnings": 0,
267
+ "sessions": 1,
268
+ "first_event": "2026-09-23T14:00:00.000Z",
269
+ "last_event": "2026-09-23T14:05:00.000Z",
270
+ "by_rule": [
271
+ {
272
+ "rule": "test-deletion",
273
+ "events": 2,
274
+ "blocked": 2,
275
+ "warnings": 0
276
+ }
277
+ ],
278
+ "by_surface": [
279
+ {
280
+ "surface": "pretooluse",
281
+ "events": 2
282
+ }
283
+ ],
284
+ "latency": {
285
+ "hook_latency_ms": {
286
+ "samples": 2,
287
+ "p50": 40,
288
+ "p95": 55,
289
+ "max": 55
290
+ },
291
+ "verify_wall_clock_ms": null,
292
+ "bare_suite_wall_clock_ms": null
293
+ },
294
+ "oob_signoffs": 1,
130
295
  "interpretation": "finding-is-not-proof-of-intent"
131
296
  }
132
297
  ]