tamperward 2.23.15 → 2.23.17

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 +171 -101
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -6756,6 +6756,25 @@ function entryShape(h) {
6756
6756
  }
6757
6757
  return { problems, runs };
6758
6758
  }
6759
+ function judgeGateEntry(event, matcher, h) {
6760
+ const cmd = typeof h.command === "string" ? h.command : "";
6761
+ const a = analyseGateCommand(cmd, event);
6762
+ const shape = entryShape(h);
6763
+ const problems = [...a.problems, ...shape.problems];
6764
+ let runs = a.runs && shape.runs;
6765
+ let tools = null;
6766
+ if (event === "PreToolUse") {
6767
+ const sel = matcherSelection(matcher);
6768
+ tools = sel.tools;
6769
+ if (sel.problem) {
6770
+ problems.push(sel.problem);
6771
+ }
6772
+ } else if (matcher !== void 0 && !(typeof matcher === "string" && (matcher === "" || matcher === "*"))) {
6773
+ problems.push(`a \`matcher\` (${JSON.stringify(matcher)}) on the Stop entry`);
6774
+ runs = false;
6775
+ }
6776
+ return { runs, problems, pin: a.pin, tools };
6777
+ }
6759
6778
  function gateCandidates(s, event) {
6760
6779
  const out3 = [];
6761
6780
  for (const group of eventEntries(s, event) ?? []) {
@@ -6778,22 +6797,7 @@ function gateCandidates(s, event) {
6778
6797
  continue;
6779
6798
  }
6780
6799
  if (!MENTIONS_GATE.test(cmd)) continue;
6781
- const a = analyseGateCommand(cmd, event);
6782
- const shape = entryShape(h);
6783
- const problems = [...a.problems, ...shape.problems];
6784
- let runs = a.runs && shape.runs;
6785
- let tools = null;
6786
- if (event === "PreToolUse") {
6787
- const sel = matcherSelection(group.matcher);
6788
- tools = sel.tools;
6789
- if (sel.problem) {
6790
- problems.push(sel.problem);
6791
- }
6792
- } else if (group.matcher !== void 0 && !(typeof group.matcher === "string" && (group.matcher === "" || group.matcher === "*"))) {
6793
- problems.push(`a \`matcher\` (${JSON.stringify(group.matcher)}) on the Stop entry`);
6794
- runs = false;
6795
- }
6796
- out3.push({ key: JSON.stringify({ m: group.matcher, h }), runs, problems, pin: a.pin, tools });
6800
+ out3.push({ key: JSON.stringify({ m: group.matcher, h }), ...judgeGateEntry(event, group.matcher, h) });
6797
6801
  }
6798
6802
  }
6799
6803
  return out3;
@@ -10885,10 +10889,55 @@ var init_verifier_limits = __esm({
10885
10889
  }
10886
10890
  });
10887
10891
 
10892
+ // src/safe-write.ts
10893
+ import { randomBytes } from "node:crypto";
10894
+ import { lstatSync as lstatSync7, renameSync as renameSync3, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
10895
+ function writeTargetKind(path) {
10896
+ let st;
10897
+ try {
10898
+ st = lstatSync7(path, { throwIfNoEntry: false });
10899
+ } catch (e) {
10900
+ if (errnoCode(e) === "ENOTDIR") return "absent";
10901
+ throw e;
10902
+ }
10903
+ if (!st) return "absent";
10904
+ if (st.isSymbolicLink()) return "symlink";
10905
+ return st.isFile() ? "file" : "irregular";
10906
+ }
10907
+ function refuseNonRegular(path) {
10908
+ switch (writeTargetKind(path)) {
10909
+ case "symlink":
10910
+ return "refusing: symlink \u2014 the target would be written instead of this path; replace the link with a regular file, then re-run";
10911
+ case "irregular":
10912
+ return "refusing: not a regular file \u2014 a directory or special file stands where a file is expected; remove it, then re-run";
10913
+ default:
10914
+ return null;
10915
+ }
10916
+ }
10917
+ function existingMode(path, fallback) {
10918
+ if (writeTargetKind(path) !== "file") return fallback;
10919
+ return lstatSync7(path).mode & 511;
10920
+ }
10921
+ function atomicReplaceFile(path, content, mode) {
10922
+ const tmp = `${path}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
10923
+ try {
10924
+ writeFileSync7(tmp, content, { encoding: "utf8", flag: "wx", mode });
10925
+ renameSync3(tmp, path);
10926
+ } finally {
10927
+ rmSync5(tmp, { force: true });
10928
+ }
10929
+ }
10930
+ var init_safe_write = __esm({
10931
+ "src/safe-write.ts"() {
10932
+ "use strict";
10933
+ init_narrow();
10934
+ }
10935
+ });
10936
+
10888
10937
  // src/cli/init.ts
10889
10938
  import { execFileSync as execFileSync6 } from "node:child_process";
10890
10939
  import { createHash as createHash6 } from "node:crypto";
10891
- import { existsSync as existsSync11, lstatSync as lstatSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync4, writeFileSync as writeFileSync7, chmodSync as chmodSync2, statSync as statSync3 } from "node:fs";
10940
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync4 } from "node:fs";
10892
10941
  import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join14, relative as relative3, resolve as resolve4 } from "node:path";
10893
10942
  function gitConfig(cwd, key2) {
10894
10943
  try {
@@ -11050,6 +11099,8 @@ function verifierSetupMessage(cwd) {
11050
11099
  }
11051
11100
  function planPolicy(cwd) {
11052
11101
  const path = join14(cwd, POLICY_FILE);
11102
+ const refused = refuseNonRegular(path);
11103
+ if (refused) return { item: "policy", path: POLICY_FILE, status: "error", detail: refused };
11053
11104
  if (existsSync11(path)) {
11054
11105
  try {
11055
11106
  loadPolicy(cwd);
@@ -11063,7 +11114,7 @@ function planPolicy(cwd) {
11063
11114
  path: POLICY_FILE,
11064
11115
  status: "create",
11065
11116
  detail: "baseline policy with commented overrides",
11066
- apply: () => writeFileSync7(path, POLICY_CONTENT)
11117
+ apply: () => atomicReplaceFile(path, POLICY_CONTENT, 420)
11067
11118
  };
11068
11119
  }
11069
11120
  function planGitignore(cwd) {
@@ -11071,23 +11122,15 @@ function planGitignore(cwd) {
11071
11122
  const path = join14(cwd, rel);
11072
11123
  if (!existsSync11(join14(cwd, "node_modules"))) return null;
11073
11124
  let existing = "";
11074
- if (existsSync11(path)) {
11075
- let st;
11076
- try {
11077
- st = lstatSync7(path);
11078
- } catch (e) {
11079
- return { item: "gitignore", path: rel, status: "error", detail: "cannot inspect \u2014 " + errText3(e) };
11080
- }
11081
- if (!st.isFile()) {
11082
- return {
11083
- item: "gitignore",
11084
- path: rel,
11085
- status: "error",
11086
- detail: st.isSymbolicLink() ? "is a symbolic link \u2014 refusing to follow it" : "exists but is not a regular file \u2014 refusing to write"
11087
- };
11088
- }
11089
- existing = readFileSync12(path, "utf8");
11125
+ let kind;
11126
+ try {
11127
+ kind = writeTargetKind(path);
11128
+ } catch (e) {
11129
+ return { item: "gitignore", path: rel, status: "error", detail: "cannot inspect \u2014 " + errText3(e) };
11090
11130
  }
11131
+ const refused = refuseNonRegular(path);
11132
+ if (refused) return { item: "gitignore", path: rel, status: "error", detail: refused };
11133
+ if (kind === "file") existing = readFileSync12(path, "utf8");
11091
11134
  try {
11092
11135
  const tracked = execFileSync6("git", ["ls-files", "--cached", "--", "node_modules"], {
11093
11136
  cwd,
@@ -11120,14 +11163,27 @@ function planGitignore(cwd) {
11120
11163
  return {
11121
11164
  item: "gitignore",
11122
11165
  path: rel,
11123
- status: existsSync11(path) ? "update" : "create",
11166
+ status: kind === "file" ? "update" : "create",
11124
11167
  detail: "exclude installed Node dependencies from repository diffs",
11125
- apply: () => writeFileSync7(path, next)
11168
+ apply: () => atomicReplaceFile(path, next, existingMode(path, 420))
11126
11169
  };
11127
11170
  }
11171
+ function gateVerdicts(event, groups, needle) {
11172
+ const out3 = [];
11173
+ for (const group of groups) {
11174
+ for (const h of group.hooks ?? []) {
11175
+ if (!needle.test(String(h.command ?? ""))) continue;
11176
+ const { runs, problems } = judgeGateEntry(event, group.matcher, h);
11177
+ out3.push({ event, group, h, runs, problems });
11178
+ }
11179
+ }
11180
+ return out3;
11181
+ }
11128
11182
  function planClaudeHooks(cwd) {
11129
11183
  const rel = ".claude/settings.json";
11130
11184
  const path = join14(cwd, rel);
11185
+ const refused = refuseNonRegular(path);
11186
+ if (refused) return { item: "agent", path: rel, status: "error", detail: refused };
11131
11187
  let settings = {};
11132
11188
  if (existsSync11(path)) {
11133
11189
  let parsed;
@@ -11156,10 +11212,24 @@ function planClaudeHooks(cwd) {
11156
11212
  const needPre = preEntries.length === 0;
11157
11213
  const stopEntries = entriesWith(hooks.Stop, SWEEP_RE);
11158
11214
  const needStop = stopEntries.length === 0;
11215
+ const neutralised = [
11216
+ ...gateVerdicts("PreToolUse", preEntries, HOOK_RE),
11217
+ ...gateVerdicts("Stop", stopEntries, SWEEP_RE)
11218
+ ].filter((v) => !v.runs);
11219
+ const foreign = neutralised.filter((v) => ours(String(v.h.command ?? "")) === null);
11220
+ if (foreign.length > 0) {
11221
+ return {
11222
+ item: "agent",
11223
+ path: rel,
11224
+ status: "error",
11225
+ detail: `${foreign.map((v) => `the ${v.event} gate entry is neutralised (${v.problems.join("; ")})`).join("; ")} \u2014 the runtime would not run the gate through it; remove the entry, then re-run init (refusing to rewrite a hand-written entry)`
11226
+ };
11227
+ }
11159
11228
  const stale = preEntries.filter((m) => missingTools(m.matcher).length > 0);
11160
- const repin = [...preEntries, ...stopEntries].flatMap((m) => m.hooks ?? []).filter((h) => stalePin(String(h.command ?? "")));
11229
+ const restoring = new Set(neutralised.map((v) => v.h));
11230
+ const repin = [...preEntries, ...stopEntries].flatMap((m) => m.hooks ?? []).filter((h) => !restoring.has(h) && stalePin(String(h.command ?? "")));
11161
11231
  const needDisableFalse = settings.disableAllHooks !== false;
11162
- if (!needPre && !needStop && stale.length === 0 && repin.length === 0 && !needDisableFalse) {
11232
+ if (!needPre && !needStop && stale.length === 0 && repin.length === 0 && neutralised.length === 0 && !needDisableFalse) {
11163
11233
  return { item: "agent", path: rel, status: "ok", detail: "PreToolUse + Stop hooks already wired" };
11164
11234
  }
11165
11235
  const repairing = stale.length > 0 ? [...new Set(stale.flatMap((m) => missingTools(m.matcher)))] : [];
@@ -11173,9 +11243,16 @@ function planClaudeHooks(cwd) {
11173
11243
  needStop && "wire Stop sweep",
11174
11244
  repairing.length > 0 && `widen the PreToolUse matcher to cover ${repairing.join(", ")}`,
11175
11245
  repin.length > 0 && `re-pin the hook commands to tamperward@${TW_VERSION} (was ${pins.join(", ")})`,
11246
+ ...neutralised.map((v) => `restore the ${v.event} gate entry to the shape init writes \u2014 it is neutralised (${v.problems.join("; ")})`),
11176
11247
  needDisableFalse && (settings.disableAllHooks === void 0 ? "declare disableAllHooks: false so the user settings file cannot switch the hooks off" : `set disableAllHooks: false (was ${JSON.stringify(settings.disableAllHooks)})`)
11177
11248
  ].filter(Boolean).join(" + "),
11178
11249
  apply: () => {
11250
+ for (const v of neutralised) {
11251
+ for (const k of Object.keys(v.h)) delete v.h[k];
11252
+ v.h.type = "command";
11253
+ v.h.command = v.event === "PreToolUse" ? HOOK_CMD : SWEEP_CMD;
11254
+ if (v.event === "Stop") delete v.group.matcher;
11255
+ }
11179
11256
  for (const h of repin) {
11180
11257
  const kind = ours(String(h.command ?? ""))?.[2];
11181
11258
  h.command = kind === "hook claude" ? HOOK_CMD : kind === "sweep claude" ? SWEEP_CMD : h.command;
@@ -11194,7 +11271,7 @@ function planClaudeHooks(cwd) {
11194
11271
  }
11195
11272
  if (needDisableFalse) settings.disableAllHooks = false;
11196
11273
  mkdirSync6(dirname4(path), { recursive: true });
11197
- writeFileSync7(path, JSON.stringify(settings, null, 2) + "\n");
11274
+ atomicReplaceFile(path, JSON.stringify(settings, null, 2) + "\n", existingMode(path, 420));
11198
11275
  }
11199
11276
  };
11200
11277
  }
@@ -11251,10 +11328,10 @@ function planPreCommit(cwd) {
11251
11328
  }
11252
11329
  }
11253
11330
  const rel = display(cwd, path);
11254
- if (existsSync11(path) && !statSync3(path).isFile()) {
11255
- return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
11256
- }
11331
+ const refused = refuseNonRegular(path);
11332
+ if (refused) return { item: "pre-commit", path: rel, status: "error", detail: refused };
11257
11333
  const existing = existsSync11(path) ? readFileSync12(path, "utf8") : null;
11334
+ const hookMode = () => existingMode(path, 493) | 73;
11258
11335
  if (existing === null) {
11259
11336
  return {
11260
11337
  item: "pre-commit",
@@ -11263,10 +11340,9 @@ function planPreCommit(cwd) {
11263
11340
  detail: `create via ${note}`,
11264
11341
  apply: () => {
11265
11342
  mkdirSync6(dirname4(path), { recursive: true });
11266
- writeFileSync7(path, `#!/bin/sh
11343
+ atomicReplaceFile(path, `#!/bin/sh
11267
11344
  ${block.join("\n")}
11268
- `);
11269
- chmodSync2(path, 493);
11345
+ `, hookMode());
11270
11346
  }
11271
11347
  };
11272
11348
  }
@@ -11276,11 +11352,8 @@ ${block.join("\n")}
11276
11352
  }
11277
11353
  const lines = existing.split("\n");
11278
11354
  const dead = unconditionalExitAt(lines);
11279
- const at = lines.findIndex((l) => PRECOMMIT_RE.test(l));
11280
- const write = (out3) => {
11281
- writeFileSync7(path, out3.join("\n"));
11282
- chmodSync2(path, 493);
11283
- };
11355
+ const at = lines.findIndex((l) => !/^\s*#/.test(l) && PRECOMMIT_RE.test(l));
11356
+ const write = (out3) => atomicReplaceFile(path, out3.join("\n"), hookMode());
11284
11357
  if (at !== -1 && (dead === -1 || at < dead)) {
11285
11358
  if (!stalePin(lines[at])) {
11286
11359
  return { item: "pre-commit", path: rel, status: "ok", detail: "already runs the staged check" };
@@ -11343,9 +11416,11 @@ ${block.join("\n")}
11343
11416
  function planCodeowners(cwd) {
11344
11417
  const rel = ".github/CODEOWNERS";
11345
11418
  const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
11346
- (r) => existsSync11(join14(cwd, r))
11419
+ (r) => writeTargetKind(join14(cwd, r)) !== "absent"
11347
11420
  );
11348
11421
  const path = join14(cwd, existingRel ?? rel);
11422
+ const refused = refuseNonRegular(path);
11423
+ if (refused) return { item: "codeowners", path: existingRel ?? rel, status: "error", detail: refused };
11349
11424
  const existing = existingRel ? readFileSync12(path, "utf8") : null;
11350
11425
  const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
11351
11426
  if (existingRel !== void 0 && missing.length === 0) {
@@ -11373,7 +11448,7 @@ ${CODEOWNERS_MARK}
11373
11448
  } : {},
11374
11449
  apply: () => {
11375
11450
  mkdirSync6(dirname4(path), { recursive: true });
11376
- writeFileSync7(path, existing === null ? block.replace(/^\n/, "") : existing.replace(/\n?$/, "\n") + block);
11451
+ atomicReplaceFile(path, existing === null ? block.replace(/^\n/, "") : existing.replace(/\n?$/, "\n") + block, existingMode(path, 420));
11377
11452
  }
11378
11453
  };
11379
11454
  }
@@ -11382,8 +11457,10 @@ function planWorkflow(cwd, force) {
11382
11457
  const path = join14(cwd, rel);
11383
11458
  const write = () => {
11384
11459
  mkdirSync6(dirname4(path), { recursive: true });
11385
- writeFileSync7(path, workflowFile(WORKFLOW_CONTENT));
11460
+ atomicReplaceFile(path, workflowFile(WORKFLOW_CONTENT), existingMode(path, 420));
11386
11461
  };
11462
+ const refused = refuseNonRegular(path);
11463
+ if (refused) return { item: "ci", path: rel, status: "error", detail: refused };
11387
11464
  if (!existsSync11(path)) {
11388
11465
  return {
11389
11466
  item: "ci",
@@ -11518,7 +11595,9 @@ var init_init = __esm({
11518
11595
  init_verifier_limits();
11519
11596
  init_wiring();
11520
11597
  init_narrow();
11598
+ init_hook_tampering();
11521
11599
  init_repo_context();
11600
+ init_safe_write();
11522
11601
  errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
11523
11602
  isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
11524
11603
  HOOK_RE = /\btamperward(?:@\S+)?\s+hook\s+claude\b/;
@@ -11693,7 +11772,7 @@ import {
11693
11772
  readFileSync as readFileSync13,
11694
11773
  readlinkSync as readlinkSync2,
11695
11774
  realpathSync as realpathSync5,
11696
- statSync as statSync4
11775
+ statSync as statSync3
11697
11776
  } from "node:fs";
11698
11777
  import {
11699
11778
  basename as basename3,
@@ -11735,7 +11814,7 @@ function fingerprintRoot(root, cwd, h) {
11735
11814
  root.kind + " root moved from " + JSON.stringify(root.realPath) + " to " + JSON.stringify(currentReal)
11736
11815
  );
11737
11816
  }
11738
- if (!statSync4(root.path).isDirectory()) {
11817
+ if (!statSync3(root.path).isDirectory()) {
11739
11818
  throw new Unattestable(root.kind + " root " + JSON.stringify(root.path) + " is not a directory");
11740
11819
  }
11741
11820
  h.update("root\0" + root.kind + "\0" + root.path + "\0" + root.realPath + "\0" + String(rootStat.mode) + "\0");
@@ -11745,7 +11824,7 @@ function fingerprintRoot(root, cwd, h) {
11745
11824
  const hashFile = (path, label) => {
11746
11825
  let st;
11747
11826
  try {
11748
- st = statSync4(path);
11827
+ st = statSync3(path);
11749
11828
  if (!st.isFile()) throw new Error("not a regular file");
11750
11829
  h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
11751
11830
  h.update(readFileSync13(path));
@@ -11811,7 +11890,7 @@ function fingerprintRoot(root, cwd, h) {
11811
11890
  } catch {
11812
11891
  continue;
11813
11892
  }
11814
- const followed = statSync4(path);
11893
+ const followed = statSync3(path);
11815
11894
  if (followed.isFile()) {
11816
11895
  hashFile(resolved, "@target:" + rel);
11817
11896
  continue;
@@ -11859,7 +11938,7 @@ function measuredSnapshot(roots, probes, cwd, metrics) {
11859
11938
  }
11860
11939
  function freezeRoot(kind, path) {
11861
11940
  try {
11862
- if (!statSync4(path).isDirectory()) {
11941
+ if (!statSync3(path).isDirectory()) {
11863
11942
  throw new Error("not a directory");
11864
11943
  }
11865
11944
  return { kind, path: resolve5(path), realPath: realpathSync5(path) };
@@ -11908,7 +11987,7 @@ function resolveExecutable(token, cwd, env) {
11908
11987
  for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
11909
11988
  const candidate = resolve5(entry, token);
11910
11989
  try {
11911
- if (existsSync12(candidate) && statSync4(candidate).isFile()) return realpathSync5(candidate);
11990
+ if (existsSync12(candidate) && statSync3(candidate).isFile()) return realpathSync5(candidate);
11912
11991
  } catch {
11913
11992
  }
11914
11993
  }
@@ -12095,7 +12174,7 @@ var init_dependency_env = __esm({
12095
12174
 
12096
12175
  // src/suite-diagnostics.ts
12097
12176
  import { spawnSync } from "node:child_process";
12098
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
12177
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
12099
12178
  import { tmpdir as tmpdir3 } from "node:os";
12100
12179
  import { join as join16 } from "node:path";
12101
12180
  function emptySuiteDiagnostics() {
@@ -12224,7 +12303,7 @@ function runCapturedProcessSync(executable, args, opts) {
12224
12303
  }
12225
12304
  return parsed;
12226
12305
  } finally {
12227
- rmSync5(stateDir, { recursive: true, force: true });
12306
+ rmSync6(stateDir, { recursive: true, force: true });
12228
12307
  }
12229
12308
  }
12230
12309
  var DIAGNOSTIC_TAIL_BYTES, EMPTY_STREAM, CAPTURE_SUPERVISOR;
@@ -12414,7 +12493,7 @@ if (child) {
12414
12493
  // src/verifier-backend.ts
12415
12494
  import { createHash as createHash9, randomUUID } from "node:crypto";
12416
12495
  import { spawnSync as spawnSync2 } from "node:child_process";
12417
- import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync15, realpathSync as realpathSync6, statSync as statSync5 } from "node:fs";
12496
+ import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync15, realpathSync as realpathSync6, statSync as statSync4 } from "node:fs";
12418
12497
  import { delimiter as delimiter2, dirname as dirname6, join as join17, resolve as resolve6 } from "node:path";
12419
12498
  function identity() {
12420
12499
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
@@ -12423,7 +12502,7 @@ function identity() {
12423
12502
  }
12424
12503
  function writableByCaller(path) {
12425
12504
  try {
12426
- const st = statSync5(path);
12505
+ const st = statSync4(path);
12427
12506
  const { uid, groups } = identity();
12428
12507
  if (uid === 0) return true;
12429
12508
  if ((st.mode & 2) !== 0) return true;
@@ -12893,7 +12972,7 @@ var init_verifier_backend = __esm({
12893
12972
  // src/cli/verify.ts
12894
12973
  import { execFileSync as execFileSync8, spawnSync as spawnSync3 } from "node:child_process";
12895
12974
  import { createHash as createHash10 } from "node:crypto";
12896
- import { chmodSync as chmodSync3, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync6, symlinkSync, writeFileSync as writeFileSync9, existsSync as existsSync13 } from "node:fs";
12975
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync7, symlinkSync, writeFileSync as writeFileSync9, existsSync as existsSync13 } from "node:fs";
12897
12976
  import { tmpdir as tmpdir4 } from "node:os";
12898
12977
  import { dirname as dirname7, isAbsolute as isAbsolute10, join as join18, relative as relative5, resolve as resolve7, sep as sep3 } from "node:path";
12899
12978
  function oracleAssuranceReport() {
@@ -12941,7 +13020,7 @@ function safeSymlink(target, out3, root, label) {
12941
13020
  if (rootedLinkTarget(target) || !inside2(root, resolve7(dirname7(out3), target))) {
12942
13021
  throw linkEscape(label, target);
12943
13022
  }
12944
- rmSync6(out3, { force: true });
13023
+ rmSync7(out3, { force: true });
12945
13024
  symlinkSync(target, out3);
12946
13025
  }
12947
13026
  function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
@@ -13032,7 +13111,7 @@ function materialize(cwd, dest, dependencyRoot) {
13032
13111
  links.push({ rel, out: out3, target });
13033
13112
  } else if (st.isFile()) {
13034
13113
  cpSync(src, out3, { dereference: true });
13035
- chmodSync3(out3, st.mode);
13114
+ chmodSync2(out3, st.mode);
13036
13115
  } else {
13037
13116
  throw new Error(`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`);
13038
13117
  }
@@ -13052,7 +13131,7 @@ function probeFilesystemCaseSensitivity(dir) {
13052
13131
  writeFileSync9(upper, "", { flag: "wx" });
13053
13132
  return !existsSync13(lower);
13054
13133
  } finally {
13055
- rmSync6(upper, { force: true });
13134
+ rmSync7(upper, { force: true });
13056
13135
  }
13057
13136
  }
13058
13137
  function foldCase(p) {
@@ -13070,7 +13149,7 @@ function caseFoldCollision(paths) {
13070
13149
  }
13071
13150
  function dropSymlink(p) {
13072
13151
  try {
13073
- if (lstatSync9(p).isSymbolicLink()) rmSync6(p, { force: true });
13152
+ if (lstatSync9(p).isSymbolicLink()) rmSync7(p, { force: true });
13074
13153
  } catch {
13075
13154
  }
13076
13155
  }
@@ -13146,14 +13225,14 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
13146
13225
  const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
13147
13226
  const out3 = join18(dest, e.path);
13148
13227
  mkdirSync7(dirname7(out3), { recursive: true });
13149
- rmSync6(out3, { force: true });
13228
+ rmSync7(out3, { force: true });
13150
13229
  if (e.mode === "120000") {
13151
13230
  const target = content.toString("utf8");
13152
13231
  safeSymlink(target, out3, dest, `the base's ${e.path}`);
13153
13232
  restoredLinks.push({ path: e.path, out: out3, target });
13154
13233
  } else {
13155
13234
  writeFileSync9(out3, content);
13156
- chmodSync3(out3, parseInt(e.mode.slice(-4), 8) & 511);
13235
+ chmodSync2(out3, parseInt(e.mode.slice(-4), 8) & 511);
13157
13236
  }
13158
13237
  restored.push(e.path);
13159
13238
  }
@@ -13161,7 +13240,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
13161
13240
  const inCopy = workingTreePaths(cwd);
13162
13241
  for (const rel of inCopy) {
13163
13242
  if (!isOverlay(rel) || baseProtected.has(fold(rel))) continue;
13164
- rmSync6(join18(dest, rel), { force: true });
13243
+ rmSync7(join18(dest, rel), { force: true });
13165
13244
  removed++;
13166
13245
  }
13167
13246
  for (const link of restoredLinks) {
@@ -13248,7 +13327,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
13248
13327
  }
13249
13328
  return { exit: r.exit, secs, diagnostics: r.diagnostics };
13250
13329
  } finally {
13251
- rmSync6(scratch, { recursive: true, force: true });
13330
+ rmSync7(scratch, { recursive: true, force: true });
13252
13331
  }
13253
13332
  }
13254
13333
  function diagnosticsJson(diagnostics, includeTail) {
@@ -13401,7 +13480,7 @@ function runVerify(opts) {
13401
13480
  const visDir = join18(visRoot, "t");
13402
13481
  const cleanup = (dirs) => {
13403
13482
  if (opts.keep) return;
13404
- for (const d of dirs) rmSync6(d, { recursive: true, force: true });
13483
+ for (const d of dirs) rmSync7(d, { recursive: true, force: true });
13405
13484
  };
13406
13485
  filesystemCaseSensitive = (opts.probeCaseSensitivity ?? probeFilesystemCaseSensitivity)(visRoot);
13407
13486
  if (!filesystemCaseSensitive) {
@@ -13767,8 +13846,8 @@ var init_verify = __esm({
13767
13846
 
13768
13847
  // src/cli/run.ts
13769
13848
  import { execFileSync as execFileSync9, spawn, spawnSync as spawnSync4 } from "node:child_process";
13770
- import { randomBytes } from "node:crypto";
13771
- import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync7, statSync as statSync6, writeFileSync as writeFileSync10 } from "node:fs";
13849
+ import { randomBytes as randomBytes2 } from "node:crypto";
13850
+ import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync8, statSync as statSync5, writeFileSync as writeFileSync10 } from "node:fs";
13772
13851
  import { tmpdir as tmpdir5 } from "node:os";
13773
13852
  import { dirname as dirname8, join as join19, resolve as resolve8 } from "node:path";
13774
13853
  function authoritativeRunLifecyclePlatform(platform = process.platform) {
@@ -13824,7 +13903,7 @@ function trustedLinuxPython(candidates = DEFAULT_TRUSTED_PYTHON_CANDIDATES) {
13824
13903
  for (const candidate of candidates) {
13825
13904
  try {
13826
13905
  const real = realpathSync8(candidate);
13827
- const st = statSync6(real);
13906
+ const st = statSync5(real);
13828
13907
  if (!st.isFile() || writableByCaller2(real)) continue;
13829
13908
  const probe = spawnSync4(
13830
13909
  real,
@@ -13947,7 +14026,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
13947
14026
  ...state.failure ? { failure: state.failure } : {}
13948
14027
  };
13949
14028
  } finally {
13950
- rmSync7(stateDir, { recursive: true, force: true });
14029
+ rmSync8(stateDir, { recursive: true, force: true });
13951
14030
  }
13952
14031
  }
13953
14032
  function waitMs(ms) {
@@ -13978,7 +14057,7 @@ function supervisedObserverLog(cwd) {
13978
14057
  return join19(
13979
14058
  gd,
13980
14059
  "tamperward",
13981
- `run-observer-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}.jsonl`
14060
+ `run-observer-${process.pid}-${Date.now()}-${randomBytes2(4).toString("hex")}.jsonl`
13982
14061
  );
13983
14062
  }
13984
14063
  function startSupervisedObserver(cwd, base, entry) {
@@ -15320,7 +15399,7 @@ import {
15320
15399
  readFileSync as readFileSync19,
15321
15400
  readdirSync as readdirSync6,
15322
15401
  realpathSync as realpathSync9,
15323
- rmSync as rmSync8,
15402
+ rmSync as rmSync9,
15324
15403
  symlinkSync as symlinkSync2
15325
15404
  } from "node:fs";
15326
15405
  import { tmpdir as tmpdir6 } from "node:os";
@@ -15518,8 +15597,8 @@ function runOneTrace(cwd, base, command, budget) {
15518
15597
  accesses: rewriteTraceRoot(accesses, root)
15519
15598
  };
15520
15599
  } finally {
15521
- rmSync8(root, { recursive: true, force: true });
15522
- rmSync8(traceDir, { recursive: true, force: true });
15600
+ rmSync9(root, { recursive: true, force: true });
15601
+ rmSync9(traceDir, { recursive: true, force: true });
15523
15602
  }
15524
15603
  }
15525
15604
  function renderText2(report2) {
@@ -15703,8 +15782,7 @@ var init_trace_verify = __esm({
15703
15782
 
15704
15783
  // src/cli/onboard.ts
15705
15784
  import { execFileSync as execFileSync12 } from "node:child_process";
15706
- import { randomBytes as randomBytes2 } from "node:crypto";
15707
- import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as realpathSync10, renameSync as renameSync3, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "node:fs";
15785
+ import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as realpathSync10, rmSync as rmSync10 } from "node:fs";
15708
15786
  import { tmpdir as tmpdir7 } from "node:os";
15709
15787
  import { join as join22, relative as relative7, resolve as resolve11, sep as sep5 } from "node:path";
15710
15788
  import { createInterface } from "node:readline";
@@ -15773,23 +15851,14 @@ function explainVerdict(v, code2) {
15773
15851
  return `Could not verify${v?.reason ? ` (${v.reason})` : ""} \u2014 TamperWard failed closed${v?.detail ? `: ${v.detail}` : "."}`;
15774
15852
  }
15775
15853
  }
15776
- function atomicReplaceFile(path, content, mode) {
15777
- const tmp = `${path}.${process.pid}.${randomBytes2(8).toString("hex")}.tmp`;
15778
- try {
15779
- writeFileSync11(tmp, content, { encoding: "utf8", flag: "wx", mode });
15780
- renameSync3(tmp, path);
15781
- } finally {
15782
- rmSync9(tmp, { force: true });
15783
- }
15784
- }
15785
15854
  function writeVerifyCommand(cwd, command) {
15786
15855
  const path = join22(cwd, POLICY_FILE);
15787
- const st = lstatSync10(path, { throwIfNoEntry: false });
15788
- if (st && (!st.isFile() || st.isSymbolicLink())) {
15856
+ const kind = writeTargetKind(path);
15857
+ if (kind === "symlink" || kind === "irregular") {
15789
15858
  return `${POLICY_FILE} is not a regular file; refusing to follow or replace a symlink or special file`;
15790
15859
  }
15791
- const mode = st ? st.mode & 511 : 420;
15792
- const original = st ? readFileSync20(path, "utf8") : null;
15860
+ const mode = existingMode(path, 420);
15861
+ const original = kind === "file" ? readFileSync20(path, "utf8") : null;
15793
15862
  let next;
15794
15863
  try {
15795
15864
  const doc = parseDocument(original && original.trim() ? original : "version: 1\n");
@@ -15806,7 +15875,7 @@ function writeVerifyCommand(cwd, command) {
15806
15875
  if (loaded2.verify?.command !== command) throw new Error("verify.command did not round-trip");
15807
15876
  return null;
15808
15877
  } catch (e) {
15809
- if (original === null) rmSync9(path, { force: true });
15878
+ if (original === null) rmSync10(path, { force: true });
15810
15879
  else atomicReplaceFile(path, original, mode);
15811
15880
  return `the updated ${POLICY_FILE} does not load (${errorMessage(e)}); restored the previous file`;
15812
15881
  }
@@ -16206,7 +16275,7 @@ function runDemo(cwd, head, runners, out3) {
16206
16275
  } finally {
16207
16276
  git5(cwd, ["worktree", "remove", "--force", wt]);
16208
16277
  git5(cwd, ["worktree", "prune"]);
16209
- rmSync9(wt, { recursive: true, force: true });
16278
+ rmSync10(wt, { recursive: true, force: true });
16210
16279
  }
16211
16280
  const after = treeFingerprint(cwd);
16212
16281
  out3(`working tree fingerprint after: ${after.slice(0, 16)}`);
@@ -16231,6 +16300,7 @@ var init_onboard = __esm({
16231
16300
  init_policy_load();
16232
16301
  init_narrow();
16233
16302
  init_wiring();
16303
+ init_safe_write();
16234
16304
  Aborted = class extends Error {
16235
16305
  };
16236
16306
  SECTIONS = [
@@ -16621,7 +16691,7 @@ var init_record = __esm({
16621
16691
 
16622
16692
  // src/research/run.ts
16623
16693
  import { execFileSync as execFileSync13 } from "node:child_process";
16624
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync22, renameSync as renameSync4, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "node:fs";
16694
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync22, renameSync as renameSync4, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
16625
16695
  import { join as join23, resolve as resolve14 } from "node:path";
16626
16696
  function git6(args, cwd) {
16627
16697
  return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
@@ -16669,12 +16739,12 @@ function resumableRecord(path, expected) {
16669
16739
  }
16670
16740
  function writeRecordAtomically(path, text) {
16671
16741
  const tmp = `${path}.${process.pid}.tmp`;
16672
- writeFileSync12(tmp, text);
16742
+ writeFileSync11(tmp, text);
16673
16743
  renameSync4(tmp, path);
16674
16744
  }
16675
16745
  function freshWorkspace(ledger, task, pair, arm, sourceBase) {
16676
16746
  const ws2 = join23(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
16677
- rmSync10(ws2, { recursive: true, force: true });
16747
+ rmSync11(ws2, { recursive: true, force: true });
16678
16748
  mkdirSync8(join23(ledger, "workspaces"), { recursive: true });
16679
16749
  try {
16680
16750
  execFileSync13("git", ["clone", "-q", "--no-hardlinks", task.repo, ws2], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.23.15",
3
+ "version": "2.23.17",
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",