tamperward 1.10.3 → 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.
Files changed (2) hide show
  1. package/dist/cli/index.js +86 -13
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -2139,18 +2139,13 @@ function snapshotProtected(cwd, policy, prev) {
2139
2139
  }
2140
2140
  if (st.isDirectory()) walk(r);
2141
2141
  else if (st.isFile() && isProtected(r, policy)) {
2142
- const p = prev?.[r];
2143
- if (p && p.mtimeMs === st.mtimeMs && p.size === st.size && p.mode === st.mode) {
2144
- out2[r] = p;
2145
- } else {
2146
- let hash = "";
2147
- try {
2148
- hash = sha(readFileSync6(join6(cwd, r)));
2149
- } catch {
2150
- hash = "unreadable";
2151
- }
2152
- out2[r] = { hash, mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
2142
+ let hash = "";
2143
+ try {
2144
+ hash = sha(readFileSync6(join6(cwd, r)));
2145
+ } catch {
2146
+ hash = "unreadable";
2153
2147
  }
2148
+ out2[r] = { hash, mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
2154
2149
  }
2155
2150
  }
2156
2151
  };
@@ -2959,11 +2954,69 @@ function parseVerify(args) {
2959
2954
 
2960
2955
  // src/cli/run.ts
2961
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";
2962
2960
  var out = (s) => process.stdout.write(s + "\n");
2963
2961
  var err = (s) => process.stderr.write(s + "\n");
2964
2962
  function git3(args, cwd) {
2965
2963
  return execFileSync5("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28 });
2966
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
+ }
2967
3020
  function runEnvelope(opts) {
2968
3021
  const cwd = opts.cwd ?? process.cwd();
2969
3022
  if (!opts.argv.length) {
@@ -3002,6 +3055,7 @@ function runEnvelope(opts) {
3002
3055
  }
3003
3056
  }
3004
3057
  out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
3058
+ const spawnTicks = nowTicks();
3005
3059
  const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
3006
3060
  const agentExit = agent.status ?? 1;
3007
3061
  const head = git3(["rev-parse", "HEAD"], cwd).trim();
@@ -3013,6 +3067,7 @@ function runEnvelope(opts) {
3013
3067
  tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMENT_FAILED`);
3014
3068
  return 1;
3015
3069
  }
3070
+ const fpBefore = treeFingerprint(cwd);
3016
3071
  let diffCode = 0;
3017
3072
  if (head !== base) {
3018
3073
  out(`
@@ -3023,10 +3078,27 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
3023
3078
  const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
3024
3079
  out("\n[envelope] pristine verification against the trusted base:");
3025
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;
3026
3098
  const cannot = diffCode === 2 || workCode === 2 || verifyCode === 2;
3027
- const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1;
3099
+ const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1 || notQuiescent;
3028
3100
  const enforcement = cannot ? 2 : blocked ? 1 : 0;
3029
- 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";
3030
3102
  out(`
3031
3103
  tamperward run \u2014 agent exit ${agentExit}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
3032
3104
  return enforcement !== 0 ? enforcement : agentExit;
@@ -3042,6 +3114,7 @@ function parseRun(args) {
3042
3114
  else if (a === "--cmd") o.cmd = args[++i];
3043
3115
  else if (a === "--budget") o.budget = Number(args[++i]);
3044
3116
  else if (a === "--allow-dirty") o.allowDirty = true;
3117
+ else if (a === "--settle") o.settle = Number(args[++i]);
3045
3118
  else if (a === "--cwd") o.cwd = args[++i];
3046
3119
  else {
3047
3120
  o.argv = args.slice(i);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "1.10.3",
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",