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.
- package/dist/cli/index.js +86 -45
- 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
|
-
|
|
12793
|
-
|
|
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
|
-
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
12871
|
-
|
|
12872
|
-
|
|
12873
|
-
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
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
|
|
12885
|
-
|
|
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((
|
|
12932
|
-
|
|
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 (
|
|
12941
|
-
process.stdout.write(`tamperward hook-service: not running (${refusal
|
|
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 (
|
|
12948
|
-
process.stdout.write(`tamperward hook-service: not running (
|
|
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 ${
|
|
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.
|
|
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",
|