tamperward 2.29.12 → 2.29.14

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +143 -33
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -17883,6 +17883,22 @@ function parseStraceFileAccess(raw) {
17883
17883
  }
17884
17884
  return out3;
17885
17885
  }
17886
+ function tracerDenialDiagnostic(stderr) {
17887
+ for (const raw of stderr.split("\n")) {
17888
+ const line = raw.trim();
17889
+ if (line.startsWith("strace:") && TRACER_DENIAL_RE.test(line)) return line;
17890
+ }
17891
+ return null;
17892
+ }
17893
+ function classifyTraceRun(r) {
17894
+ if (r.spawnError) return { kind: "tracer-error", diagnostic: `strace could not be launched: ${r.spawnError}` };
17895
+ const denial = tracerDenialDiagnostic(r.stderr);
17896
+ if (denial) return { kind: "tracer-error", diagnostic: denial };
17897
+ if (r.signal) return { kind: "tracer-error", diagnostic: `strace was killed by ${r.signal} before the trace completed` };
17898
+ if (r.traceFileCount === 0) return { kind: "tracer-error", diagnostic: "strace produced no trace files; the tracer did not attach (ptrace may be denied)" };
17899
+ if (r.accessCount === 0) return { kind: "tracer-error", diagnostic: "strace recorded no file syscalls; the tracer attached to nothing (ptrace may be denied)" };
17900
+ return { kind: "ok", exit: r.status ?? 1 };
17901
+ }
17886
17902
  function inside3(root, path) {
17887
17903
  const rel = relative6(resolve11(root), resolve11(path));
17888
17904
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute12(rel);
@@ -18009,11 +18025,9 @@ function rewriteTraceRoot(accesses, actualRoot) {
18009
18025
  return { ...item, path: join22(TRACE_ROOT, ...rel.split("/")) };
18010
18026
  });
18011
18027
  }
18012
- function runOneTrace(cwd, base, command, budget) {
18013
- const root = mkdtempSync5(join22(tmpdir6(), "tw-trace-base-"));
18028
+ function straceOnce(root, command, budget) {
18014
18029
  const traceDir = mkdtempSync5(join22(tmpdir6(), "tw-trace-log-"));
18015
18030
  try {
18016
- materializeBase(base, cwd, root);
18017
18031
  const prefix = join22(traceDir, "trace");
18018
18032
  const traced = spawnSync5(
18019
18033
  "strace",
@@ -18042,21 +18056,40 @@ function runOneTrace(cwd, base, command, budget) {
18042
18056
  maxBuffer: 64 * 1024 * 1024
18043
18057
  }
18044
18058
  );
18045
- if (traced.error) throw traced.error;
18046
- const logs = traceFiles(prefix);
18047
- if (logs.length === 0) {
18048
- throw new Error("strace produced no trace files");
18049
- }
18059
+ const logs = traced.error ? [] : traceFiles(prefix);
18050
18060
  const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync20(path, "utf8")));
18051
- return {
18052
- exit: traced.status ?? 1,
18053
- accesses: rewriteTraceRoot(accesses, root)
18061
+ const raw = {
18062
+ spawnError: traced.error ? traced.error.message : null,
18063
+ status: traced.status,
18064
+ signal: traced.signal ?? null,
18065
+ stderr: String(traced.stderr ?? "").slice(0, 8192),
18066
+ traceFileCount: logs.length,
18067
+ accessCount: accesses.length
18054
18068
  };
18069
+ return { raw, accesses };
18055
18070
  } finally {
18056
- rmSync9(root, { recursive: true, force: true });
18057
18071
  rmSync9(traceDir, { recursive: true, force: true });
18058
18072
  }
18059
18073
  }
18074
+ function runOneTrace(cwd, base, command, budget) {
18075
+ const root = mkdtempSync5(join22(tmpdir6(), "tw-trace-base-"));
18076
+ try {
18077
+ materializeBase(base, cwd, root);
18078
+ const { raw, accesses } = straceOnce(root, command, budget);
18079
+ return { outcome: classifyTraceRun(raw), accesses: rewriteTraceRoot(accesses, root) };
18080
+ } finally {
18081
+ rmSync9(root, { recursive: true, force: true });
18082
+ }
18083
+ }
18084
+ function tracerPreflight(budget = 10) {
18085
+ const probeRoot = mkdtempSync5(join22(tmpdir6(), "tw-trace-probe-"));
18086
+ try {
18087
+ const { raw } = straceOnce(probeRoot, "true", Math.max(1, Math.min(budget, 10)));
18088
+ return classifyTraceRun(raw);
18089
+ } finally {
18090
+ rmSync9(probeRoot, { recursive: true, force: true });
18091
+ }
18092
+ }
18060
18093
  function renderText2(report2) {
18061
18094
  const out3 = (s = "") => void process.stdout.write(s + "\n");
18062
18095
  out3("tamperward trace-verify \u2014 ADVISORY observation, not an enforcement verdict");
@@ -18143,6 +18176,15 @@ function runTraceVerify(opts = {}) {
18143
18176
  `);
18144
18177
  return 2;
18145
18178
  }
18179
+ const preflight = tracerPreflight(budget);
18180
+ if (preflight.kind === "tracer-error") {
18181
+ process.stderr.write(
18182
+ `tamperward trace-verify: the tracer cannot observe in this environment, so no verifier input can be discovered \u2014 ${preflight.diagnostic}.
18183
+ This is a tracing/tooling limitation (e.g. ptrace denied by seccomp or Yama), not a verifier result.
18184
+ `
18185
+ );
18186
+ return 2;
18187
+ }
18146
18188
  const atBase = trackedAt(base, cwd);
18147
18189
  const tracked = new Set(atBase);
18148
18190
  const covered = verifierCoveredInputs(command, atBase, policy);
@@ -18150,9 +18192,16 @@ function runTraceVerify(opts = {}) {
18150
18192
  const runExits = [];
18151
18193
  try {
18152
18194
  for (let i = 0; i < runs; i++) {
18153
- const result = runOneTrace(cwd, base, command, budget);
18154
- observed.push(result.accesses);
18155
- runExits.push(result.exit);
18195
+ const { outcome, accesses } = runOneTrace(cwd, base, command, budget);
18196
+ if (outcome.kind === "tracer-error") {
18197
+ process.stderr.write(
18198
+ `tamperward trace-verify: tracing failed during run ${i + 1}/${runs} \u2014 ${outcome.diagnostic}. No advisory report emitted.
18199
+ `
18200
+ );
18201
+ return 2;
18202
+ }
18203
+ observed.push(accesses);
18204
+ runExits.push(outcome.exit);
18156
18205
  }
18157
18206
  } catch (e) {
18158
18207
  process.stderr.write(`tamperward trace-verify: tracing failed (${e instanceof Error ? e.message : String(e)}).
@@ -18199,7 +18248,7 @@ function parseTraceVerify(args) {
18199
18248
  }
18200
18249
  return out3;
18201
18250
  }
18202
- var TRACE_ROOT, CONFIG_BASENAMES;
18251
+ var TRACE_ROOT, TRACER_DENIAL_RE, CONFIG_BASENAMES;
18203
18252
  var init_trace_verify = __esm({
18204
18253
  "src/cli/trace-verify.ts"() {
18205
18254
  "use strict";
@@ -18209,6 +18258,7 @@ var init_trace_verify = __esm({
18209
18258
  init_policy_load();
18210
18259
  init_verify();
18211
18260
  TRACE_ROOT = "/__tamperward_trusted_base__";
18261
+ TRACER_DENIAL_RE = /\b(?:PTRACE_[A-Z_]+|ptrace|seccomp|no[-_ ]new[-_ ]privs|yama)\b|Operation not permitted|Permission denied/i;
18212
18262
  CONFIG_BASENAMES = /* @__PURE__ */ new Set([
18213
18263
  "package.json",
18214
18264
  "pytest.ini",
@@ -18237,18 +18287,69 @@ var init_trace_verify = __esm({
18237
18287
  });
18238
18288
 
18239
18289
  // src/runtimes.ts
18240
- import { existsSync as existsSync17 } from "node:fs";
18290
+ import { existsSync as existsSync17, readFileSync as readFileSync21 } from "node:fs";
18241
18291
  import { join as join23 } from "node:path";
18292
+ function isRecord4(x) {
18293
+ return typeof x === "object" && x !== null && !Array.isArray(x);
18294
+ }
18295
+ function hooksBeyondTamperward(hooks) {
18296
+ if (!isRecord4(hooks)) return hooks != null;
18297
+ for (const entries of Object.values(hooks)) {
18298
+ if (!Array.isArray(entries)) {
18299
+ if (entries != null) return true;
18300
+ continue;
18301
+ }
18302
+ for (const entry of entries) {
18303
+ const hs = isRecord4(entry) ? entry.hooks : null;
18304
+ if (!Array.isArray(hs)) {
18305
+ if (hs != null) return true;
18306
+ continue;
18307
+ }
18308
+ for (const h of hs) {
18309
+ const cmd = isRecord4(h) ? String(h.command ?? "") : "";
18310
+ if (!/tamperward/.test(cmd)) return true;
18311
+ }
18312
+ }
18313
+ }
18314
+ return false;
18315
+ }
18316
+ function claudeSettingsBeyondTamperward(cwd) {
18317
+ const path = join23(cwd, ".claude", "settings.json");
18318
+ let raw;
18319
+ try {
18320
+ if (!existsSync17(path)) return false;
18321
+ raw = readFileSync21(path, "utf8");
18322
+ } catch {
18323
+ return false;
18324
+ }
18325
+ let parsed;
18326
+ try {
18327
+ parsed = JSON.parse(raw);
18328
+ } catch {
18329
+ return raw.trim().length > 0;
18330
+ }
18331
+ if (!isRecord4(parsed)) return raw.trim().length > 0;
18332
+ for (const [key2, value] of Object.entries(parsed)) {
18333
+ if (key2 === "disableAllHooks") continue;
18334
+ if (key2 === "hooks") {
18335
+ if (hooksBeyondTamperward(value)) return true;
18336
+ continue;
18337
+ }
18338
+ return true;
18339
+ }
18340
+ return false;
18341
+ }
18242
18342
  function detectRuntimes(cwd) {
18243
18343
  const found = [];
18244
18344
  for (const rt of KNOWN_RUNTIMES) {
18245
- const matchedMarker = rt.markers.find((m) => {
18345
+ const marker = rt.markers.find((m) => {
18246
18346
  try {
18247
18347
  return existsSync17(join23(cwd, m));
18248
18348
  } catch {
18249
18349
  return false;
18250
18350
  }
18251
18351
  });
18352
+ const matchedMarker = marker ?? rt.detectExtra?.(cwd) ?? void 0;
18252
18353
  if (matchedMarker !== void 0) found.push({ ...rt, matchedMarker });
18253
18354
  }
18254
18355
  return found;
@@ -18268,10 +18369,14 @@ function detectionHeadline(detected) {
18268
18369
  return `Detected ${detected.length} agent runtimes: ${parts.join(", ")}.`;
18269
18370
  }
18270
18371
  function neutralOnlyCaveat(detected) {
18271
- if (detected.length === 0 || hasInLoopRuntime(detected)) return null;
18272
- const names = detected.map((r) => r.label).join(", ");
18273
- const tracking = [...new Set(detected.map((r) => r.adapterTracking).filter(Boolean))].join(", ");
18274
- return `In-loop steering (deny-before-execute) ships for Claude Code only today, so on ${names} your live protection is the agent-neutral layers: pre-commit and CI. The Claude hook wiring is still written and is inert unless Claude Code runs in this repository.${tracking ? ` A native in-loop adapter is tracked in ${tracking}.` : ""}`;
18372
+ const neutral = detected.filter((r) => r.steering === "neutral");
18373
+ if (neutral.length === 0) return null;
18374
+ const names = neutral.map((r) => r.label).join(", ");
18375
+ const tracking = [...new Set(neutral.map((r) => r.adapterTracking).filter(Boolean))].join(", ");
18376
+ const claudeAlso = hasInLoopRuntime(detected);
18377
+ const lead = claudeAlso ? `In-loop steering (deny-before-execute) ships for Claude Code only today: Claude Code here gets it, but on ${names} your live protection is ` : `In-loop steering (deny-before-execute) ships for Claude Code only today, so on ${names} your live protection is `;
18378
+ const tail = claudeAlso ? "" : " The Claude hook wiring is still written and is inert unless Claude Code runs in this repository.";
18379
+ return `${lead}the agent-neutral layers: pre-commit and CI.${tail}${tracking ? ` A native in-loop adapter is tracked in ${tracking}.` : ""}`;
18275
18380
  }
18276
18381
  var KNOWN_RUNTIMES;
18277
18382
  var init_runtimes = __esm({
@@ -18280,8 +18385,13 @@ var init_runtimes = __esm({
18280
18385
  KNOWN_RUNTIMES = [
18281
18386
  {
18282
18387
  id: "claude-code",
18388
+ // Markers are Claude Code files `init` never writes, so detection never mistakes
18389
+ // TamperWard's own generated `.claude/settings.json` for evidence a human runs
18390
+ // Claude here (#526). `detectExtra` looks at that generated file's CONTENT for
18391
+ // real use (configuration beyond the two hooks init merges in).
18283
18392
  label: "Claude Code",
18284
- markers: [".claude/settings.json", ".claude"],
18393
+ markers: ["CLAUDE.md", ".claude/settings.local.json", ".claude/commands", ".claude/agents"],
18394
+ detectExtra: (cwd) => claudeSettingsBeyondTamperward(cwd) ? ".claude/settings.json" : null,
18285
18395
  steering: "in-loop",
18286
18396
  note: "native in-loop steering (PreToolUse deny + Stop sweep) plus the agent-neutral layers."
18287
18397
  },
@@ -18315,7 +18425,7 @@ var init_runtimes = __esm({
18315
18425
 
18316
18426
  // src/cli/onboard.ts
18317
18427
  import { execFileSync as execFileSync12 } from "node:child_process";
18318
- import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync21, realpathSync as realpathSync11, rmSync as rmSync10 } from "node:fs";
18428
+ import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync22, realpathSync as realpathSync11, rmSync as rmSync10 } from "node:fs";
18319
18429
  import { tmpdir as tmpdir7 } from "node:os";
18320
18430
  import { join as join24, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
18321
18431
  import { createInterface } from "node:readline";
@@ -18391,7 +18501,7 @@ function writeVerifyCommand(cwd, command) {
18391
18501
  return `${POLICY_FILE} is not a regular file; refusing to follow or replace a symlink or special file`;
18392
18502
  }
18393
18503
  const mode = existingMode(path, 420);
18394
- const original = kind === "file" ? readFileSync21(path, "utf8") : null;
18504
+ const original = kind === "file" ? readFileSync22(path, "utf8") : null;
18395
18505
  let next;
18396
18506
  try {
18397
18507
  const doc = parseDocument(original && original.trim() ? original : "version: 1\n");
@@ -18992,7 +19102,7 @@ var init_capture = __esm({
18992
19102
 
18993
19103
  // src/research/manifest.ts
18994
19104
  import { createHash as createHash13 } from "node:crypto";
18995
- import { readFileSync as readFileSync22 } from "node:fs";
19105
+ import { readFileSync as readFileSync23 } from "node:fs";
18996
19106
  import { dirname as dirname10, resolve as resolve14 } from "node:path";
18997
19107
  function taskFrom(raw, index, manifestDir) {
18998
19108
  const where = `task #${index + 1}`;
@@ -19026,7 +19136,7 @@ function readManifest(path) {
19026
19136
  const abs = resolve14(path);
19027
19137
  let bytes;
19028
19138
  try {
19029
- bytes = readFileSync22(abs);
19139
+ bytes = readFileSync23(abs);
19030
19140
  } catch (e) {
19031
19141
  throw new ResearchError(`cannot read task manifest ${abs}: ${e instanceof Error ? e.message : String(e)}`);
19032
19142
  }
@@ -19238,7 +19348,7 @@ var init_record = __esm({
19238
19348
 
19239
19349
  // src/research/run.ts
19240
19350
  import { execFileSync as execFileSync13 } from "node:child_process";
19241
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
19351
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync24, renameSync as renameSync4, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
19242
19352
  import { join as join25, resolve as resolve15 } from "node:path";
19243
19353
  function git6(args, cwd) {
19244
19354
  return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
@@ -19249,7 +19359,7 @@ function pairRecordPath(ledger, task, pair) {
19249
19359
  function resumableRecord(path, expected) {
19250
19360
  let raw;
19251
19361
  try {
19252
- raw = JSON.parse(readFileSync23(path, "utf8"));
19362
+ raw = JSON.parse(readFileSync24(path, "utf8"));
19253
19363
  } catch (e) {
19254
19364
  throw new ResearchError(`ledger record ${path} is malformed (${errorMessage(e).split("\n")[0]}); remove it to regenerate the pair, or use a new --out`);
19255
19365
  }
@@ -19672,7 +19782,7 @@ var init_run2 = __esm({
19672
19782
  });
19673
19783
 
19674
19784
  // src/research/summarize.ts
19675
- import { readdirSync as readdirSync7, readFileSync as readFileSync24 } from "node:fs";
19785
+ import { readdirSync as readdirSync7, readFileSync as readFileSync25 } from "node:fs";
19676
19786
  import { join as join26, resolve as resolve16 } from "node:path";
19677
19787
  function readLedger2(dir) {
19678
19788
  const pairsDir = join26(resolve16(dir), "pairs");
@@ -19687,7 +19797,7 @@ function readLedger2(dir) {
19687
19797
  const path = join26(pairsDir, n);
19688
19798
  let raw;
19689
19799
  try {
19690
- raw = JSON.parse(readFileSync24(path, "utf8"));
19800
+ raw = JSON.parse(readFileSync25(path, "utf8"));
19691
19801
  } catch (e) {
19692
19802
  throw new ResearchError(`ledger record ${path} is not valid JSON: ${errorMessage(e)}`);
19693
19803
  }
@@ -20394,7 +20504,7 @@ var init_main = __esm({
20394
20504
  // src/cli/index.ts
20395
20505
  init_hook_client();
20396
20506
  init_exit();
20397
- import { readFileSync as readFileSync25 } from "node:fs";
20507
+ import { readFileSync as readFileSync26 } from "node:fs";
20398
20508
  function loadMain() {
20399
20509
  return Promise.resolve().then(() => (init_main(), main_exports));
20400
20510
  }
@@ -20407,7 +20517,7 @@ async function launch(argv) {
20407
20517
  if (kind && hookServiceEnabled()) {
20408
20518
  let raw = null;
20409
20519
  try {
20410
- raw = readFileSync25(0, "utf8");
20520
+ raw = readFileSync26(0, "utf8");
20411
20521
  } catch {
20412
20522
  raw = null;
20413
20523
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.29.12",
3
+ "version": "2.29.14",
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",