tamperward 1.10.4 → 1.11.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/dist/cli/index.js +80 -2
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2954,11 +2954,69 @@ 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, realpathSync } from "node:fs";
|
|
2959
|
+
import { join as join10 } 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 nowTicks() {
|
|
2981
|
+
try {
|
|
2982
|
+
return Math.floor(parseFloat(readFileSync11("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
2983
|
+
} catch {
|
|
2984
|
+
return Number.POSITIVE_INFINITY;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
2988
|
+
const out2 = [];
|
|
2989
|
+
let real;
|
|
2990
|
+
try {
|
|
2991
|
+
real = realpathSync(cwd);
|
|
2992
|
+
} catch {
|
|
2993
|
+
return out2;
|
|
2994
|
+
}
|
|
2995
|
+
let pids;
|
|
2996
|
+
try {
|
|
2997
|
+
pids = readdirSync3("/proc").filter((d) => /^\d+$/.test(d));
|
|
2998
|
+
} catch {
|
|
2999
|
+
return out2;
|
|
3000
|
+
}
|
|
3001
|
+
for (const pid of pids) {
|
|
3002
|
+
const n = Number(pid);
|
|
3003
|
+
if (n === process.pid) continue;
|
|
3004
|
+
let held;
|
|
3005
|
+
try {
|
|
3006
|
+
held = realpathSync(`/proc/${pid}/cwd`);
|
|
3007
|
+
} catch {
|
|
3008
|
+
continue;
|
|
3009
|
+
}
|
|
3010
|
+
if (held !== real && !held.startsWith(real + "/")) continue;
|
|
3011
|
+
try {
|
|
3012
|
+
const stat = readFileSync11(`/proc/${pid}/stat`, "utf8");
|
|
3013
|
+
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
3014
|
+
if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
|
|
3015
|
+
} catch {
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
return out2;
|
|
3019
|
+
}
|
|
2962
3020
|
function runEnvelope(opts) {
|
|
2963
3021
|
const cwd = opts.cwd ?? process.cwd();
|
|
2964
3022
|
if (!opts.argv.length) {
|
|
@@ -2997,6 +3055,7 @@ function runEnvelope(opts) {
|
|
|
2997
3055
|
}
|
|
2998
3056
|
}
|
|
2999
3057
|
out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
3058
|
+
const spawnTicks = nowTicks();
|
|
3000
3059
|
const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
|
|
3001
3060
|
const agentExit = agent.status ?? 1;
|
|
3002
3061
|
const head = git3(["rev-parse", "HEAD"], cwd).trim();
|
|
@@ -3008,6 +3067,7 @@ function runEnvelope(opts) {
|
|
|
3008
3067
|
tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
|
|
3009
3068
|
return 1;
|
|
3010
3069
|
}
|
|
3070
|
+
const fpBefore = treeFingerprint(cwd);
|
|
3011
3071
|
let diffCode = 0;
|
|
3012
3072
|
if (head !== base) {
|
|
3013
3073
|
out(`
|
|
@@ -3018,10 +3078,27 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
3018
3078
|
const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
|
|
3019
3079
|
out("\n[envelope] pristine verification against the trusted base:");
|
|
3020
3080
|
const verifyCode = runVerify({ cwd, base, cmd: frozenCmd, budget: frozenBudget, policyOverride: frozenPolicy });
|
|
3081
|
+
if (opts.settle && opts.settle > 0) {
|
|
3082
|
+
try {
|
|
3083
|
+
execFileSync5("sleep", [String(opts.settle)]);
|
|
3084
|
+
} catch {
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
const mutatedDuringAdjudication = treeFingerprint(cwd) !== fpBefore;
|
|
3088
|
+
if (mutatedDuringAdjudication) {
|
|
3089
|
+
err("tamperward run: the working tree changed while it was being adjudicated \u2014");
|
|
3090
|
+
err("the verdict below would describe a tree that no longer exists.");
|
|
3091
|
+
}
|
|
3092
|
+
const survivors = survivorsHoldingTree(cwd, spawnTicks);
|
|
3093
|
+
if (survivors.length) {
|
|
3094
|
+
err(`tamperward run: ${survivors.length} process(es) started during this run still hold the`);
|
|
3095
|
+
err(`working tree (pid ${survivors.join(", ")}). A verdict cannot outlive the tree it describes.`);
|
|
3096
|
+
}
|
|
3097
|
+
const notQuiescent = mutatedDuringAdjudication || survivors.length > 0;
|
|
3021
3098
|
const cannot = diffCode === 2 || workCode === 2 || verifyCode === 2;
|
|
3022
|
-
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1;
|
|
3099
|
+
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent;
|
|
3023
3100
|
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";
|
|
3101
|
+
const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : 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
3102
|
out(`
|
|
3026
3103
|
tamperward run \u2014 agent exit ${agentExit}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
|
|
3027
3104
|
return enforcement !== 0 ? enforcement : agentExit;
|
|
@@ -3037,6 +3114,7 @@ function parseRun(args) {
|
|
|
3037
3114
|
else if (a === "--cmd") o.cmd = args[++i];
|
|
3038
3115
|
else if (a === "--budget") o.budget = Number(args[++i]);
|
|
3039
3116
|
else if (a === "--allow-dirty") o.allowDirty = true;
|
|
3117
|
+
else if (a === "--settle") o.settle = Number(args[++i]);
|
|
3040
3118
|
else if (a === "--cwd") o.cwd = args[++i];
|
|
3041
3119
|
else {
|
|
3042
3120
|
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.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",
|