tamperward 2.29.2 → 2.29.4

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 +86 -45
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1391,10 +1391,10 @@ function relForDisplay(path, cwd) {
1391
1391
  const rel = relative(cwd, path);
1392
1392
  return rel && !rel.startsWith("..") && !isAbsolute4(rel) ? rel : path;
1393
1393
  }
1394
- function applyEdit(content, oldStr, newStr) {
1394
+ function applyEdit(content, oldStr, newStr, replaceAll = false) {
1395
1395
  if (content === null) return newStr;
1396
1396
  if (oldStr !== "" && !content.includes(oldStr)) return content + "\n" + newStr;
1397
- return content.replace(oldStr, () => newStr);
1397
+ return replaceAll ? content.replaceAll(oldStr, () => newStr) : content.replace(oldStr, () => newStr);
1398
1398
  }
1399
1399
  function synthFileChange(displayPath, before, after) {
1400
1400
  if (before === after) return [];
@@ -1446,7 +1446,7 @@ function changesFromClaudeHook(input, cwd, base = cwd) {
1446
1446
  const fp = asStr(ti.file_path);
1447
1447
  if (!fp) return [];
1448
1448
  const before = readDisk(abs(fp));
1449
- const after = applyEdit(before, asStr(ti.old_string), asStr(ti.new_string));
1449
+ const after = applyEdit(before, asStr(ti.old_string), asStr(ti.new_string), ti.replace_all === true);
1450
1450
  return synthFileChange(relForDisplay(abs(fp), cwd), before, after);
1451
1451
  }
1452
1452
  case "MultiEdit": {
@@ -1457,7 +1457,7 @@ function changesFromClaudeHook(input, cwd, base = cwd) {
1457
1457
  const edits = Array.isArray(ti.edits) ? ti.edits : [];
1458
1458
  for (const raw of edits) {
1459
1459
  const ed = isRecord(raw) ? raw : {};
1460
- after = applyEdit(after, asStr(ed.old_string), asStr(ed.new_string));
1460
+ after = applyEdit(after, asStr(ed.old_string), asStr(ed.new_string), ed.replace_all === true);
1461
1461
  }
1462
1462
  return synthFileChange(relForDisplay(abs(fp), cwd), before, after);
1463
1463
  }
@@ -12759,6 +12759,7 @@ async function startHookService(opts) {
12759
12759
  removeQuietly(paths.state);
12760
12760
  const cache3 = opts.cache ?? new SnapshotCache();
12761
12761
  let served = 0;
12762
+ let inFlight = false;
12762
12763
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
12763
12764
  const respond = (sock, body) => {
12764
12765
  try {
@@ -12789,19 +12790,27 @@ async function startHookService(opts) {
12789
12790
  if (!within(root, req2.cwd)) return respond(sock, { refused: "cwd outside the bound repository" });
12790
12791
  const named = payloadCwd(req2.raw);
12791
12792
  if (named !== null && !within(root, named)) return respond(sock, { refused: "payload cwd outside the bound repository" });
12792
- sock.write(JSON.stringify({ v: HOOK_SERVICE_PROTOCOL, version: TW_VERSION, accepted: true }) + "\n", () => {
12793
- let result;
12793
+ if (inFlight) return respond(sock, { refused: "another request is in flight" });
12794
+ inFlight = true;
12795
+ sock.write(JSON.stringify({ v: HOOK_SERVICE_PROTOCOL, version: TW_VERSION, accepted: true }) + "\n");
12796
+ setImmediate(() => {
12794
12797
  try {
12795
- result = withEnv(req2.env, () => req2.kind === "PreToolUse" ? preToolUseFromRaw(req2.raw, req2.cwd) : stopFromRaw(req2.raw, req2.cwd));
12796
- } catch (e) {
12797
- const detail = e instanceof Error ? e.message : String(e);
12798
- const reason = `Tamperward's hook service could not complete the accepted evaluation (${detail}), so it is denied rather than retried concurrently.`;
12799
- const stdout = req2.kind === "PreToolUse" ? JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } }) + "\n" : JSON.stringify({ decision: "block", reason }) + "\n";
12798
+ if (sock.destroyed || sock.writableEnded) return;
12799
+ let result;
12800
+ try {
12801
+ result = withEnv(req2.env, () => req2.kind === "PreToolUse" ? preToolUseFromRaw(req2.raw, req2.cwd) : stopFromRaw(req2.raw, req2.cwd));
12802
+ } catch (e) {
12803
+ const detail = e instanceof Error ? e.message : String(e);
12804
+ const reason = `Tamperward's hook service could not complete the accepted evaluation (${detail}), so it is denied rather than retried concurrently.`;
12805
+ const stdout = req2.kind === "PreToolUse" ? JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } }) + "\n" : JSON.stringify({ decision: "block", reason }) + "\n";
12806
+ served++;
12807
+ return respond(sock, { exitCode: 0, stdout });
12808
+ }
12800
12809
  served++;
12801
- return respond(sock, { exitCode: 0, stdout });
12810
+ respond(sock, { exitCode: result.exitCode, stdout: result.stdout });
12811
+ } finally {
12812
+ inFlight = false;
12802
12813
  }
12803
- served++;
12804
- respond(sock, { exitCode: result.exitCode, stdout: result.stdout });
12805
12814
  });
12806
12815
  };
12807
12816
  const server = createServer((sock) => {
@@ -12860,36 +12869,44 @@ async function startHookService(opts) {
12860
12869
  function sleepSync(ms) {
12861
12870
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
12862
12871
  }
12872
+ async function probeSocket(paths) {
12873
+ if (socketRefusal(paths) !== null) return { answered: false, status: null };
12874
+ const req2 = { v: HOOK_SERVICE_PROTOCOL, version: TW_VERSION, kind: "status", raw: "", cwd: process.cwd(), env: {} };
12875
+ const res = await exchange(paths, req2, 1e3);
12876
+ if (res === null) return { answered: false, status: null };
12877
+ if (isRecord(res) && res.v === HOOK_SERVICE_PROTOCOL && typeof res.pid === "number" && typeof res.root === "string" && typeof res.version === "string") {
12878
+ return { answered: true, status: { pid: res.pid, root: res.root, version: res.version } };
12879
+ }
12880
+ return { answered: true, status: null };
12881
+ }
12882
+ function describeStateFile(state) {
12883
+ return state ? `records pid ${state.pid}, root ${state.root}, tamperward@${state.version}` : "is absent or unreadable";
12884
+ }
12863
12885
  async function stopHookService(paths) {
12886
+ const probe = await probeSocket(paths);
12864
12887
  const state = readServiceState(paths);
12865
- let outcome = "not-running";
12866
- let confirmed = false;
12867
- if (state && state.pid !== process.pid && pidAlive2(state.pid) && state.version === TW_VERSION && socketRefusal(paths) === null) {
12868
- const req2 = {
12869
- v: HOOK_SERVICE_PROTOCOL,
12870
- version: TW_VERSION,
12871
- kind: "status",
12872
- raw: "",
12873
- cwd: process.cwd(),
12874
- env: {}
12875
- };
12876
- const res = await exchange(paths, req2, 1e3);
12877
- confirmed = isRecord(res) && res.v === HOOK_SERVICE_PROTOCOL && res.version === TW_VERSION && res.pid === state.pid && res.root === state.root;
12878
- }
12879
- if (state && confirmed) {
12880
- try {
12881
- process.kill(state.pid, "SIGTERM");
12882
- } catch {
12888
+ if (probe.answered) {
12889
+ const st = probe.status;
12890
+ const agrees = st !== null && state !== null && state.pid === st.pid && state.root === st.root && state.version === st.version;
12891
+ if (agrees && st.pid !== process.pid) {
12892
+ try {
12893
+ process.kill(st.pid, "SIGTERM");
12894
+ } catch {
12895
+ }
12896
+ const gone = () => !pidAlive2(st.pid) || !existsSync11(paths.socket);
12897
+ const until = Date.now() + STOP_WAIT_MS;
12898
+ while (!gone() && Date.now() < until) sleepSync(50);
12899
+ if (!gone()) throw new Error(`hook service pid ${st.pid} did not exit within ${STOP_WAIT_MS / 1e3}s`);
12900
+ removeQuietly(paths.socket);
12901
+ removeQuietly(paths.state);
12902
+ return { outcome: "stopped", pid: st.pid };
12883
12903
  }
12884
- const gone = () => !pidAlive2(state.pid) || !existsSync11(paths.socket);
12885
- const until = Date.now() + STOP_WAIT_MS;
12886
- while (!gone() && Date.now() < until) sleepSync(50);
12887
- if (!gone()) throw new Error(`hook service pid ${state.pid} did not exit within ${STOP_WAIT_MS / 1e3}s`);
12888
- outcome = "stopped";
12904
+ const detail = st ? `a listener answered on ${paths.socket} (pid ${st.pid}, root ${st.root}, tamperward@${st.version}) but the state file ${describeStateFile(state)}` : `a listener answered on ${paths.socket} but did not report a readable status (possibly a different tamperward version); the state file ${describeStateFile(state)}`;
12905
+ return { outcome: "mismatch", pid: st?.pid ?? state?.pid, root: st?.root ?? state?.root, detail };
12889
12906
  }
12890
12907
  removeQuietly(paths.socket);
12891
12908
  removeQuietly(paths.state);
12892
- return outcome;
12909
+ return { outcome: "not-running" };
12893
12910
  }
12894
12911
  function parseStart(args) {
12895
12912
  let dir = process.cwd();
@@ -12928,8 +12945,21 @@ function runHookService(args) {
12928
12945
  return -1;
12929
12946
  }
12930
12947
  if (sub === "stop") {
12931
- return stopHookService(paths).then((outcome) => {
12932
- process.stdout.write(`tamperward hook-service: ${outcome === "stopped" ? "stopped" : "not running"}; ${paths.socket} removed
12948
+ return stopHookService(paths).then((res) => {
12949
+ if (res.outcome === "stopped") {
12950
+ process.stdout.write(`tamperward hook-service: stopped (pid ${res.pid}); ${paths.socket} removed
12951
+ `);
12952
+ return 0;
12953
+ }
12954
+ if (res.outcome === "mismatch") {
12955
+ process.stderr.write(
12956
+ `tamperward hook-service: NOT stopped \u2014 ${res.detail}. The socket was left intact; a live listener is never orphaned. ` + (res.pid ? `If you mean to stop it, signal it directly: kill ${res.pid}.
12957
+ ` : `Inspect the process holding ${paths.socket} to stop it.
12958
+ `)
12959
+ );
12960
+ return 1;
12961
+ }
12962
+ process.stdout.write(`tamperward hook-service: not running; ${paths.socket} removed
12933
12963
  `);
12934
12964
  return 0;
12935
12965
  });
@@ -12937,22 +12967,33 @@ function runHookService(args) {
12937
12967
  const state = readServiceState(paths);
12938
12968
  const refusal = socketRefusal(paths);
12939
12969
  const optIn = hookServiceEnabled() ? `${HOOK_SERVICE_ENV}=1 (hooks consult the service)` : `${HOOK_SERVICE_ENV} unset (hooks run in-process)`;
12940
- if (!state || !pidAlive2(state.pid) || refusal !== null) {
12941
- process.stdout.write(`tamperward hook-service: not running (${refusal ?? "no live pid"}); ${optIn}
12970
+ if (refusal !== null) {
12971
+ process.stdout.write(`tamperward hook-service: not running (${refusal}); ${optIn}
12942
12972
  `);
12943
12973
  return 0;
12944
12974
  }
12945
12975
  const req2 = { v: HOOK_SERVICE_PROTOCOL, version: TW_VERSION, kind: "status", raw: "", cwd: process.cwd(), env: {} };
12946
12976
  void exchange(paths, req2, 5e3).then((res) => {
12947
- if (!isRecord(res) || typeof res.served !== "number") {
12948
- process.stdout.write(`tamperward hook-service: not running (pid ${state.pid} did not answer, or is another version); ${optIn}
12977
+ if (res === null) {
12978
+ process.stdout.write(`tamperward hook-service: not running (socket did not connect); ${optIn}
12949
12979
  `);
12950
12980
  exitAfterFlush(0);
12951
12981
  return;
12952
12982
  }
12983
+ if (!isRecord(res) || typeof res.served !== "number" || typeof res.pid !== "number" || typeof res.root !== "string" || typeof res.version !== "string") {
12984
+ process.stdout.write(
12985
+ `tamperward hook-service: a listener answered on ${paths.socket} but did not report a readable status (possibly a different tamperward version); the state file ${state ? `records pid ${state.pid}, tamperward@${state.version}` : "is absent or unreadable"}; ${optIn}
12986
+ `
12987
+ );
12988
+ exitAfterFlush(0);
12989
+ return;
12990
+ }
12953
12991
  const cache3 = isRecord(res.cache) ? res.cache : {};
12992
+ const startedAt = typeof res.started_at === "string" ? res.started_at : state?.started_at ?? "unknown";
12993
+ const disagrees = !state || state.pid !== res.pid || state.root !== res.root || state.version !== res.version;
12994
+ const warn = disagrees ? `WARNING: the state file ${describeStateFile(state)}, which disagrees with the live listener; ` : "";
12954
12995
  process.stdout.write(
12955
- `tamperward hook-service: running (pid ${state.pid}, tamperward@${state.version}, root ${state.root}, since ${state.started_at}); served: ${res.served}; snapshot cache hits/misses/entries: ${String(cache3.hits ?? 0)}/${String(cache3.misses ?? 0)}/${String(cache3.size ?? 0)}; ${optIn}
12996
+ `tamperward hook-service: running (pid ${res.pid}, tamperward@${res.version}, root ${res.root}, since ${startedAt}); served: ${res.served}; snapshot cache hits/misses/entries: ${String(cache3.hits ?? 0)}/${String(cache3.misses ?? 0)}/${String(cache3.size ?? 0)}; ${warn}${optIn}
12956
12997
  `
12957
12998
  );
12958
12999
  exitAfterFlush(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.29.2",
3
+ "version": "2.29.4",
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",