tamperward 2.13.2 → 2.14.0
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/README.md +18 -2
- package/dist/cli/index.js +354 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,7 +149,23 @@ weakening operations, detect protected-tree drift, and sweep the turn's net
|
|
|
149
149
|
changes. The `Stop` hook runs that sweep — it does **not** invoke `verify`.
|
|
150
150
|
`tamperward watch` can additionally record transient mutations, but it is an
|
|
151
151
|
optional observer that `init` neither starts nor supervises, and it is not an
|
|
152
|
-
enforcement boundary.
|
|
152
|
+
enforcement boundary. Since **2.13.3** the observer writes a health sidecar next to
|
|
153
|
+
its JSONL event log, recording backend, PID/start time, watched-directory count,
|
|
154
|
+
successful appends, dropped events and errors. `tamperward doctor` reports that
|
|
155
|
+
channel as **healthy**, **degraded**, or **unavailable**, and Stop writes the same
|
|
156
|
+
non-authoritative state into the audit log when `TAMPERWARD_DENYLOG` is enabled.
|
|
157
|
+
This distinguishes “healthy observer, zero events” from “observer telemetry was
|
|
158
|
+
unavailable” without turning absence of watcher telemetry into an enforcement pass
|
|
159
|
+
or failure.
|
|
160
|
+
|
|
161
|
+
Since **2.14.0**, `tamperward run --observe-transients -- <agent...>` can supervise
|
|
162
|
+
that observer as part of the envelope lifecycle. It creates a unique session log under
|
|
163
|
+
the repository git directory, pins the watcher's protected-path policy to the same
|
|
164
|
+
trusted base used by the envelope, waits for observer health before launching the
|
|
165
|
+
agent, consumes the temporal events itself after the agent exits, and shuts the
|
|
166
|
+
observer down deterministically. Observer health remains advisory; transient findings
|
|
167
|
+
remain warnings by default and affect the envelope only when the operator explicitly
|
|
168
|
+
sets `TAMPERWARD_TRANSIENT=block`.
|
|
153
169
|
|
|
154
170
|
After the runtime exits its exit code is treated as untrusted, and the envelope
|
|
155
171
|
checks that post-agent `HEAD` still descends from the entry commit; the committed
|
|
@@ -349,7 +365,7 @@ option can never be reinterpreted as the agent command.
|
|
|
349
365
|
| `check` | one view — `--staged` · `--worktree` · `--diff <base>...<head>` — plus `--format text\|json\|github\|auto` (default `auto`) · `--json` (alias for `--format json`) · `--cwd <dir>` |
|
|
350
366
|
| `verify` | `--base <rev>` (default `HEAD`) · `--cmd <suite command>` · `--budget <seconds>` · `--json` · `--keep` (keep the two materialised copies and report their paths) · `--require-ancestor` (refuse a base that is not an ancestor of `HEAD`) · `--cwd <dir>` |
|
|
351
367
|
| `doctor` | `--base <rev>` (trusted policy revision) · `--workflow <path>` (default `.github/workflows/tamperward.yml`) · `--cwd <dir>` — validates every job that runs `tamperward verify` has a static outer timeout covering visible + pristine + authority reserve |
|
|
352
|
-
| `run` | `--base <rev>` · `--cmd <suite command>` · `--budget <seconds>` (per verifier suite) · `--agent-budget <seconds>` (optional wrapped-agent wall clock) · `--allow-dirty` · `--settle <seconds>` (wait before the final quiescence check) · `--allow-dep-drift` · `--cwd <dir>` · then `-- <agent command...>` |
|
|
368
|
+
| `run` | `--base <rev>` · `--cmd <suite command>` · `--budget <seconds>` (per verifier suite) · `--agent-budget <seconds>` (optional wrapped-agent wall clock) · `--observe-transients` (start a session-scoped transient observer) · `--allow-dirty` · `--settle <seconds>` (wait before the final quiescence check) · `--allow-dep-drift` · `--cwd <dir>` · then `-- <agent command...>` |
|
|
353
369
|
| `allow` | `<rule>` · `--file <path>` · `--reason "<why>"` (required) · `--cwd <dir>` |
|
|
354
370
|
| `init` | `--cwd <dir>` · `--dry-run` · `--force-workflow` |
|
|
355
371
|
| `watch` | `--dir <dir>` · `--log <file>` — a daemon; it runs until signalled |
|
package/dist/cli/index.js
CHANGED
|
@@ -6828,7 +6828,7 @@ function runCheck(opts) {
|
|
|
6828
6828
|
}
|
|
6829
6829
|
|
|
6830
6830
|
// src/cli/hook.ts
|
|
6831
|
-
import { readFileSync as readFileSync11, appendFileSync as appendFileSync4, writeFileSync as
|
|
6831
|
+
import { readFileSync as readFileSync11, appendFileSync as appendFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "node:fs";
|
|
6832
6832
|
import { createHash as createHash5 } from "node:crypto";
|
|
6833
6833
|
|
|
6834
6834
|
// src/adapters/claude/deny.ts
|
|
@@ -7025,7 +7025,7 @@ function dropTurnTree(cwd, sessionId) {
|
|
|
7025
7025
|
}
|
|
7026
7026
|
|
|
7027
7027
|
// src/cli/watch.ts
|
|
7028
|
-
import { appendFileSync as appendFileSync3, lstatSync as lstatSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, watch } from "node:fs";
|
|
7028
|
+
import { appendFileSync as appendFileSync3, lstatSync as lstatSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync9, readdirSync as readdirSync2, watch, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7029
7029
|
import { join as join11 } from "node:path";
|
|
7030
7030
|
import { createHash as createHash4 } from "node:crypto";
|
|
7031
7031
|
var SKIP2 = /(^|\/)(\.git|node_modules)(\/|$)/;
|
|
@@ -7040,17 +7040,51 @@ function defaultEventLog(cwd) {
|
|
|
7040
7040
|
if (process.env.TAMPERWARD_FSEVENTS) return process.env.TAMPERWARD_FSEVENTS;
|
|
7041
7041
|
return join11(gitDir(cwd) ?? join11(cwd, ".git"), "tamperward", "fsevents.jsonl");
|
|
7042
7042
|
}
|
|
7043
|
-
function
|
|
7043
|
+
function watcherHealthPath(log) {
|
|
7044
|
+
return `${log}.health.json`;
|
|
7045
|
+
}
|
|
7046
|
+
function readWatcherHealth(log) {
|
|
7047
|
+
try {
|
|
7048
|
+
const value = JSON.parse(readFileSync9(watcherHealthPath(log), "utf8"));
|
|
7049
|
+
if (value.version !== 1 || typeof value.pid !== "number" || typeof value.started_at !== "string" || !["healthy", "degraded", "stopped"].includes(String(value.state))) return null;
|
|
7050
|
+
return value;
|
|
7051
|
+
} catch {
|
|
7052
|
+
return null;
|
|
7053
|
+
}
|
|
7054
|
+
}
|
|
7055
|
+
function pidAlive(pid) {
|
|
7056
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
7057
|
+
try {
|
|
7058
|
+
process.kill(pid, 0);
|
|
7059
|
+
return true;
|
|
7060
|
+
} catch (e) {
|
|
7061
|
+
return e.code === "EPERM";
|
|
7062
|
+
}
|
|
7063
|
+
}
|
|
7064
|
+
function watcherTelemetry(log) {
|
|
7065
|
+
const health = readWatcherHealth(log);
|
|
7066
|
+
if (!health) return { state: "unavailable", health: null, reason: "no health record" };
|
|
7067
|
+
if (health.state === "stopped")
|
|
7068
|
+
return { state: "unavailable", health, reason: "observer stopped" };
|
|
7069
|
+
if (!pidAlive(health.pid))
|
|
7070
|
+
return { state: "unavailable", health, reason: `observer pid ${health.pid} is not running` };
|
|
7071
|
+
if (health.state === "degraded")
|
|
7072
|
+
return { state: "degraded", health, reason: health.last_error ?? "observer reported an error" };
|
|
7073
|
+
return { state: "healthy", health };
|
|
7074
|
+
}
|
|
7075
|
+
function watchTree(dir, cb, onState, onError) {
|
|
7044
7076
|
if (process.env.TAMPERWARD_WATCH_NO_RECURSIVE !== "1") {
|
|
7045
7077
|
try {
|
|
7046
7078
|
const w = watch(dir, { recursive: true }, (kind, fname) => {
|
|
7047
7079
|
if (fname) cb(kind, String(fname));
|
|
7048
7080
|
});
|
|
7081
|
+
onState("recursive", 1);
|
|
7049
7082
|
return { close: () => w.close() };
|
|
7050
7083
|
} catch {
|
|
7051
7084
|
}
|
|
7052
7085
|
}
|
|
7053
7086
|
const watchers = /* @__PURE__ */ new Map();
|
|
7087
|
+
onState("fallback", 0);
|
|
7054
7088
|
const addDir = (rel) => {
|
|
7055
7089
|
if (watchers.has(rel) || SKIP2.test(rel + "/")) return;
|
|
7056
7090
|
let w;
|
|
@@ -7064,17 +7098,24 @@ function watchTree(dir, cb) {
|
|
|
7064
7098
|
} catch {
|
|
7065
7099
|
}
|
|
7066
7100
|
});
|
|
7067
|
-
} catch {
|
|
7101
|
+
} catch (e) {
|
|
7102
|
+
onError(
|
|
7103
|
+
`watch directory ${rel || "."}: ${e instanceof Error ? e.message : String(e)}`
|
|
7104
|
+
);
|
|
7068
7105
|
return;
|
|
7069
7106
|
}
|
|
7070
7107
|
watchers.set(rel, w);
|
|
7108
|
+
onState("fallback", watchers.size);
|
|
7071
7109
|
};
|
|
7072
7110
|
const walk2 = (rel) => {
|
|
7073
7111
|
addDir(rel);
|
|
7074
7112
|
let names = [];
|
|
7075
7113
|
try {
|
|
7076
7114
|
names = readdirSync2(rel ? join11(dir, rel) : dir);
|
|
7077
|
-
} catch {
|
|
7115
|
+
} catch (e) {
|
|
7116
|
+
onError(
|
|
7117
|
+
`enumerate directory ${rel || "."}: ${e instanceof Error ? e.message : String(e)}`
|
|
7118
|
+
);
|
|
7078
7119
|
return;
|
|
7079
7120
|
}
|
|
7080
7121
|
for (const name of names) {
|
|
@@ -7096,32 +7137,117 @@ function startWatcher(dir, log, policy) {
|
|
|
7096
7137
|
mkdirSync4(join11(log, ".."), { recursive: true });
|
|
7097
7138
|
} catch {
|
|
7098
7139
|
}
|
|
7099
|
-
const
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7140
|
+
const healthPath = watcherHealthPath(log);
|
|
7141
|
+
const health = {
|
|
7142
|
+
version: 1,
|
|
7143
|
+
state: "healthy",
|
|
7144
|
+
backend: "initializing",
|
|
7145
|
+
pid: process.pid,
|
|
7146
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7147
|
+
stopped_at: null,
|
|
7148
|
+
watched_dirs: 0,
|
|
7149
|
+
last_append_at: null,
|
|
7150
|
+
event_count: 0,
|
|
7151
|
+
dropped_events: 0,
|
|
7152
|
+
error_count: 0,
|
|
7153
|
+
last_error: null,
|
|
7154
|
+
log
|
|
7155
|
+
};
|
|
7156
|
+
let healthWriteWarningEmitted = false;
|
|
7157
|
+
const persistHealth = () => {
|
|
7107
7158
|
try {
|
|
7108
|
-
|
|
7109
|
-
|
|
7159
|
+
writeFileSync4(healthPath, JSON.stringify(health) + "\n", { mode: 384 });
|
|
7160
|
+
healthWriteWarningEmitted = false;
|
|
7161
|
+
} catch (e) {
|
|
7162
|
+
if (!healthWriteWarningEmitted) {
|
|
7163
|
+
process.stderr.write(
|
|
7164
|
+
`tamperward watch: WARNING: observer health cannot be recorded at ${healthPath} (${e instanceof Error ? e.message : String(e)}); telemetry availability is unknown.
|
|
7165
|
+
`
|
|
7166
|
+
);
|
|
7167
|
+
healthWriteWarningEmitted = true;
|
|
7168
|
+
}
|
|
7110
7169
|
}
|
|
7111
|
-
}
|
|
7170
|
+
};
|
|
7171
|
+
const degrade = (detail, droppedEvent = false) => {
|
|
7172
|
+
health.state = "degraded";
|
|
7173
|
+
health.error_count++;
|
|
7174
|
+
if (droppedEvent) health.dropped_events++;
|
|
7175
|
+
health.last_error = detail;
|
|
7176
|
+
persistHealth();
|
|
7177
|
+
process.stderr.write(
|
|
7178
|
+
`tamperward watch: WARNING: observer degraded \u2014 ${detail}. Transient telemetry may be incomplete.
|
|
7179
|
+
`
|
|
7180
|
+
);
|
|
7181
|
+
};
|
|
7182
|
+
persistHealth();
|
|
7183
|
+
const last = /* @__PURE__ */ new Map();
|
|
7184
|
+
const tree = watchTree(
|
|
7185
|
+
dir,
|
|
7186
|
+
(kind, rel) => {
|
|
7187
|
+
if (SKIP2.test(rel) || !isProtected(rel, policy)) return;
|
|
7188
|
+
const s = snap(join11(dir, rel));
|
|
7189
|
+
const key2 = `${s.hash}:${s.mode}`;
|
|
7190
|
+
if (last.get(rel) === key2) return;
|
|
7191
|
+
last.set(rel, key2);
|
|
7192
|
+
const ev = {
|
|
7193
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7194
|
+
path: rel,
|
|
7195
|
+
kind: kind === "rename" ? "rename" : "change",
|
|
7196
|
+
...s
|
|
7197
|
+
};
|
|
7198
|
+
try {
|
|
7199
|
+
appendFileSync3(log, JSON.stringify(ev) + "\n");
|
|
7200
|
+
health.event_count++;
|
|
7201
|
+
health.last_append_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
7202
|
+
persistHealth();
|
|
7203
|
+
} catch (e) {
|
|
7204
|
+
degrade(
|
|
7205
|
+
`append event to ${log}: ${e instanceof Error ? e.message : String(e)}`,
|
|
7206
|
+
true
|
|
7207
|
+
);
|
|
7208
|
+
}
|
|
7209
|
+
},
|
|
7210
|
+
(backend, watchedDirs) => {
|
|
7211
|
+
health.backend = backend;
|
|
7212
|
+
health.watched_dirs = watchedDirs;
|
|
7213
|
+
persistHealth();
|
|
7214
|
+
},
|
|
7215
|
+
(detail) => degrade(detail)
|
|
7216
|
+
);
|
|
7217
|
+
return {
|
|
7218
|
+
close: () => {
|
|
7219
|
+
tree.close();
|
|
7220
|
+
health.state = "stopped";
|
|
7221
|
+
health.stopped_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
7222
|
+
persistHealth();
|
|
7223
|
+
}
|
|
7224
|
+
};
|
|
7112
7225
|
}
|
|
7113
7226
|
function runWatch(args) {
|
|
7114
7227
|
let dir = process.cwd();
|
|
7115
7228
|
let log = null;
|
|
7229
|
+
let base = null;
|
|
7116
7230
|
for (let i = 0; i < args.length; i++) {
|
|
7117
7231
|
if (args[i] === "--dir" && args[i + 1]) dir = args[++i];
|
|
7118
7232
|
else if (args[i] === "--log" && args[i + 1]) log = args[++i];
|
|
7233
|
+
else if (args[i] === "--base" && args[i + 1]) base = args[++i];
|
|
7119
7234
|
}
|
|
7120
|
-
const policy = loadPolicy(dir);
|
|
7235
|
+
const policy = base ? loadPolicyAt(base, dir) ?? defaultPolicy() : loadPolicy(dir);
|
|
7121
7236
|
const out2 = log ?? defaultEventLog(dir);
|
|
7122
|
-
startWatcher(dir, out2, policy);
|
|
7123
|
-
|
|
7124
|
-
|
|
7237
|
+
const watcher = startWatcher(dir, out2, policy);
|
|
7238
|
+
let closing = false;
|
|
7239
|
+
const shutdown = () => {
|
|
7240
|
+
if (closing) return;
|
|
7241
|
+
closing = true;
|
|
7242
|
+
watcher.close();
|
|
7243
|
+
process.exit(0);
|
|
7244
|
+
};
|
|
7245
|
+
process.once("SIGTERM", shutdown);
|
|
7246
|
+
process.once("SIGINT", shutdown);
|
|
7247
|
+
process.stdout.write(
|
|
7248
|
+
`tamperward watch: recording protected-file events under ${dir} -> ${out2} (health: ${watcherHealthPath(out2)}${base ? `; trusted base: ${base}` : ""})
|
|
7249
|
+
`
|
|
7250
|
+
);
|
|
7125
7251
|
return -1;
|
|
7126
7252
|
}
|
|
7127
7253
|
|
|
@@ -7441,6 +7567,10 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7441
7567
|
const none = { blocks: [], commit: () => {
|
|
7442
7568
|
} };
|
|
7443
7569
|
const log = defaultEventLog(cwd);
|
|
7570
|
+
const telemetry = watcherTelemetry(log);
|
|
7571
|
+
if (telemetry.state !== "healthy") {
|
|
7572
|
+
recordObserverHealth(telemetry.state, telemetry.reason);
|
|
7573
|
+
}
|
|
7444
7574
|
const cp = cursorPath(cwd, sessionId);
|
|
7445
7575
|
if (!existsSync8(log)) return none;
|
|
7446
7576
|
let offset = 0;
|
|
@@ -7451,7 +7581,7 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7451
7581
|
const { events, newOffset } = readEvents(log, offset);
|
|
7452
7582
|
if (events.length === 0) return { ...none, commit: () => {
|
|
7453
7583
|
if (cp) try {
|
|
7454
|
-
|
|
7584
|
+
writeFileSync5(cp, String(newOffset));
|
|
7455
7585
|
} catch {
|
|
7456
7586
|
}
|
|
7457
7587
|
} };
|
|
@@ -7466,7 +7596,7 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7466
7596
|
blocks: findings.filter((f) => f.severity === "block"),
|
|
7467
7597
|
commit: () => {
|
|
7468
7598
|
if (cp) try {
|
|
7469
|
-
|
|
7599
|
+
writeFileSync5(cp, String(newOffset));
|
|
7470
7600
|
} catch {
|
|
7471
7601
|
}
|
|
7472
7602
|
}
|
|
@@ -7480,6 +7610,19 @@ function recordWarns(warns) {
|
|
|
7480
7610
|
} catch {
|
|
7481
7611
|
}
|
|
7482
7612
|
}
|
|
7613
|
+
function recordObserverHealth(state, reason) {
|
|
7614
|
+
const log = process.env.TAMPERWARD_DENYLOG;
|
|
7615
|
+
if (!log) return;
|
|
7616
|
+
const detail = (reason ?? "").replace(/[\r\n]+/g, " ").trim();
|
|
7617
|
+
try {
|
|
7618
|
+
appendFileSync4(
|
|
7619
|
+
log,
|
|
7620
|
+
`warn:transient-observer:${state}${detail ? ":" + detail : ""}
|
|
7621
|
+
`
|
|
7622
|
+
);
|
|
7623
|
+
} catch {
|
|
7624
|
+
}
|
|
7625
|
+
}
|
|
7483
7626
|
function stopVerdict(input) {
|
|
7484
7627
|
if (input.stop_hook_active) return { exitCode: 0, stdout: "" };
|
|
7485
7628
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -7598,7 +7741,7 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
7598
7741
|
// src/cli/init.ts
|
|
7599
7742
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7600
7743
|
import { createHash as createHash6 } from "node:crypto";
|
|
7601
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as
|
|
7744
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6, chmodSync, statSync as statSync2 } from "node:fs";
|
|
7602
7745
|
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join12, relative as relative3, resolve as resolve3 } from "node:path";
|
|
7603
7746
|
|
|
7604
7747
|
// src/verifier-limits.ts
|
|
@@ -7832,7 +7975,7 @@ function planPolicy(cwd) {
|
|
|
7832
7975
|
path: POLICY_FILE,
|
|
7833
7976
|
status: "create",
|
|
7834
7977
|
detail: "baseline policy with commented overrides",
|
|
7835
|
-
apply: () =>
|
|
7978
|
+
apply: () => writeFileSync6(path, POLICY_CONTENT)
|
|
7836
7979
|
};
|
|
7837
7980
|
}
|
|
7838
7981
|
function planClaudeHooks(cwd) {
|
|
@@ -7904,7 +8047,7 @@ function planClaudeHooks(cwd) {
|
|
|
7904
8047
|
}
|
|
7905
8048
|
if (needDisableFalse) settings.disableAllHooks = false;
|
|
7906
8049
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
7907
|
-
|
|
8050
|
+
writeFileSync6(path, JSON.stringify(settings, null, 2) + "\n");
|
|
7908
8051
|
}
|
|
7909
8052
|
};
|
|
7910
8053
|
}
|
|
@@ -7997,7 +8140,7 @@ function planPreCommit(cwd) {
|
|
|
7997
8140
|
detail: `create via ${note}`,
|
|
7998
8141
|
apply: () => {
|
|
7999
8142
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
8000
|
-
|
|
8143
|
+
writeFileSync6(path, `#!/bin/sh
|
|
8001
8144
|
${block.join("\n")}
|
|
8002
8145
|
`);
|
|
8003
8146
|
chmodSync(path, 493);
|
|
@@ -8012,7 +8155,7 @@ ${block.join("\n")}
|
|
|
8012
8155
|
const dead = unconditionalExitAt(lines);
|
|
8013
8156
|
const at = lines.findIndex((l) => PRECOMMIT_RE.test(l));
|
|
8014
8157
|
const write = (out2) => {
|
|
8015
|
-
|
|
8158
|
+
writeFileSync6(path, out2.join("\n"));
|
|
8016
8159
|
chmodSync(path, 493);
|
|
8017
8160
|
};
|
|
8018
8161
|
if (at !== -1 && (dead === -1 || at < dead)) {
|
|
@@ -8107,7 +8250,7 @@ ${CODEOWNERS_MARK}
|
|
|
8107
8250
|
} : {},
|
|
8108
8251
|
apply: () => {
|
|
8109
8252
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
8110
|
-
|
|
8253
|
+
writeFileSync6(path, existing === null ? block.replace(/^\n/, "") : existing.replace(/\n?$/, "\n") + block);
|
|
8111
8254
|
}
|
|
8112
8255
|
};
|
|
8113
8256
|
}
|
|
@@ -8116,7 +8259,7 @@ function planWorkflow(cwd, force) {
|
|
|
8116
8259
|
const path = join12(cwd, rel);
|
|
8117
8260
|
const write = () => {
|
|
8118
8261
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
8119
|
-
|
|
8262
|
+
writeFileSync6(path, workflowFile(WORKFLOW_CONTENT));
|
|
8120
8263
|
};
|
|
8121
8264
|
if (!existsSync9(path)) {
|
|
8122
8265
|
return {
|
|
@@ -8529,13 +8672,30 @@ function runDoctor(opts = {}) {
|
|
|
8529
8672
|
"tamperward doctor: GitHub repository authority OK \u2014 " + github.repo + "#" + github.branch + " requires tamperward, Code Owner review, and stale-review dismissal on new pushes.\n"
|
|
8530
8673
|
);
|
|
8531
8674
|
}
|
|
8675
|
+
const observer = watcherTelemetry(defaultEventLog(cwd));
|
|
8676
|
+
if (observer.state === "healthy" && observer.health) {
|
|
8677
|
+
process.stdout.write(
|
|
8678
|
+
`tamperward doctor: transient observer: healthy \u2014 ${observer.health.backend}, ${observer.health.watched_dirs} watched dir(s), ${observer.health.event_count} event(s); advisory telemetry only, not verification authority.
|
|
8679
|
+
`
|
|
8680
|
+
);
|
|
8681
|
+
} else if (observer.state === "degraded" && observer.health) {
|
|
8682
|
+
process.stdout.write(
|
|
8683
|
+
`tamperward doctor: transient observer: degraded \u2014 ${observer.health.error_count} error(s), ${observer.health.dropped_events} dropped event(s); ${observer.reason ?? "telemetry may be incomplete"}. Advisory only; zero events are not evidence of no transient activity.
|
|
8684
|
+
`
|
|
8685
|
+
);
|
|
8686
|
+
} else {
|
|
8687
|
+
process.stdout.write(
|
|
8688
|
+
`tamperward doctor: transient observer: unavailable${observer.reason ? " \u2014 " + observer.reason : ""}. Observer telemetry is optional/advisory; zero events are not evidence of no transient activity.
|
|
8689
|
+
`
|
|
8690
|
+
);
|
|
8691
|
+
}
|
|
8532
8692
|
return 0;
|
|
8533
8693
|
}
|
|
8534
8694
|
|
|
8535
8695
|
// src/cli/verify.ts
|
|
8536
8696
|
import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
|
|
8537
8697
|
import { createHash as createHash10 } from "node:crypto";
|
|
8538
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync5, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as
|
|
8698
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync5, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync3, symlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
8539
8699
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8540
8700
|
import { dirname as dirname7, isAbsolute as isAbsolute10, join as join16, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
|
|
8541
8701
|
|
|
@@ -9655,7 +9815,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
9655
9815
|
safeSymlink(target, out2, dest, `the base's ${e.path}`);
|
|
9656
9816
|
restoredLinks.push({ path: e.path, out: out2, target });
|
|
9657
9817
|
} else {
|
|
9658
|
-
|
|
9818
|
+
writeFileSync7(out2, content);
|
|
9659
9819
|
chmodSync2(out2, parseInt(e.mode.slice(-4), 8) & 511);
|
|
9660
9820
|
}
|
|
9661
9821
|
restored.push(e.path);
|
|
@@ -9711,7 +9871,7 @@ function suiteEnv(scratch) {
|
|
|
9711
9871
|
}
|
|
9712
9872
|
const userRc = join16(scratch, "npmrc-user");
|
|
9713
9873
|
const globalRc = join16(scratch, "npmrc-global");
|
|
9714
|
-
for (const f of [userRc, globalRc])
|
|
9874
|
+
for (const f of [userRc, globalRc]) writeFileSync7(f, "", { mode: 292 });
|
|
9715
9875
|
env.npm_config_userconfig = userRc;
|
|
9716
9876
|
env.npm_config_globalconfig = globalRc;
|
|
9717
9877
|
env.npm_config_node_options = " ";
|
|
@@ -10035,7 +10195,8 @@ function parseVerify(args) {
|
|
|
10035
10195
|
}
|
|
10036
10196
|
|
|
10037
10197
|
// src/cli/run.ts
|
|
10038
|
-
import { execFileSync as execFileSync10, spawnSync as spawnSync3 } from "node:child_process";
|
|
10198
|
+
import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync3 } from "node:child_process";
|
|
10199
|
+
import { randomBytes } from "node:crypto";
|
|
10039
10200
|
import { mkdtempSync as mkdtempSync3, readdirSync as readdirSync5, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync6, rmSync as rmSync4 } from "node:fs";
|
|
10040
10201
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
10041
10202
|
import { join as join17, resolve as resolve8 } from "node:path";
|
|
@@ -10257,6 +10418,124 @@ function runAgentWithBudget(argv2, cwd, budgetSecs) {
|
|
|
10257
10418
|
rmSync4(stateDir, { recursive: true, force: true });
|
|
10258
10419
|
}
|
|
10259
10420
|
}
|
|
10421
|
+
var waitCell = new Int32Array(new SharedArrayBuffer(4));
|
|
10422
|
+
function waitMs(ms) {
|
|
10423
|
+
Atomics.wait(waitCell, 0, 0, ms);
|
|
10424
|
+
}
|
|
10425
|
+
function pidAlive2(pid) {
|
|
10426
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) return false;
|
|
10427
|
+
try {
|
|
10428
|
+
process.kill(pid, 0);
|
|
10429
|
+
return true;
|
|
10430
|
+
} catch (e) {
|
|
10431
|
+
return e.code === "EPERM";
|
|
10432
|
+
}
|
|
10433
|
+
}
|
|
10434
|
+
function supervisedObserverLog(cwd) {
|
|
10435
|
+
const gd = gitDir(cwd) ?? join17(cwd, ".git");
|
|
10436
|
+
return join17(
|
|
10437
|
+
gd,
|
|
10438
|
+
"tamperward",
|
|
10439
|
+
`run-observer-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}.jsonl`
|
|
10440
|
+
);
|
|
10441
|
+
}
|
|
10442
|
+
function startSupervisedObserver(cwd, base, entry) {
|
|
10443
|
+
const log = supervisedObserverLog(cwd);
|
|
10444
|
+
const cliEntry = entry ?? process.argv[1];
|
|
10445
|
+
if (!cliEntry) return { log, pid: null, finished: false };
|
|
10446
|
+
let child;
|
|
10447
|
+
try {
|
|
10448
|
+
child = spawn(
|
|
10449
|
+
process.execPath,
|
|
10450
|
+
[resolve8(cliEntry), "watch", "--dir", cwd, "--log", log, "--base", base],
|
|
10451
|
+
{
|
|
10452
|
+
cwd,
|
|
10453
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
10454
|
+
env: process.env
|
|
10455
|
+
}
|
|
10456
|
+
);
|
|
10457
|
+
} catch {
|
|
10458
|
+
return { log, pid: null, finished: false };
|
|
10459
|
+
}
|
|
10460
|
+
const pid = child.pid ?? null;
|
|
10461
|
+
const deadline = Date.now() + 3e3;
|
|
10462
|
+
while (Date.now() < deadline) {
|
|
10463
|
+
const telemetry = watcherTelemetry(log);
|
|
10464
|
+
if (telemetry.state === "healthy" || telemetry.state === "degraded") break;
|
|
10465
|
+
if (!pidAlive2(pid)) break;
|
|
10466
|
+
waitMs(25);
|
|
10467
|
+
}
|
|
10468
|
+
return { log, pid, finished: false };
|
|
10469
|
+
}
|
|
10470
|
+
function stopObserverProcess(observer) {
|
|
10471
|
+
waitMs(100);
|
|
10472
|
+
const beforeStop = watcherTelemetry(observer.log);
|
|
10473
|
+
const pid = observer.pid;
|
|
10474
|
+
if (pidAlive2(pid)) {
|
|
10475
|
+
try {
|
|
10476
|
+
process.kill(pid, "SIGTERM");
|
|
10477
|
+
} catch {
|
|
10478
|
+
}
|
|
10479
|
+
const deadline = Date.now() + 2e3;
|
|
10480
|
+
while (Date.now() < deadline) {
|
|
10481
|
+
const health = watcherTelemetry(observer.log);
|
|
10482
|
+
if (health.health?.state === "stopped" || !pidAlive2(pid)) break;
|
|
10483
|
+
waitMs(25);
|
|
10484
|
+
}
|
|
10485
|
+
if (pidAlive2(pid)) {
|
|
10486
|
+
try {
|
|
10487
|
+
process.kill(pid, "SIGKILL");
|
|
10488
|
+
} catch {
|
|
10489
|
+
}
|
|
10490
|
+
}
|
|
10491
|
+
}
|
|
10492
|
+
observer.finished = true;
|
|
10493
|
+
return beforeStop;
|
|
10494
|
+
}
|
|
10495
|
+
function observerSummary(observer, telemetry) {
|
|
10496
|
+
const h = telemetry.health;
|
|
10497
|
+
const counts = h ? `${h.event_count} event(s), ${h.dropped_events} dropped, ${h.error_count} error(s)` : "no valid health record";
|
|
10498
|
+
const reason = telemetry.reason ? `; ${telemetry.reason}` : "";
|
|
10499
|
+
out(
|
|
10500
|
+
`tamperward run \u2014 transient observer: ${telemetry.state} (advisory; ${counts}; log ${observer.log}${reason})`
|
|
10501
|
+
);
|
|
10502
|
+
}
|
|
10503
|
+
function collectObserverFindings(observer, telemetry, cwd, base, head, policy) {
|
|
10504
|
+
observerSummary(observer, telemetry);
|
|
10505
|
+
const { events } = readEvents(observer.log, 0);
|
|
10506
|
+
if (events.length === 0) return { blocking: false };
|
|
10507
|
+
let persistent = /* @__PURE__ */ new Set();
|
|
10508
|
+
try {
|
|
10509
|
+
const changes = [
|
|
10510
|
+
...head !== base ? diffRange(base, head, { cwd }) : [],
|
|
10511
|
+
...diffWorktreeWithUntracked({ cwd }, (rel) => isProtected(rel, policy))
|
|
10512
|
+
];
|
|
10513
|
+
persistent = new Set(
|
|
10514
|
+
changes.filter((change) => change.kind === "file").map((change) => change.path)
|
|
10515
|
+
);
|
|
10516
|
+
} catch (e) {
|
|
10517
|
+
out(
|
|
10518
|
+
`tamperward run \u2014 transient observer: recorded ${events.length} event(s), but could not classify them against the final diff (${e instanceof Error ? e.message : String(e)}).`
|
|
10519
|
+
);
|
|
10520
|
+
return { blocking: false };
|
|
10521
|
+
}
|
|
10522
|
+
const finalHash = (path) => {
|
|
10523
|
+
const e = inspectRel(cwd, path);
|
|
10524
|
+
return e.kind === "file" && e.content != null ? contentHash(e.content) : null;
|
|
10525
|
+
};
|
|
10526
|
+
const findings = transientFindings(events, persistent, policy, finalHash);
|
|
10527
|
+
for (const finding of findings) {
|
|
10528
|
+
out(
|
|
10529
|
+
`[observer] ${finding.severity === "block" ? "BLOCK" : "warn"} ${finding.rule}${finding.file ? " " + finding.file : ""}: ${finding.message}`
|
|
10530
|
+
);
|
|
10531
|
+
}
|
|
10532
|
+
return { blocking: findings.some((finding) => finding.severity === "block") };
|
|
10533
|
+
}
|
|
10534
|
+
function finishObserverAdvisory(observer) {
|
|
10535
|
+
if (!observer || observer.finished) return;
|
|
10536
|
+
const telemetry = stopObserverProcess(observer);
|
|
10537
|
+
observerSummary(observer, telemetry);
|
|
10538
|
+
}
|
|
10260
10539
|
function git3(args, cwd) {
|
|
10261
10540
|
return execFileSync10("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
10262
10541
|
}
|
|
@@ -10383,6 +10662,10 @@ function runEnvelope(opts) {
|
|
|
10383
10662
|
} else {
|
|
10384
10663
|
out(`tamperward run \u2014 dependency environment: verifier-owned by ${verifierBackend.image ?? "isolated image"}`);
|
|
10385
10664
|
}
|
|
10665
|
+
const observer = opts.observeTransients ? startSupervisedObserver(cwd, base, opts.observerEntry) : null;
|
|
10666
|
+
if (!observer) {
|
|
10667
|
+
out("tamperward run \u2014 transient observer: disabled (advisory; pass --observe-transients to enable)");
|
|
10668
|
+
}
|
|
10386
10669
|
const rewritesBefore = objectRewriteState(cwd);
|
|
10387
10670
|
const spawnTicks = nowTicks();
|
|
10388
10671
|
const agentRun = opts.agentBudget === void 0 ? (() => {
|
|
@@ -10410,6 +10693,7 @@ function runEnvelope(opts) {
|
|
|
10410
10693
|
err2("read against the anchor we started from. That is the finding.");
|
|
10411
10694
|
out(`
|
|
10412
10695
|
tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
10696
|
+
finishObserverAdvisory(observer);
|
|
10413
10697
|
return 1;
|
|
10414
10698
|
}
|
|
10415
10699
|
const dependencyBeforeAdjudication = dependencyEnvironment ? checkDependencyEnvironment(cwd, dependencyEnvironment) : { ok: true };
|
|
@@ -10419,6 +10703,7 @@ tamperward run \u2014 agent exit ${agentExit}; OBJECT_REWRITE \u2192 ENFORCEMENT
|
|
|
10419
10703
|
if (!opts.allowDepDrift) {
|
|
10420
10704
|
out(`
|
|
10421
10705
|
tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEMENT_FAILED`);
|
|
10706
|
+
finishObserverAdvisory(observer);
|
|
10422
10707
|
return 1;
|
|
10423
10708
|
}
|
|
10424
10709
|
err2("(--allow-dep-drift: proceeding anyway, on the operator's judgement.)");
|
|
@@ -10430,6 +10715,7 @@ tamperward run \u2014 agent exit ${agentExit}; DEPENDENCY_DRIFT \u2192 ENFORCEME
|
|
|
10430
10715
|
err2("the agent rewrote history out from under the anchor. That is the finding.");
|
|
10431
10716
|
out(`
|
|
10432
10717
|
tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
10718
|
+
finishObserverAdvisory(observer);
|
|
10433
10719
|
return 1;
|
|
10434
10720
|
}
|
|
10435
10721
|
const protectedIgnored = (rel) => isProtected(rel, frozenPolicy);
|
|
@@ -10480,11 +10766,24 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
10480
10766
|
err2(`tamperward run: ${survivors.length} process(es) started during this run still hold the`);
|
|
10481
10767
|
err2(`working tree (pid ${survivors.join(", ")}). A verdict cannot outlive the tree it describes.`);
|
|
10482
10768
|
}
|
|
10769
|
+
let observerBlocked = false;
|
|
10770
|
+
if (observer && !observer.finished) {
|
|
10771
|
+
const telemetry = stopObserverProcess(observer);
|
|
10772
|
+
const observed = collectObserverFindings(
|
|
10773
|
+
observer,
|
|
10774
|
+
telemetry,
|
|
10775
|
+
cwd,
|
|
10776
|
+
base,
|
|
10777
|
+
head,
|
|
10778
|
+
frozenPolicy
|
|
10779
|
+
);
|
|
10780
|
+
observerBlocked = observed.blocking;
|
|
10781
|
+
}
|
|
10483
10782
|
const notQuiescent = mutatedDuringAdjudication || survivors.length > 0 || depsDrifted || rewrote;
|
|
10484
10783
|
const cannot = !notQuiescent && (diffCode === 2 || workCode === 2 || verifyCode === 2);
|
|
10485
|
-
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent;
|
|
10784
|
+
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent || observerBlocked;
|
|
10486
10785
|
const enforcement = cannot ? 2 : blocked ? 1 : 0;
|
|
10487
|
-
const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : rewrote ? "OBJECT_REWRITE \u2014 the base the checks read is not the base we anchored to" : depsDrifted ? "DEPENDENCY_DRIFT \u2014 the suite runner changed under the envelope" : notQuiescent ? "NOT_QUIESCENT \u2014 the tree moved, or something still holds it" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentTimedOut ? "AGENT_TIMEOUT \u2014 runtime budget expired; post-timeout enforcement clean" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
|
|
10786
|
+
const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : rewrote ? "OBJECT_REWRITE \u2014 the base the checks read is not the base we anchored to" : depsDrifted ? "DEPENDENCY_DRIFT \u2014 the suite runner changed under the envelope" : notQuiescent ? "NOT_QUIESCENT \u2014 the tree moved, or something still holds it" : observerBlocked ? "TRANSIENT_OBSERVER_BLOCK \u2014 observed temporal weakening under strict transient policy" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentTimedOut ? "AGENT_TIMEOUT \u2014 runtime budget expired; post-timeout enforcement clean" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
|
|
10488
10787
|
const agentSummary = agentTimedOut ? `AGENT_TIMEOUT (${opts.agentBudget}s; exit 124)` : `agent exit ${agentExit}`;
|
|
10489
10788
|
out(`
|
|
10490
10789
|
tamperward run \u2014 ${agentSummary}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
|
|
@@ -10504,6 +10803,7 @@ function parseRun(args) {
|
|
|
10504
10803
|
else if (a === "--allow-dirty") o.allowDirty = true;
|
|
10505
10804
|
else if (a === "--settle") o.settle = Number(args[++i]);
|
|
10506
10805
|
else if (a === "--allow-dep-drift") o.allowDepDrift = true;
|
|
10806
|
+
else if (a === "--observe-transients") o.observeTransients = true;
|
|
10507
10807
|
else if (a === "--cwd") o.cwd = args[++i];
|
|
10508
10808
|
else {
|
|
10509
10809
|
o.argv = args.slice(i);
|
|
@@ -10637,7 +10937,7 @@ function validateCliArgs(cmd, args) {
|
|
|
10637
10937
|
const prefix = args.slice(0, delimiter3);
|
|
10638
10938
|
const command = args.slice(delimiter3 + 1);
|
|
10639
10939
|
const parsed = validateFlatArgs(prefix, {
|
|
10640
|
-
flags: ["--allow-dirty", "--allow-dep-drift"],
|
|
10940
|
+
flags: ["--allow-dirty", "--allow-dep-drift", "--observe-transients"],
|
|
10641
10941
|
values: {
|
|
10642
10942
|
"--base": "string",
|
|
10643
10943
|
"--cmd": "string",
|
|
@@ -10704,7 +11004,7 @@ function validateCliArgs(cmd, args) {
|
|
|
10704
11004
|
}
|
|
10705
11005
|
if (cmd === "watch") {
|
|
10706
11006
|
return validateFlatArgs(args, {
|
|
10707
|
-
values: { "--dir": "string", "--log": "string" }
|
|
11007
|
+
values: { "--dir": "string", "--log": "string", "--base": "string" }
|
|
10708
11008
|
}).error;
|
|
10709
11009
|
}
|
|
10710
11010
|
return void 0;
|
|
@@ -10731,8 +11031,10 @@ Formats:
|
|
|
10731
11031
|
tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
|
|
10732
11032
|
tamperward sweep claude Stop sweep (re-scan the turn's working tree)
|
|
10733
11033
|
tamperward watch [--dir D] [--log F] filesystem-event observer daemon: records
|
|
10734
|
-
|
|
10735
|
-
observe supported transient effects
|
|
11034
|
+
[--base R] protected-file events so the sweep can
|
|
11035
|
+
observe supported transient effects.
|
|
11036
|
+
--base freezes observer policy to a
|
|
11037
|
+
trusted revision
|
|
10736
11038
|
tamperward verify [--base R] [--cmd C] pristine-suite re-execution: run the suite
|
|
10737
11039
|
[--budget S] [--json] [--keep] as-is AND with protected files restored
|
|
10738
11040
|
[--require-ancestor] [--cwd D] from the trusted base; a visible-green /
|
|
@@ -10744,9 +11046,13 @@ Formats:
|
|
|
10744
11046
|
[--base R] [--cmd C] base, run the agent, treat its exit as
|
|
10745
11047
|
[--budget S] [--agent-budget S] untrusted, then re-adjudicate the tree it
|
|
10746
11048
|
[--allow-dirty] [--settle S] left (policy over base...HEAD and the
|
|
10747
|
-
[--allow-dep-drift] [--cwd D] worktree, plus verify).
|
|
10748
|
-
|
|
10749
|
-
|
|
11049
|
+
[--allow-dep-drift] [--cwd D] worktree, plus verify).
|
|
11050
|
+
[--observe-transients] --observe-transients starts a session-
|
|
11051
|
+
scoped observer before the agent and
|
|
11052
|
+
reports temporal evidence/health after it.
|
|
11053
|
+
--agent-budget bounds the agent runtime
|
|
11054
|
+
itself and still adjudicates after timeout.
|
|
11055
|
+
Exit: agent's
|
|
10750
11056
|
code when clean; 124 on clean AGENT_TIMEOUT;
|
|
10751
11057
|
1 on any blocking finding/masked failure;
|
|
10752
11058
|
2 when it cannot adjudicate (fails closed)
|
|
@@ -10809,7 +11115,12 @@ function main(argv2) {
|
|
|
10809
11115
|
case "verify":
|
|
10810
11116
|
return runVerify(parseVerify(rest));
|
|
10811
11117
|
case "run":
|
|
10812
|
-
return runEnvelope(
|
|
11118
|
+
return runEnvelope({
|
|
11119
|
+
...parseRun(rest),
|
|
11120
|
+
// The supervised observer re-launches this exact TamperWard CLI under
|
|
11121
|
+
// process.execPath rather than resolving another binary from PATH.
|
|
11122
|
+
observerEntry: process.argv[1]
|
|
11123
|
+
});
|
|
10813
11124
|
case void 0:
|
|
10814
11125
|
case "-h":
|
|
10815
11126
|
case "--help":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
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",
|