tamperward 1.10.4 → 1.11.1
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 +129 -5
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2791,7 +2791,7 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
2791
2791
|
import { execFileSync as execFileSync4, spawnSync } from "node:child_process";
|
|
2792
2792
|
import { chmodSync as chmodSync2, cpSync, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, rmSync as rmSync2, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "node:fs";
|
|
2793
2793
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
2794
|
-
import { dirname as dirname4, join as join9 } from "node:path";
|
|
2794
|
+
import { dirname as dirname4, join as join9, resolve } from "node:path";
|
|
2795
2795
|
var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
|
|
2796
2796
|
function git2(args, cwd) {
|
|
2797
2797
|
return execFileSync4("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28 });
|
|
@@ -2820,7 +2820,7 @@ function materialize(cwd, dest) {
|
|
|
2820
2820
|
cpSync(src, out2);
|
|
2821
2821
|
chmodSync2(out2, st.mode);
|
|
2822
2822
|
}
|
|
2823
|
-
const nm =
|
|
2823
|
+
const nm = resolve(cwd, "node_modules");
|
|
2824
2824
|
if (existsSync8(nm)) symlinkSync(nm, join9(dest, "node_modules"), "dir");
|
|
2825
2825
|
}
|
|
2826
2826
|
function overlayPristine(cwd, base, dest, policy) {
|
|
@@ -2954,13 +2954,110 @@ function parseVerify(args) {
|
|
|
2954
2954
|
|
|
2955
2955
|
// src/cli/run.ts
|
|
2956
2956
|
import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "node:child_process";
|
|
2957
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
2958
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync11, readlinkSync, realpathSync, statSync as statSync6 } from "node:fs";
|
|
2959
|
+
import { join as join10, resolve as resolve2 } from "node:path";
|
|
2957
2960
|
var out = (s) => process.stdout.write(s + "\n");
|
|
2958
2961
|
var err = (s) => process.stderr.write(s + "\n");
|
|
2959
2962
|
function git3(args, cwd) {
|
|
2960
2963
|
return execFileSync5("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28 });
|
|
2961
2964
|
}
|
|
2965
|
+
function treeFingerprint(cwd) {
|
|
2966
|
+
const files = git3(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean).sort();
|
|
2967
|
+
const h = createHash5("sha256");
|
|
2968
|
+
for (const rel of files) {
|
|
2969
|
+
h.update(rel);
|
|
2970
|
+
h.update("\0");
|
|
2971
|
+
try {
|
|
2972
|
+
h.update(readFileSync11(join10(cwd, rel)));
|
|
2973
|
+
} catch {
|
|
2974
|
+
h.update("<unreadable>");
|
|
2975
|
+
}
|
|
2976
|
+
h.update("\0");
|
|
2977
|
+
}
|
|
2978
|
+
return h.digest("hex");
|
|
2979
|
+
}
|
|
2980
|
+
function depsFingerprint(cwd) {
|
|
2981
|
+
const root = join10(cwd, "node_modules");
|
|
2982
|
+
const h = createHash5("sha256");
|
|
2983
|
+
const walk = (dir) => {
|
|
2984
|
+
let entries;
|
|
2985
|
+
try {
|
|
2986
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
2987
|
+
} catch {
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2991
|
+
const p = join10(dir, e.name);
|
|
2992
|
+
if (e.isDirectory()) walk(p);
|
|
2993
|
+
else if (e.isFile()) {
|
|
2994
|
+
h.update(p);
|
|
2995
|
+
try {
|
|
2996
|
+
h.update(readFileSync11(p));
|
|
2997
|
+
h.update(String(statSync6(p).mode));
|
|
2998
|
+
} catch {
|
|
2999
|
+
h.update("<unreadable>");
|
|
3000
|
+
}
|
|
3001
|
+
} else if (e.isSymbolicLink()) {
|
|
3002
|
+
h.update(p);
|
|
3003
|
+
try {
|
|
3004
|
+
h.update(readlinkSync(p));
|
|
3005
|
+
} catch {
|
|
3006
|
+
h.update("<unreadable-link>");
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
};
|
|
3011
|
+
try {
|
|
3012
|
+
if (!statSync6(root).isDirectory()) return "none";
|
|
3013
|
+
} catch {
|
|
3014
|
+
return "none";
|
|
3015
|
+
}
|
|
3016
|
+
walk(root);
|
|
3017
|
+
return h.digest("hex");
|
|
3018
|
+
}
|
|
3019
|
+
function nowTicks() {
|
|
3020
|
+
try {
|
|
3021
|
+
return Math.floor(parseFloat(readFileSync11("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
3022
|
+
} catch {
|
|
3023
|
+
return Number.POSITIVE_INFINITY;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
3027
|
+
const out2 = [];
|
|
3028
|
+
let real;
|
|
3029
|
+
try {
|
|
3030
|
+
real = realpathSync(cwd);
|
|
3031
|
+
} catch {
|
|
3032
|
+
return out2;
|
|
3033
|
+
}
|
|
3034
|
+
let pids;
|
|
3035
|
+
try {
|
|
3036
|
+
pids = readdirSync3("/proc").filter((d) => /^\d+$/.test(d));
|
|
3037
|
+
} catch {
|
|
3038
|
+
return out2;
|
|
3039
|
+
}
|
|
3040
|
+
for (const pid of pids) {
|
|
3041
|
+
const n = Number(pid);
|
|
3042
|
+
if (n === process.pid) continue;
|
|
3043
|
+
let held;
|
|
3044
|
+
try {
|
|
3045
|
+
held = realpathSync(`/proc/${pid}/cwd`);
|
|
3046
|
+
} catch {
|
|
3047
|
+
continue;
|
|
3048
|
+
}
|
|
3049
|
+
if (held !== real && !held.startsWith(real + "/")) continue;
|
|
3050
|
+
try {
|
|
3051
|
+
const stat = readFileSync11(`/proc/${pid}/stat`, "utf8");
|
|
3052
|
+
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
3053
|
+
if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
|
|
3054
|
+
} catch {
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
return out2;
|
|
3058
|
+
}
|
|
2962
3059
|
function runEnvelope(opts) {
|
|
2963
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
3060
|
+
const cwd = resolve2(opts.cwd ?? process.cwd());
|
|
2964
3061
|
if (!opts.argv.length) {
|
|
2965
3062
|
err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
|
|
2966
3063
|
return 2;
|
|
@@ -2997,6 +3094,8 @@ function runEnvelope(opts) {
|
|
|
2997
3094
|
}
|
|
2998
3095
|
}
|
|
2999
3096
|
out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
3097
|
+
const depsBefore = depsFingerprint(cwd);
|
|
3098
|
+
const spawnTicks = nowTicks();
|
|
3000
3099
|
const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
|
|
3001
3100
|
const agentExit = agent.status ?? 1;
|
|
3002
3101
|
const head = git3(["rev-parse", "HEAD"], cwd).trim();
|
|
@@ -3008,6 +3107,7 @@ function runEnvelope(opts) {
|
|
|
3008
3107
|
tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
3009
3108
|
return 1;
|
|
3010
3109
|
}
|
|
3110
|
+
const fpBefore = treeFingerprint(cwd);
|
|
3011
3111
|
let diffCode = 0;
|
|
3012
3112
|
if (head !== base) {
|
|
3013
3113
|
out(`
|
|
@@ -3018,10 +3118,32 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
3018
3118
|
const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
|
|
3019
3119
|
out("\n[envelope] pristine verification against the trusted base:");
|
|
3020
3120
|
const verifyCode = runVerify({ cwd, base, cmd: frozenCmd, budget: frozenBudget, policyOverride: frozenPolicy });
|
|
3121
|
+
if (opts.settle && opts.settle > 0) {
|
|
3122
|
+
try {
|
|
3123
|
+
execFileSync5("sleep", [String(opts.settle)]);
|
|
3124
|
+
} catch {
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
const mutatedDuringAdjudication = treeFingerprint(cwd) !== fpBefore;
|
|
3128
|
+
if (mutatedDuringAdjudication) {
|
|
3129
|
+
err("tamperward run: the working tree changed while it was being adjudicated \u2014");
|
|
3130
|
+
err("the verdict below would describe a tree that no longer exists.");
|
|
3131
|
+
}
|
|
3132
|
+
const depsDrifted = !opts.allowDepDrift && depsFingerprint(cwd) !== depsBefore;
|
|
3133
|
+
if (depsDrifted) {
|
|
3134
|
+
err("tamperward run: the installed dependency tree changed during this run \u2014 the program that");
|
|
3135
|
+
err("runs the suite is not the one we started with. Both verification runs execute it.");
|
|
3136
|
+
}
|
|
3137
|
+
const survivors = survivorsHoldingTree(cwd, spawnTicks);
|
|
3138
|
+
if (survivors.length) {
|
|
3139
|
+
err(`tamperward run: ${survivors.length} process(es) started during this run still hold the`);
|
|
3140
|
+
err(`working tree (pid ${survivors.join(", ")}). A verdict cannot outlive the tree it describes.`);
|
|
3141
|
+
}
|
|
3142
|
+
const notQuiescent = mutatedDuringAdjudication || survivors.length > 0 || depsDrifted;
|
|
3021
3143
|
const cannot = diffCode === 2 || workCode === 2 || verifyCode === 2;
|
|
3022
|
-
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1;
|
|
3144
|
+
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent;
|
|
3023
3145
|
const enforcement = cannot ? 2 : blocked ? 1 : 0;
|
|
3024
|
-
const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
|
|
3146
|
+
const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : 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" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
|
|
3025
3147
|
out(`
|
|
3026
3148
|
tamperward run \u2014 agent exit ${agentExit}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
|
|
3027
3149
|
return enforcement !== 0 ? enforcement : agentExit;
|
|
@@ -3037,6 +3159,8 @@ function parseRun(args) {
|
|
|
3037
3159
|
else if (a === "--cmd") o.cmd = args[++i];
|
|
3038
3160
|
else if (a === "--budget") o.budget = Number(args[++i]);
|
|
3039
3161
|
else if (a === "--allow-dirty") o.allowDirty = true;
|
|
3162
|
+
else if (a === "--settle") o.settle = Number(args[++i]);
|
|
3163
|
+
else if (a === "--allow-dep-drift") o.allowDepDrift = true;
|
|
3040
3164
|
else if (a === "--cwd") o.cwd = args[++i];
|
|
3041
3165
|
else {
|
|
3042
3166
|
o.argv = args.slice(i);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
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",
|