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.
Files changed (2) hide show
  1. package/dist/cli/index.js +129 -5
  2. 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 = join9(cwd, "node_modules");
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.10.4",
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",