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.
- package/dist/cli/index.js +143 -33
- 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
|
|
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
|
-
|
|
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
|
-
|
|
18052
|
-
|
|
18053
|
-
|
|
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
|
|
18154
|
-
|
|
18155
|
-
|
|
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
|
|
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
|
-
|
|
18272
|
-
|
|
18273
|
-
const
|
|
18274
|
-
|
|
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
|
|
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" ?
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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.
|
|
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",
|