tamperward 2.13.1 → 2.13.3
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 +8 -1
- package/dist/cli/index.js +197 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,7 +149,14 @@ 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.
|
|
153
160
|
|
|
154
161
|
After the runtime exits its exit code is treated as untrusted, and the envelope
|
|
155
162
|
checks that post-agent `HEAD` still descends from the entry commit; the committed
|
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, mkdirSync as mkdirSync4, readdirSync as readdirSync2,
|
|
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;
|
|
@@ -7060,28 +7094,35 @@ function watchTree(dir, cb) {
|
|
|
7060
7094
|
const child = rel ? `${rel}/${String(fname)}` : String(fname);
|
|
7061
7095
|
cb(kind, child);
|
|
7062
7096
|
try {
|
|
7063
|
-
if (
|
|
7097
|
+
if (lstatSync3(join11(dir, child)).isDirectory()) addDir(child);
|
|
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) {
|
|
7081
7122
|
const child = rel ? `${rel}/${name}` : name;
|
|
7082
7123
|
if (SKIP2.test(child + "/")) continue;
|
|
7083
7124
|
try {
|
|
7084
|
-
if (
|
|
7125
|
+
if (lstatSync3(join11(dir, child)).isDirectory()) walk2(child);
|
|
7085
7126
|
} catch {
|
|
7086
7127
|
}
|
|
7087
7128
|
}
|
|
@@ -7096,19 +7137,91 @@ 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();
|
|
@@ -7120,18 +7233,18 @@ function runWatch(args) {
|
|
|
7120
7233
|
const policy = loadPolicy(dir);
|
|
7121
7234
|
const out2 = log ?? defaultEventLog(dir);
|
|
7122
7235
|
startWatcher(dir, out2, policy);
|
|
7123
|
-
process.stdout.write(`tamperward watch: recording protected-file events under ${dir} -> ${out2}
|
|
7236
|
+
process.stdout.write(`tamperward watch: recording protected-file events under ${dir} -> ${out2} (health: ${watcherHealthPath(out2)})
|
|
7124
7237
|
`);
|
|
7125
7238
|
return -1;
|
|
7126
7239
|
}
|
|
7127
7240
|
|
|
7128
7241
|
// src/detectors/fs-events.ts
|
|
7129
|
-
import { readFileSync as readFileSync10, statSync
|
|
7242
|
+
import { readFileSync as readFileSync10, statSync } from "node:fs";
|
|
7130
7243
|
var RULE11 = "transient-protected-mutation";
|
|
7131
7244
|
function readEvents(log, offset) {
|
|
7132
7245
|
let size = 0;
|
|
7133
7246
|
try {
|
|
7134
|
-
size =
|
|
7247
|
+
size = statSync(log).size;
|
|
7135
7248
|
} catch {
|
|
7136
7249
|
return { events: [], newOffset: offset };
|
|
7137
7250
|
}
|
|
@@ -7441,6 +7554,10 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7441
7554
|
const none = { blocks: [], commit: () => {
|
|
7442
7555
|
} };
|
|
7443
7556
|
const log = defaultEventLog(cwd);
|
|
7557
|
+
const telemetry = watcherTelemetry(log);
|
|
7558
|
+
if (telemetry.state !== "healthy") {
|
|
7559
|
+
recordObserverHealth(telemetry.state, telemetry.reason);
|
|
7560
|
+
}
|
|
7444
7561
|
const cp = cursorPath(cwd, sessionId);
|
|
7445
7562
|
if (!existsSync8(log)) return none;
|
|
7446
7563
|
let offset = 0;
|
|
@@ -7451,7 +7568,7 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7451
7568
|
const { events, newOffset } = readEvents(log, offset);
|
|
7452
7569
|
if (events.length === 0) return { ...none, commit: () => {
|
|
7453
7570
|
if (cp) try {
|
|
7454
|
-
|
|
7571
|
+
writeFileSync5(cp, String(newOffset));
|
|
7455
7572
|
} catch {
|
|
7456
7573
|
}
|
|
7457
7574
|
} };
|
|
@@ -7466,7 +7583,7 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7466
7583
|
blocks: findings.filter((f) => f.severity === "block"),
|
|
7467
7584
|
commit: () => {
|
|
7468
7585
|
if (cp) try {
|
|
7469
|
-
|
|
7586
|
+
writeFileSync5(cp, String(newOffset));
|
|
7470
7587
|
} catch {
|
|
7471
7588
|
}
|
|
7472
7589
|
}
|
|
@@ -7480,6 +7597,19 @@ function recordWarns(warns) {
|
|
|
7480
7597
|
} catch {
|
|
7481
7598
|
}
|
|
7482
7599
|
}
|
|
7600
|
+
function recordObserverHealth(state, reason) {
|
|
7601
|
+
const log = process.env.TAMPERWARD_DENYLOG;
|
|
7602
|
+
if (!log) return;
|
|
7603
|
+
const detail = (reason ?? "").replace(/[\r\n]+/g, " ").trim();
|
|
7604
|
+
try {
|
|
7605
|
+
appendFileSync4(
|
|
7606
|
+
log,
|
|
7607
|
+
`warn:transient-observer:${state}${detail ? ":" + detail : ""}
|
|
7608
|
+
`
|
|
7609
|
+
);
|
|
7610
|
+
} catch {
|
|
7611
|
+
}
|
|
7612
|
+
}
|
|
7483
7613
|
function stopVerdict(input) {
|
|
7484
7614
|
if (input.stop_hook_active) return { exitCode: 0, stdout: "" };
|
|
7485
7615
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -7598,7 +7728,7 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
7598
7728
|
// src/cli/init.ts
|
|
7599
7729
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7600
7730
|
import { createHash as createHash6 } from "node:crypto";
|
|
7601
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as
|
|
7731
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6, chmodSync, statSync as statSync2 } from "node:fs";
|
|
7602
7732
|
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join12, relative as relative3, resolve as resolve3 } from "node:path";
|
|
7603
7733
|
|
|
7604
7734
|
// src/verifier-limits.ts
|
|
@@ -7832,7 +7962,7 @@ function planPolicy(cwd) {
|
|
|
7832
7962
|
path: POLICY_FILE,
|
|
7833
7963
|
status: "create",
|
|
7834
7964
|
detail: "baseline policy with commented overrides",
|
|
7835
|
-
apply: () =>
|
|
7965
|
+
apply: () => writeFileSync6(path, POLICY_CONTENT)
|
|
7836
7966
|
};
|
|
7837
7967
|
}
|
|
7838
7968
|
function planClaudeHooks(cwd) {
|
|
@@ -7904,7 +8034,7 @@ function planClaudeHooks(cwd) {
|
|
|
7904
8034
|
}
|
|
7905
8035
|
if (needDisableFalse) settings.disableAllHooks = false;
|
|
7906
8036
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
7907
|
-
|
|
8037
|
+
writeFileSync6(path, JSON.stringify(settings, null, 2) + "\n");
|
|
7908
8038
|
}
|
|
7909
8039
|
};
|
|
7910
8040
|
}
|
|
@@ -7985,7 +8115,7 @@ function planPreCommit(cwd) {
|
|
|
7985
8115
|
}
|
|
7986
8116
|
}
|
|
7987
8117
|
const rel = display(cwd, path);
|
|
7988
|
-
if (existsSync9(path) && !
|
|
8118
|
+
if (existsSync9(path) && !statSync2(path).isFile()) {
|
|
7989
8119
|
return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
|
|
7990
8120
|
}
|
|
7991
8121
|
const existing = existsSync9(path) ? readFileSync12(path, "utf8") : null;
|
|
@@ -7997,7 +8127,7 @@ function planPreCommit(cwd) {
|
|
|
7997
8127
|
detail: `create via ${note}`,
|
|
7998
8128
|
apply: () => {
|
|
7999
8129
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
8000
|
-
|
|
8130
|
+
writeFileSync6(path, `#!/bin/sh
|
|
8001
8131
|
${block.join("\n")}
|
|
8002
8132
|
`);
|
|
8003
8133
|
chmodSync(path, 493);
|
|
@@ -8012,7 +8142,7 @@ ${block.join("\n")}
|
|
|
8012
8142
|
const dead = unconditionalExitAt(lines);
|
|
8013
8143
|
const at = lines.findIndex((l) => PRECOMMIT_RE.test(l));
|
|
8014
8144
|
const write = (out2) => {
|
|
8015
|
-
|
|
8145
|
+
writeFileSync6(path, out2.join("\n"));
|
|
8016
8146
|
chmodSync(path, 493);
|
|
8017
8147
|
};
|
|
8018
8148
|
if (at !== -1 && (dead === -1 || at < dead)) {
|
|
@@ -8107,7 +8237,7 @@ ${CODEOWNERS_MARK}
|
|
|
8107
8237
|
} : {},
|
|
8108
8238
|
apply: () => {
|
|
8109
8239
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
8110
|
-
|
|
8240
|
+
writeFileSync6(path, existing === null ? block.replace(/^\n/, "") : existing.replace(/\n?$/, "\n") + block);
|
|
8111
8241
|
}
|
|
8112
8242
|
};
|
|
8113
8243
|
}
|
|
@@ -8116,7 +8246,7 @@ function planWorkflow(cwd, force) {
|
|
|
8116
8246
|
const path = join12(cwd, rel);
|
|
8117
8247
|
const write = () => {
|
|
8118
8248
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
8119
|
-
|
|
8249
|
+
writeFileSync6(path, workflowFile(WORKFLOW_CONTENT));
|
|
8120
8250
|
};
|
|
8121
8251
|
if (!existsSync9(path)) {
|
|
8122
8252
|
return {
|
|
@@ -8529,13 +8659,30 @@ function runDoctor(opts = {}) {
|
|
|
8529
8659
|
"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
8660
|
);
|
|
8531
8661
|
}
|
|
8662
|
+
const observer = watcherTelemetry(defaultEventLog(cwd));
|
|
8663
|
+
if (observer.state === "healthy" && observer.health) {
|
|
8664
|
+
process.stdout.write(
|
|
8665
|
+
`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.
|
|
8666
|
+
`
|
|
8667
|
+
);
|
|
8668
|
+
} else if (observer.state === "degraded" && observer.health) {
|
|
8669
|
+
process.stdout.write(
|
|
8670
|
+
`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.
|
|
8671
|
+
`
|
|
8672
|
+
);
|
|
8673
|
+
} else {
|
|
8674
|
+
process.stdout.write(
|
|
8675
|
+
`tamperward doctor: transient observer: unavailable${observer.reason ? " \u2014 " + observer.reason : ""}. Observer telemetry is optional/advisory; zero events are not evidence of no transient activity.
|
|
8676
|
+
`
|
|
8677
|
+
);
|
|
8678
|
+
}
|
|
8532
8679
|
return 0;
|
|
8533
8680
|
}
|
|
8534
8681
|
|
|
8535
8682
|
// src/cli/verify.ts
|
|
8536
8683
|
import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
|
|
8537
8684
|
import { createHash as createHash10 } from "node:crypto";
|
|
8538
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as
|
|
8685
|
+
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
8686
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8540
8687
|
import { dirname as dirname7, isAbsolute as isAbsolute10, join as join16, relative as relative5, resolve as resolve7, sep as sep2 } from "node:path";
|
|
8541
8688
|
|
|
@@ -8569,12 +8716,12 @@ function treeFingerprint(cwd, keepIgnored) {
|
|
|
8569
8716
|
import { createHash as createHash8 } from "node:crypto";
|
|
8570
8717
|
import {
|
|
8571
8718
|
existsSync as existsSync11,
|
|
8572
|
-
lstatSync as
|
|
8719
|
+
lstatSync as lstatSync4,
|
|
8573
8720
|
readdirSync as readdirSync4,
|
|
8574
8721
|
readFileSync as readFileSync14,
|
|
8575
8722
|
readlinkSync as readlinkSync2,
|
|
8576
8723
|
realpathSync as realpathSync3,
|
|
8577
|
-
statSync as
|
|
8724
|
+
statSync as statSync3
|
|
8578
8725
|
} from "node:fs";
|
|
8579
8726
|
import {
|
|
8580
8727
|
basename as basename3,
|
|
@@ -8593,7 +8740,7 @@ function inside(root, candidate) {
|
|
|
8593
8740
|
return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute9(rel);
|
|
8594
8741
|
}
|
|
8595
8742
|
function modeKind(path) {
|
|
8596
|
-
const st =
|
|
8743
|
+
const st = lstatSync4(path);
|
|
8597
8744
|
if (st.isFile()) return { kind: "file", mode: st.mode };
|
|
8598
8745
|
if (st.isDirectory()) return { kind: "dir", mode: st.mode };
|
|
8599
8746
|
if (st.isSymbolicLink()) return { kind: "symlink", mode: st.mode };
|
|
@@ -8603,7 +8750,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
8603
8750
|
let rootStat;
|
|
8604
8751
|
let currentReal;
|
|
8605
8752
|
try {
|
|
8606
|
-
rootStat =
|
|
8753
|
+
rootStat = lstatSync4(root.path);
|
|
8607
8754
|
currentReal = realpathSync3(root.path);
|
|
8608
8755
|
} catch (e) {
|
|
8609
8756
|
throw new Unattestable(
|
|
@@ -8615,7 +8762,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
8615
8762
|
root.kind + " root moved from " + JSON.stringify(root.realPath) + " to " + JSON.stringify(currentReal)
|
|
8616
8763
|
);
|
|
8617
8764
|
}
|
|
8618
|
-
if (!
|
|
8765
|
+
if (!statSync3(root.path).isDirectory()) {
|
|
8619
8766
|
throw new Unattestable(root.kind + " root " + JSON.stringify(root.path) + " is not a directory");
|
|
8620
8767
|
}
|
|
8621
8768
|
h.update("root\0" + root.kind + "\0" + root.path + "\0" + root.realPath + "\0" + String(rootStat.mode) + "\0");
|
|
@@ -8625,7 +8772,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
8625
8772
|
const hashFile = (path, label) => {
|
|
8626
8773
|
let st;
|
|
8627
8774
|
try {
|
|
8628
|
-
st =
|
|
8775
|
+
st = statSync3(path);
|
|
8629
8776
|
if (!st.isFile()) throw new Error("not a regular file");
|
|
8630
8777
|
h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
|
|
8631
8778
|
h.update(readFileSync14(path));
|
|
@@ -8691,7 +8838,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
8691
8838
|
} catch {
|
|
8692
8839
|
continue;
|
|
8693
8840
|
}
|
|
8694
|
-
const followed =
|
|
8841
|
+
const followed = statSync3(path);
|
|
8695
8842
|
if (followed.isFile()) {
|
|
8696
8843
|
hashFile(resolved, "@target:" + rel);
|
|
8697
8844
|
continue;
|
|
@@ -8729,7 +8876,7 @@ function snapshot(roots, probes, cwd) {
|
|
|
8729
8876
|
}
|
|
8730
8877
|
function freezeRoot(kind, path) {
|
|
8731
8878
|
try {
|
|
8732
|
-
if (!
|
|
8879
|
+
if (!statSync3(path).isDirectory()) {
|
|
8733
8880
|
throw new Error("not a directory");
|
|
8734
8881
|
}
|
|
8735
8882
|
return { kind, path: resolve5(path), realPath: realpathSync3(path) };
|
|
@@ -8778,7 +8925,7 @@ function resolveExecutable(token, cwd, env) {
|
|
|
8778
8925
|
for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
8779
8926
|
const candidate = resolve5(entry, token);
|
|
8780
8927
|
try {
|
|
8781
|
-
if (existsSync11(candidate) &&
|
|
8928
|
+
if (existsSync11(candidate) && statSync3(candidate).isFile()) return realpathSync3(candidate);
|
|
8782
8929
|
} catch {
|
|
8783
8930
|
}
|
|
8784
8931
|
}
|
|
@@ -8921,7 +9068,7 @@ function dependencyEnvironmentSummary(descriptor) {
|
|
|
8921
9068
|
// src/verifier-backend.ts
|
|
8922
9069
|
import { createHash as createHash9, randomUUID } from "node:crypto";
|
|
8923
9070
|
import { spawnSync } from "node:child_process";
|
|
8924
|
-
import { accessSync, constants as constants2, readFileSync as readFileSync15, realpathSync as realpathSync4, statSync as
|
|
9071
|
+
import { accessSync, constants as constants2, readFileSync as readFileSync15, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
|
|
8925
9072
|
import { delimiter as delimiter2, dirname as dirname6, join as join15, resolve as resolve6 } from "node:path";
|
|
8926
9073
|
var DEFAULT_CONTAINER_RESOURCE_LIMITS = Object.freeze({
|
|
8927
9074
|
memoryBytes: 2 * 1024 * 1024 * 1024,
|
|
@@ -8939,7 +9086,7 @@ function identity() {
|
|
|
8939
9086
|
}
|
|
8940
9087
|
function writableByCaller(path) {
|
|
8941
9088
|
try {
|
|
8942
|
-
const st =
|
|
9089
|
+
const st = statSync4(path);
|
|
8943
9090
|
const { uid, groups } = identity();
|
|
8944
9091
|
if (uid === 0) return true;
|
|
8945
9092
|
if ((st.mode & 2) !== 0) return true;
|
|
@@ -9512,7 +9659,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
9512
9659
|
const current = join16(base, ...stack);
|
|
9513
9660
|
let st;
|
|
9514
9661
|
try {
|
|
9515
|
-
st =
|
|
9662
|
+
st = lstatSync5(current);
|
|
9516
9663
|
} catch {
|
|
9517
9664
|
continue;
|
|
9518
9665
|
}
|
|
@@ -9548,7 +9695,7 @@ function rejectLinkedParent(cwd, rel) {
|
|
|
9548
9695
|
current = join16(current, part);
|
|
9549
9696
|
let st;
|
|
9550
9697
|
try {
|
|
9551
|
-
st =
|
|
9698
|
+
st = lstatSync5(current);
|
|
9552
9699
|
} catch {
|
|
9553
9700
|
continue;
|
|
9554
9701
|
}
|
|
@@ -9565,7 +9712,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
9565
9712
|
rejectLinkedParent(cwd, rel);
|
|
9566
9713
|
let st;
|
|
9567
9714
|
try {
|
|
9568
|
-
st =
|
|
9715
|
+
st = lstatSync5(src);
|
|
9569
9716
|
} catch {
|
|
9570
9717
|
continue;
|
|
9571
9718
|
}
|
|
@@ -9590,7 +9737,7 @@ function materialize(cwd, dest, dependencyRoot) {
|
|
|
9590
9737
|
}
|
|
9591
9738
|
function dropSymlink(p) {
|
|
9592
9739
|
try {
|
|
9593
|
-
if (
|
|
9740
|
+
if (lstatSync5(p).isSymbolicLink()) rmSync3(p, { force: true });
|
|
9594
9741
|
} catch {
|
|
9595
9742
|
}
|
|
9596
9743
|
}
|
|
@@ -9621,7 +9768,7 @@ function overlayDigest(dest, paths) {
|
|
|
9621
9768
|
h.update("\0");
|
|
9622
9769
|
const p = join16(dest, rel);
|
|
9623
9770
|
try {
|
|
9624
|
-
const st =
|
|
9771
|
+
const st = lstatSync5(p);
|
|
9625
9772
|
h.update(String(st.mode));
|
|
9626
9773
|
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync16(p));
|
|
9627
9774
|
} catch {
|
|
@@ -9655,7 +9802,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot) {
|
|
|
9655
9802
|
safeSymlink(target, out2, dest, `the base's ${e.path}`);
|
|
9656
9803
|
restoredLinks.push({ path: e.path, out: out2, target });
|
|
9657
9804
|
} else {
|
|
9658
|
-
|
|
9805
|
+
writeFileSync7(out2, content);
|
|
9659
9806
|
chmodSync2(out2, parseInt(e.mode.slice(-4), 8) & 511);
|
|
9660
9807
|
}
|
|
9661
9808
|
restored.push(e.path);
|
|
@@ -9711,7 +9858,7 @@ function suiteEnv(scratch) {
|
|
|
9711
9858
|
}
|
|
9712
9859
|
const userRc = join16(scratch, "npmrc-user");
|
|
9713
9860
|
const globalRc = join16(scratch, "npmrc-global");
|
|
9714
|
-
for (const f of [userRc, globalRc])
|
|
9861
|
+
for (const f of [userRc, globalRc]) writeFileSync7(f, "", { mode: 292 });
|
|
9715
9862
|
env.npm_config_userconfig = userRc;
|
|
9716
9863
|
env.npm_config_globalconfig = globalRc;
|
|
9717
9864
|
env.npm_config_node_options = " ";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.3",
|
|
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",
|