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.
Files changed (2) hide show
  1. package/dist/cli/index.js +80 -2
  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.10.4",
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",